
Este artigo se concentra no uso duplo da API Expression Trees - para analisar expressões e gerar código. A análise de expressões ajuda a criar estruturas de apresentação (elas também são estruturas de apresentação da DSL interna da linguagem orientada a problemas) e a geração de código permite criar dinamicamente funções eficazes - conjuntos de instruções definidas pelas estruturas de apresentação.
Vou demonstrar a criação dinâmica de iteradores de propriedades: serializar, copiar, clonar, iguais . Usando serialize como exemplo, mostrarei como otimizar a serialização (em comparação com os serializadores de fluxo) na situação clássica em que o conhecimento "preliminar" é usado para melhorar o desempenho. A idéia é que chamar o serializador de streaming sempre perderá a função "non-streaming", sabendo exatamente quais nós da árvore para contornar. Ao mesmo tempo, esse serializador é criado "não à mão", mas dinamicamente, mas de acordo com regras de desvio predefinidas. O DSL interno proposto resolve o problema de uma descrição compacta das regras para percorrer as estruturas em árvore dos objetos por suas propriedades / propriedades (e, no caso geral: atravessar a árvore de computação com o nome de nós) . O benchmark do serializador é modesto, mas é importante, pois contribui para a abordagem criada em torno do uso de um Internal DSL Include específico (um dialeto de Include / ThenInclude from EF Core ) e o uso do Internal DSL como um todo, a persuasão necessária.
1. Introdução
Compare:
var p = new Point(){X=-1,Y=1};
O segundo método é obviamente mais rápido (os nós são conhecidos e estão "amontoados no código"), enquanto o método é obviamente mais complicado. Mas quando você obtém esse código como uma função (gerada e compilada dinamicamente), a complexidade fica oculta (mesmo o que fica claro fica oculto
onde está reflexão e onde está o tempo de execução da geração de código).
var p = new Point(){X=-1,Y=1};
Aqui, o JsonManager.ComposeFormatter
é a ferramenta real . A regra pela qual o desvio de estrutura é gerado durante a serialização não é óbvia, mas soa assim "com os parâmetros padrão, pois os tipos de valor personalizados percorrem todos os campos do primeiro nível". Se você configurá-lo explicitamente:
Esta é a descrição dos metadados através de Inclui DSL. O DSL esclareceu a análise dos prós e contras da descrição de metadados, mas agora ignorando a forma de metadados de gravação, enfatizo que o C # fornece a capacidade de compilar e compilar o "serializador ideal" usando as Árvores de Expressão.
Como ele faz isso - muitos códigos e guia de geração de código das Árvores de Expressão ...transição do formatter
para o serilizer
(até agora sem árvores de expressão):
Func<StringBuilder, Point, bool> serializer = ...
Por sua vez, o serializer
é construído assim (se definido com código estático):
Expression<Func<StringBuilder, Point, bool>> serializerExpression = SerializeAssociativeArray(sb, p, (sb1, t1) => SerializeValueProperty(sb1, t1, "X", o => oX, SerializeValueToString), (sb4, t4) => SerializeValueProperty(sb1, t1, "Y", o => oY, SerializeValueToString) ); Func<StringBuilder, Point, bool> serializer = serializerExpression.Compile();
Por que é tão "funcional", por que você não pode serializar dois campos através de um ponto e vírgula? Em resumo: porque esta expressão pode ser atribuída a uma variável do tipo Expression<Func<StringBuilder, Box, bool>>
, mas um ponto-e-vírgula não é permitido.
Por que não consegui escrever diretamente Func<StringBuilder, Point, bool> serializer = (sb,p)=>SerializeAssociativeArray(sb,p,...
? É possível, mas não estou demonstrando a criação de um delegado, mas de um assembly (neste caso, código estático) árvore de expressão, com uma compilação para o delegado no futuro, em uso prático, serializerExpression
será definido de uma maneira completamente diferente - dinamicamente (abaixo).
Mas o que é importante na própria solução: SerializeAssociativeArray
aceita uma matriz de params Func<..> propertySerializers
acordo com o número de nós a serem ignorados. Ignorar alguns deles pode ser definido pelos serializadores SerializeValueProperty de folhas (aceitando o formatador SerializeValueToString
) e outros novamente pelo SerializeAssociativeArray
(ou seja, ramificações) e, portanto, um iterador (árvore) do percurso é construído.
Se Point continha a propriedade NextPoint:
var @delegate = SerializeAssociativeArray(sb, p, (sb1, t1) => SerializeValueProperty(sb1, t1, "X", o => oX, SerializeValueToString), (sb4, t4) => SerializeValueProperty(sb1, t1, "Y", o => oY, SerializeValueToString), (sb4, t4) => SerializeValueProperty(sb1, t1, "NextPoint", o => o.NextPoint, (sb4, t4) =>SerializeAssociativeArray(sb1, p1, (sb1, t1) => SerializeValueProperty(sb2, t2, "X", o => oX, SerializeValueToString), (sb4, t4) => SerializeValueProperty(sb2, t2, "Y", o => oY, SerializeValueToString) ) ) );
O dispositivo das três funções SerializeAssociativeArray
, SerializeValueProperty
, SerializeValueToString
não SerializeValueToString
complicado:
Serializar ... public static bool SerializeAssociativeArray<T>(StringBuilder stringBuilder, T t, params Func<StringBuilder, T, bool>[] propertySerializers) { var @value = false; stringBuilder.Append('{'); foreach (var propertySerializer in propertySerializers) { var notEmpty = propertySerializer(stringBuilder, t); if (notEmpty) { if (!@value) @value = true; stringBuilder.Append(','); } }; stringBuilder.Length--; if (@value) stringBuilder.Append('}'); return @value; } public static bool SerializeValueProperty<T, TProp>(StringBuilder stringBuilder, T t, string propertyName, Func<T, TProp> getter, Func<StringBuilder, TProp, bool> serializer) where TProp : struct { stringBuilder.Append('"').Append(propertyName).Append('"').Append(':'); var value = getter(t); var notEmpty = serializer(stringBuilder, value); if (!notEmpty) stringBuilder.Length -= (propertyName.Length + 3); return notEmpty; } public static bool SerializeValueToString<T>(StringBuilder stringBuilder, T t) where T : struct { stringBuilder.Append(t); return true; }
Muitos detalhes não são fornecidos aqui (suporte de lista, tipo de referência e nulo). E, no entanto, está claro que realmente recebo json na saída, e todo o resto são ainda mais das funções padrão SerializeArray
, SerializeNullable
, SerializeRef
.
Era uma árvore de expressão estática, sem dinâmica, sem avaliação em C # .
Você pode ver como a Árvore de Expressões é criada dinamicamente em duas etapas:
Etapa 1 - descompilador observe o código atribuído pela Expression<T>

É claro que isso irá surpreendê-lo pela primeira vez. Nada está claro, mas você pode ver como as quatro primeiras linhas formam algo como:
("sb","t") .. SerializeAssociativeArray..
Em seguida, a conexão com o código fonte é capturada. E deve ficar claro que, se você dominar esse registro (combinando 'Expression.Const', 'Expression.Parameter', 'Expression.Call', 'Expression.Lambda' etc ...), poderá realmente compor dinamicamente - qualquer desvio de nós (com base em metadados). Isso é eval em c # .
O mesmo código de descompilador, mas compilado pelo homem.
Somente o autor do intérprete é obrigado a se envolver neste bordado de miçangas. Todas essas artes permanecem dentro da biblioteca de serialização . É importante aprender a idéia de que você pode fornecer bibliotecas que geram dinamicamente funções eficientes compiladas em C # (e .NET Standard).
No entanto, um serializador de streaming ultrapassará uma função gerada dinamicamente se você chamar a compilação todas as vezes antes da serialização (a compilação dentro do ComposeFormatter
é uma operação cara), mas você pode salvar o link e reutilizá-lo:
static Func<Point, string> formatter = JsonManager.ComposeFormatter<Point>(); public string Get(Point p){
Se você precisar criar e salvar o serializador de tipo anônimo para reutilização, precisará de infraestrutura adicional:
static CachedFormatter cachedFormatter = new CachedFormatter(); public string Get(List<Point> list){
Depois disso, levamos em conta com confiança a primeira microoptimização para nós mesmos e acumulamos, acumulamos, acumulamos ... Quem é a piada, quem não é, mas antes de passar à pergunta de que o novo serializador é novo, eu fixo a vantagem óbvia - será mais rápido.
O que em troca?
O intérprete DSL Inclui em serilizar (e da mesma maneira que é possível em iteradores iguais, copiar, clonar - e isso também será necessário) exigiu os seguintes custos:
1 - custos da infraestrutura para armazenar links para código compilado.
Esses custos geralmente não são necessários, assim como o uso de Árvores de Expressão na compilação - o intérprete pode criar um serializador em reflexos e até lamber tanto que chegará à velocidade em termos de serializadores de fluxo (a propósito, copie, clone e iguais não são coletados através de árvores de expressão, nem são lambidos, não existe tal tarefa, ao contrário de ultrapassar o ServiceStack e o Json.NET dentro da estrutura da bem-compreendida tarefa de otimizar a serialização no json - uma condição necessária para apresentar uma nova solução).
2 - você precisa ter em mente vazamentos de abstrações e um problema semelhante: alterações na semântica em comparação com as soluções existentes.
Por exemplo, Point e IEnumerable precisam de dois serializadores diferentes para serializar.
var formatter1 = JsonManager.ComposeFormatter<Point>(); var formatter2 = JsonManager.ComposeEnumerableFormatter<Point>();
Ou: "o fechamento funciona?". Funciona, apenas o nó precisa definir um nome (exclusivo):
string DATEFORMAT= "YYYY"; var formatter3 = JsonManager.ComposeFormatter<Record>( chain => chain .Include(i => i.RecordId) .Include(i => i.CreatedAt.ToString(DATEFORMAT) , "CreatedAt"); );
Esse comportamento é determinado pelo dispositivo interno do interpretador ComposeFormatter
.
Custos desse tipo são inevitáveis. Além disso, verifica-se que, expandindo a funcionalidade e o escopo do DSL interno, os vazamentos de abstração também aumentam. Certamente oprimirá o desenvolvedor do Internal DSL; aqui você precisa estocar um clima filosófico.
Para o usuário, os vazamentos de abstração são superados pelo conhecimento dos detalhes técnicos do DSL interno ( o que esperar? ) E a riqueza da funcionalidade de um DSL específico e de seus intérpretes (o que, em troca? ). Portanto, a resposta à pergunta: "vale a pena criar e usar DSL interno?", Só pode haver uma história sobre a funcionalidade de uma DSL específica - sobre todos os seus detalhes e conveniências e suas possíveis aplicações (intérpretes), ou seja, uma história sobre como superar custos.
Com tudo isso em mente, volto à eficácia de um determinado DSL Inclui.
Uma eficiência significativamente maior é alcançada quando o objetivo é substituir o triplo (DTO, transformar em DTO, serializar DTO) por uma função de serialização gerada e detalhada localmente. No final, o dualismo função-objeto permite que você diga "DTO é uma função" e defina uma meta: aprender a definir uma função DTO.
A serialização deve ser configurada:
- Árvore de desvio (para descrever os nós através dos quais a serialização ocorrerá, pela maneira como isso resolve o problema dos links circulares), no caso de folhas - atribua um formatador (por tipo).
- A regra para incluir folhas (se não estiverem especificadas) - propriedade vs campos? somente leitura?
- Para poder especificar uma ramificação (um nó com navegação) e uma planilha não é apenas MemberExpression (
e=>e.Name
), mas geralmente qualquer função (`e => e.Name.ToUpper ()," MyMemberName ") - defina o formatador como um específico nó
Outras opções para aumentar a flexibilidade:
- serializar uma planilha contendo uma string json "como está" (formatador de string especial);
- definir formatadores para grupos, ou seja, ramos inteiros, neste ramo como este - de outro de uma maneira diferente (por exemplo, data aqui com tempo e neste sem tempo).
Em todos os lugares, as construções envolvidas: desvio de árvore, ramo, folha e tudo isso pode ser escrito usando o DSL Inclui.
DSL Inclui
Como todos estão familiarizados com o EF Core, o significado das seguintes expressões deve ser capturado imediatamente (este é um subconjunto do xpath).
Aqui estão os nós "com navegação" - "ramificações".
A resposta para a pergunta que nós "deixa" (campos / propriedades) está incluída na árvore assim definida - nenhuma. Para incluir folhas, você deve listá-las explicitamente:
Include<User> include2 = chain=> chain .Include(e => e.UserName)
Ou adicione dinamicamente pela regra, através de um intérprete especializado:
Aqui, a regra é uma regra que pode ser selecionada por ChainNode.Type, ou seja, por tipo de expressão retornada pelo nó (ChainNode - representação interna do DSL Inclui, que será discutido mais adiante) propriedades (MemberInfo) para participação na serialização, por exemplo somente propriedade, ou apenas propriedade de leitura / gravação, ou apenas aquelas para as quais existe um formatador, você pode selecionar em uma lista de tipos e até a própria expressão include pode definir uma regra (se listar nós de folha - ou seja, a forma de junção de árvore) .
Ou ... deixe a critério do intérprete do usuário, que decide o que fazer com os nós. Inclui DSL é apenas um registro de metadados - como interpretar esse registro depende do intérprete. Ele pode interpretar os metadados como quiser, até ignorá-los. Alguns intérpretes executam a ação eles mesmos, enquanto outros constroem uma função pronta para executá-la (via Expression Tree ou mesmo Reflection.Emit). Uma boa DSL interna é projetada para uso universal e a existência de muitos intérpretes, cada um com suas próprias especificidades, sua própria abstração vaza.
Código usando DSL interno pode ser muito diferente do que era antes.
Fora da caixa
Integração com o EF Core.
A tarefa em execução é "interromper links cíclicos", para inicializar apenas o que é especificado na expressão de inclusão:
static CachedFormatter cachedFormatter1 = new CachedFormatter(); string GetJson() { using (var dbContext = GetEfCoreContext()) { string json = EfCoreExtensions.ToJsonEf<User>(cachedFormatter1, dbContext, chain=>chain .IncludeAll(e => e.Roles) .ThenIncludeAll(e => e.Privileges)); } }
ToJsonEf
aceita a sequência de navegação, ao serializar, a usa (seleciona folhas pela regra "padrão para EF Core", isto é, propriedade pública de leitura / gravação), está interessada no modelo - onde string / json usa formatadores de campo para inserir como está por padrão (byte [] por string, data e hora em ISO, etc.). Portanto, ele deve executar o IQuaryable de baixo de si mesmo.
No caso em que o resultado é transformado, as regras mudam - não é necessário usar o DSL Inclui para especificar a navegação (se não houver reutilização da regra), outro intérprete é usado e a configuração ocorre localmente:
static CachedFormatter cachedFormatter1 = new CachedFormatter(); string GetJson() { using (var dbContext = GetEfCoreContext()) { var json = dbContext.ParentRecords
É claro que todos esses detalhes, tudo isso é "por padrão", só podem ser lembrados se você realmente precisar e / ou se esse for seu próprio intérprete. Por outro lado, voltamos novamente às vantagens: o DTO não é manchado pelo código, é especificado por uma função específica, os intérpretes são universais. O código está ficando menor - isso é bom.
É necessário avisar : embora pareça que o conhecimento preliminar esteja sempre disponível no ASP, e um serializador de streaming não é uma coisa muito necessária no mundo da Web, onde até bancos de dados transmitem dados em json, mas o uso do DSL Include no ASP MVC não é a história mais fácil . Como combinar programação funcional com ASP MVC merece um estudo separado.
Neste artigo, vou me limitar aos meandros do DSL Inclui, mostrarei novas funcionalidades e vazamentos de abstrações para mostrar que o problema de análise de "custos e aquisições" é realmente esgotável.
Mais DSL Inclui
Include<Point> include = chain => chain.Include(e=>eX).Include(e=>eY);
Isso difere do EF Core Inclui funções estáticas que não podem ser atribuídas a variáveis e passadas como parâmetros. O DSL Include em si nasceu da necessidade de passar "include" para minha implementação do modelo de Repositório sem degradar as informações de tipo que apareceriam quando fossem padronizadas em strings.
A diferença mais dramática ainda está no compromisso. EF Core Inclui - inclusão de propriedades de navegação (nós das filiais), DSL Inclui - registro da travessia de uma árvore de cálculos, atribuindo um nome (caminho) ao resultado de cada cálculo.
A representação interna do EF Core Include é uma lista de seqüências de caracteres recebidas pelo MemberExpression.Member (a expressão especificada por e=>User.Name
pode ser apenas [MemberExpression] ( https://msdn.microsoft.com/en-us/library/system.linq.expressions. memberexpression (v = vs. 110) .aspx e, nas visualizações internas, apenas a linha Name
é salva.
No DSL Inclui, a representação interna são as classes ChainNode e ChainMemberNode que salvam a expressão inteira (por exemplo, e=>User.Name
), que, como está, está embutida na Árvore de Expressão. É precisamente disso que o DSL Inclui suporta campos e tipos de valor do usuário e chamadas de função:
Execução de funções:
Include<User> include = chain => chain .Include(i => i.UserName) .Include(i => i.Email.ToUpper(),"EAddress");
O que fazer com isso depende do intérprete. CreateFormatter- retornará {"UserName": "John", "EAddress": "JOHN@MAIL.COM"}
A execução também pode ser útil para definir a travessia sobre estruturas anuláveis.
Include<StrangePointF> include = chain => chain .Include(e => e.NextPoint)
O DSL Inclui também possui uma pequena entrada para a solução alternativa de vários níveis ThenIncluding.
Include<User> include = chain => chain .Include(i => i.UserName) .IncludeAll(i => i.Groups)
compare com
Include<User> include = chain => chain .Include(i => i.UserName) .IncludeAll(i => i.Groups) .ThenInclude(e => e.GroupName) .IncludeAll(i => i.Groups) .ThenInclude(e => e.GroupDescription) .IncludeAll(i => i.Groups) .ThenInclude(e => e.AdGroup);
E aqui também há um vazamento de abstração. Se eu escrevi a navegação neste formulário, eu deveria saber como funciona um intérprete que chamará QuaryableExtensions. E ele traduz as chamadas para Include e ThenInclude para Include "string". O que pode importar (você deve ter em mente).
Álgebra inclui expressões .
As expressões de inclusão podem ser:
Compare var b1 = InlcudeExtensions.IsEqualTo(include1, include2); var b2 = InlcudeExtensions.IsSubTreeOf(include1, include2); var b3 = InlcudeExtensions.IsSuperTreeOf(include1, include2);
Clonar var include2 = InlcudeExtensions.Clone(include1);
Mesclar var include3 = InlcudeExtensions.Merge(include1, include2);
Converter em listas XPath - todos os caminhos para folhas IReadOnlyCollection<string> paths1 = InlcudeExtensions.ListLeafXPaths(include);
etc.
A boa notícia é: não há vazamentos de abstrações, o nível de pura abstração é alcançado aqui. Existem metadados e funcionam com metadados.
Dialética
O DSL inclui permite alcançar um novo nível de abstração, mas no momento da conquista, é necessária uma necessidade de ir para o próximo nível: gerar as próprias expressões de inclusão.
Nesse caso, a geração de DSL como uma cadeia fluente não é necessária, você só precisa criar estruturas de representação interna.
var root = new ChainNode(typeof(Point)); var child = new ChainPropertyNode( typeof(int), expression: typeof(Point).CreatePropertyLambda("X"), memberName:"X", isEnumerable:false, parent:root ); root.Children.Add("X", child);
Você também pode passar estruturas de apresentação para intérpretes. Por que, então, o registro DSL fluente inclui? Esta é uma pergunta puramente especulativa, cuja resposta: porque na prática - desenvolver uma representação interna (e também se desenvolve) é obtida apenas com o desenvolvimento da DSL (isto é, um pequeno registro expressivo conveniente para código estático). Mais uma vez, isso será dito mais perto da conclusão.
Copiar, Clonar, Igual a
Todos os itens acima são verdadeiros para os intérpretes de inclusão-expressão que implementam copiar , clonar e iguais iteradores.
Igual aComparação apenas nas folhas da expressão Incluir.
Problema semântico oculto: avalie ou não a ordem na lista
Include<User> include = chain=>chain.Include(e=>e.UserId).IncludeAll(e=>e.Groups).ThenInclude(e=>e.GroupId) bool b1 = ObjectExtensions.Equals(user1, user2, include); bool b2 = ObjectExtensions.EqualsAll(userList1, userList2, include);
ClonarPasse pelos nós de expressão. As propriedades correspondentes à regra são copiadas.
Include<User> include = chain=>chain.Include(e=>e.UserId).IncludeAll(e=>e.Groups).ThenInclude(e=>e.GroupId) var newUser = ObjectExtensions.Clone(user1, include, leafRule1); var newUserList = ObjectExtensions.CloneAll(userList1, leafRule1);
Pode haver um intérprete que selecionará a folha de inclusões. Por que isso é feito - através de uma regra separada? O que era semelhante à semântica de ObjectExtensions.Copy
CopiarPassar por nós - um ramo de expressão e identificação por nós de folha. As propriedades correspondentes à regra são copiadas (semelhante ao Clone).
Include<User> include = chain=>chain.IncludeAll(e=>e.Groups); ObjectExtensions.Copy(user1, user2, include, supportedLeafsRule); ObjectExtensions.CopyAll(userList1, userList2, include, supportedLeafsRule);
Pode haver um intérprete que selecionará a folha de inclusões. Por que isso é feito - através de uma regra separada? ObjectExtensions.Copy ( — include , supportedLeafsRule — ).
copy / clone :
- readonly , Tuple<,> Anonymous Type. , .
- (. IEnumerable ) — public .
- expression include-, — .
- " " .
DSL , .. . , Tuple<,>
, .. c readonly , ValueTuple<,>
c writabale ( ).
, ( Expression Trees) Includes — . Include DSL .
Detach, FindDifferences ..
run-time, .cs ?
.cs , , run-time :
- ( , , source control).
- , , , — .
- .
- " ". dev time , : "" "" , "" , , "" .
Roslyn', . Typescript ( DTO , .. ) — DSL Includes Roslyn' ( ) — typescript ( ). " " " " .cs ( Expression Trees).
: run time — , . ( Expression Trees).
Expression Trees
Internal DSL Expression Tree :
LambdaExpression.Compile
Lambda . , . , "" expression tree, CallExpression — LambdaExpression, (. LambdaExpression) ConstantExpression. , " /" — , Expression Trees.
ssmbly , ( 10 ) ( assembly , — ). , , , — .
, ( ), , . : . — — .cs .
— 600 15 . JSON.NET, ServiceStack reflection' GetProperties().
dslComposeFormatter — ComposeFormatter , .
BenchmarkDotNet =v0.10.14, OS=Windows 10.0.17134
Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=2.1.300
Method | Mean | Error | StdDev | Min | Max | Median | Allocated |
---|
dslComposeFormatter | 2.208 ms | 0.0093 ms | 0.0078 ms | 2.193 ms | 2.220 ms | 2.211 ms | 849.47 KB |
JsonNet_Default | 2.902 ms | 0.0160 ms | 0.0150 ms | 2.883 ms | 2.934 ms | 2.899 ms | 658.63 KB |
JsonNet_NullIgnore | 2.944 ms | 0.0089 ms | 0.0079 ms | 2.932 ms | 2.960 ms | 2.942 ms | 564.97 KB |
JsonNet_DateFormatFF | 3.480 ms | 0.0121 ms | 0.0113 ms | 3.458 ms | 3.497 ms | 3.479 ms | 757.41 KB |
JsonNet_DateFormatSS | 3.880 ms | 0.0139 ms | 0.0130 ms | 3.854 ms | 3.899 ms | 3.877 ms | 785.53 KB |
ServiceStack_SerializeToString | 4.225 ms | 0.0120 ms | 0.0106 ms | 4.201 ms | 4.243 ms | 4.226 ms | 805.13 KB |
fake_expressionManuallyConstruted | 54.396 ms | 0.1758 ms | 0.1644 ms | 54.104 ms | 54.629 ms | 54.383 ms | 7401.58 KB |
fake_expressionManuallyConstruted — expression ( ).
DSL : DSL ; Internal DSL run-time .
Expression Tree .NET Standard .
Expression Trees Internal DSL Fluent API. # .
fluent ( Expression Trees), Internal DSL # fluent, "" Expression Trees.
Expression Trees DSL Includes ( , ), / run-time — (run-time ).
Internal DSL : - serialize , copy , clone , equals "" . , " ", . : includes ( ) , ( , ).
Conclusão
DSL Includes DTO — ( json). , , , " ", . = .
Internal DSL , DSL, Internal DSL ( Expression) ( Expression Tree).
DSL Includes json ComposeFormatter DashboardCodes.Routines nuget GitHub.