DSL interno y árboles de expresión: creación dinámica de funciones de serialización, copia, clonación, igualdad (Parte I)


Este artículo se centra en el uso dual de la API de árboles de expresión : para analizar expresiones y generar código. El análisis de expresiones ayuda a construir estructuras de presentación (también son estructuras de presentación del lenguaje interno orientado a problemas DSL interno ), y la generación de código le permite crear dinámicamente funciones efectivas: conjuntos de instrucciones especificadas por estructuras de presentación.


Demostraré la creación dinámica de iteradores de propiedades: serializar, copiar, clonar, iguales . Usando serializar como ejemplo, le mostraré cómo optimizar la serialización (en comparación con los serializadores de transmisión) en la situación clásica en la que se utiliza el conocimiento "preliminar" para mejorar el rendimiento. La idea es que llamar al serializador de transmisión siempre perderá la función de "no transmisión", sabiendo exactamente qué nodos de árbol deben moverse. Al mismo tiempo, dicho serializador se crea "no a mano" sino de forma dinámica, sino de acuerdo con las reglas de omisión predefinidas. El DSL Inernal propuesto resuelve el problema de una descripción compacta de las reglas para atravesar las estructuras de árbol de los objetos por sus propiedades / propiedades (y en el caso general: atravesar el árbol de cómputo con el nombre de los nodos) . El punto de referencia del serializador es modesto, pero es importante porque agrega al enfoque construido alrededor del uso de un DSL interno específico incluye (un dialecto de Incluir / EntoncesIncluir de EF Core ) y el uso del DSL interno en su conjunto, la persuasión necesaria.


Introduccion


Compara:


var p = new Point(){X=-1,Y=1}; // which has better performance ? var json1 = JsonConvert.SerializeObject(p); var json2 = $"{{\"X\":{pX}, \"Y\":{pY}}}"; 

El segundo método es obviamente más rápido (los nodos son conocidos y están "abarrotados de código"), mientras que el método es, por supuesto, más complicado. Pero cuando obtiene este código como una función (generada y compilada dinámicamente), la complejidad está oculta (incluso lo que no está claro está oculto
dónde está la reflexión y dónde está el tiempo de ejecución de generación de código).


 var p = new Point(){X=-1,Y=1}; // which has better performance ? var json1 = JsonConvert.SerializeObject(p); var formatter = JsonManager.ComposeFormatter<Point>(); var json2 = formatter(p); 

Aquí JsonManager.ComposeFormatter es la herramienta real . La regla por la cual se genera la omisión de estructura durante la serialización no es obvia, pero suena así "con los parámetros predeterminados, para los tipos de valores personalizados rodean todos los campos del primer nivel". Si lo configura explícitamente:


 //    var formatter2 = JsonManager.ComposeFormatter<Point>( chain=>chain .Include(e=>eX) .Include(e=>eY) // DSL Includes ) 

Esta es la descripción de los metadatos a través de DSL Incluye. DSL ha iluminado el análisis de los pros y los contras de describir metadatos, pero ahora ignorando la forma de grabar metadatos, enfatizo que C # proporciona la capacidad de compilar y compilar el "serializador ideal" usando Expression Trees.


Cómo lo hace: una gran cantidad de código y una guía de generación de código de Expression Trees ...

transición del formatter al serilizer (hasta ahora sin árboles de expresión):


  Func<StringBuilder, Point, bool> serializer = ... // later string formatter(Point p) { var stringBuilder = new StringBuilder(); serializer(stringBuilder, p); return stringBuilder.ToString(); } 

A su vez, el serializer se construye así (si está configurado con 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 qué es tan "funcional", por qué no puedes serializar dos campos a través de un punto y coma? En resumen: porque esta expresión se puede asignar a una variable del tipo Expression<Func<StringBuilder, Box, bool>> , pero no se permite el punto y coma.
¿Por qué no podría escribir directamente Func<StringBuilder, Point, bool> serializer = (sb,p)=>SerializeAssociativeArray(sb,p,... ? Es posible, pero no estoy demostrando la creación de un delegado, sino un ensamblado (en este caso, código estático) árbol de expresión, con una compilación para el delegado en el futuro, en uso práctico, serializerExpression se establecerá de una manera completamente diferente, dinámicamente (a continuación).


Pero lo que es importante en la solución en sí: SerializeAssociativeArray acepta una matriz de params Func<..> propertySerializers acuerdo con el número de nodos que se omitirán. Eludir algunos de ellos puede establecerlo SerializeValueProperty serializadores de hojas (aceptando el formateador SerializeValueToString ), y otros nuevamente por SerializeAssociativeArray (es decir, ramas), y así se construye un iterador (árbol) del recorrido.


Si Point contenía la propiedad 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) ) ) ); 

El dispositivo de las tres funciones SerializeAssociativeArray , SerializeValueProperty , SerializeValueToString no 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; } 

Aquí no se dan muchos detalles (soporte de lista, tipo de referencia y anulable). Y sin embargo, está claro que realmente obtengo json en la salida, y todo lo demás es aún más que las funciones estándar SerializeArray , SerializeNullable , SerializeRef .


Era un árbol de expresión estático, no dinámico, no evaluado en C # .


Puede ver cómo Expression Tree se construye dinámicamente en dos pasos:


Paso 1: el descompilador observa el código asignado por Expression<T>



Por supuesto, esto te sorprenderá la primera vez. Nada está claro, pero puedes ver cómo las primeras cuatro líneas forman algo como:


 ("sb","t") .. SerializeAssociativeArray.. 

Luego se captura la conexión con el código fuente. Y debe quedar claro que si domina un registro de este tipo (combinando 'Expression.Const', 'Expression.Parameter', 'Expression.Call', 'Expression.Lambda', etc. ) realmente puede componer dinámicamente, cualquier bypass de nodos (basado en metadatos). Esto es eval en C # .


Paso 2: sigue este enlace ,


El mismo código de descompilador, pero compilado por man.


Solo el autor del intérprete está obligado a participar en este bordado de cuentas. Todas estas artes permanecen dentro de la biblioteca de serialización . Es importante conocer la idea de que puede proporcionar bibliotecas que generen dinámicamente funciones compiladas eficientes en C # (y .NET Standard).


Sin embargo, un serializador de transmisión superará una función generada dinámicamente si llama a la compilación cada vez antes de la serialización (la compilación dentro de ComposeFormatter es una operación costosa), pero puede guardar el enlace y reutilizarlo:


 static Func<Point, string> formatter = JsonManager.ComposeFormatter<Point>(); public string Get(Point p){ // which has better performance ? var json1 = JsonConvert.SerializeObject(p); var json2 = formatter(p); return json2; } 

Si necesita crear y guardar un serializador de tipo anónimo para su reutilización, necesita infraestructura adicional:


 static CachedFormatter cachedFormatter = new CachedFormatter(); public string Get(List<Point> list){ // there json formatter will be build only for first call // and assigned to cachedFormatter.Formatter // in all next calls cachedFormatter.Formatter will be used. // since building of formatter is determenistic it is lock free var json3 = list.Select(e=> {X:eX, Sum:e.X+EY}) .ToJson(cachedFormatter, e=>e.Sum); return json3; } 

Después de eso, tenemos en cuenta con confianza la primera microoptimización para nosotros y acumulamos, acumulamos, acumulamos ... Quién es el chiste, quién no, pero antes de pasar a la pregunta de que el nuevo serializador es nuevo, soluciono la ventaja obvia: será más rápido.


¿Qué a cambio?


El DSL incluye intérprete en serilize (y de la misma manera es posible en iteradores equals, copy, clone, y esto también se tratará) requirió los siguientes costos:


1 - costos de la infraestructura para almacenar enlaces a código compilado.


Estos costos generalmente no son necesarios, como lo es el uso de Expression Trees con compilación: el intérprete puede crear un serializador en reflejos e incluso lamerlo tanto que se acercará a la velocidad en términos de serializadores de flujo (por cierto, copie, clone y los iguales no se recopilan a través de árboles de expresión, ni se lamen, no existe tal tarea, a diferencia de superar a ServiceStack y Json.NET en el marco de la tarea bien entendida de optimizar la serialización en json, una condición necesaria para presentar una nueva solución).


2 : debe tener en cuenta las fugas de abstracciones y un problema similar: cambios en la semántica en comparación con las soluciones existentes.


Por ejemplo, Point e IEnumerable necesitan dos serializadores diferentes para serializar.

 var formatter1 = JsonManager.ComposeFormatter<Point>(); var formatter2 = JsonManager.ComposeEnumerableFormatter<Point>(); // but not // var formatter2 = JsonManager.ComposeEnumerableFormatter<List<Point>>(); 

O: "¿funciona el cierre?". Funciona, solo el nodo necesita establecer un nombre (único):


 string DATEFORMAT= "YYYY"; var formatter3 = JsonManager.ComposeFormatter<Record>( chain => chain .Include(i => i.RecordId) .Include(i => i.CreatedAt.ToString(DATEFORMAT) , "CreatedAt"); ); 

Este comportamiento lo dicta el dispositivo interno del intérprete ComposeFormatter .


Los costos de este tipo son el mal inevitable. Además, se descubre que al expandir la funcionalidad y expandir el alcance del DSL interno, también aumentan las fugas de abstracción. Ciertamente oprimirá al desarrollador de DSL interno, aquí debe abastecerse de un estado de ánimo filosófico.


Para el usuario, las fugas de abstracción se superan mediante el conocimiento de los detalles técnicos de DSL interno ( ¿qué esperar? ) Y la riqueza de la funcionalidad de un DSL particular y sus intérpretes ( ¿qué a cambio? ). Por lo tanto, la respuesta a la pregunta: "¿vale la pena crear y usar DSL interno?", Solo puede haber una historia sobre la funcionalidad de un DSL particular, sobre todos sus detalles y comodidades, y sus posibles aplicaciones (intérpretes), es decir. Una historia sobre la superación de los costos.


Con todo esto en mente, vuelvo a la efectividad de un DSL en particular.


Se logra una eficiencia significativamente mayor cuando el objetivo es reemplazar el triple (DTO, transformar en DTO, serializar DTO) con una función de serialización generada y detallada localmente. Al final, el objeto de función dualismo le permite decir "DTO es una función" y establecer un objetivo: aprender cómo establecer una función DTO.


La serialización debe configurarse:


  1. Desvíe el árbol (para describir los nodos a través de los cuales tendrá lugar la serialización, por cierto, esto resuelve el problema de los enlaces circulares), en el caso de las hojas, asigne un formateador (por tipo).
  2. La regla para incluir hojas (si no se especifican): ¿propiedad frente a campos? solo lectura?
  3. Para poder especificar tanto una rama (un nodo con navegación) como una hoja no es solo MemberExpression ( e=>e.Name ), sino generalmente cualquier función (`e => e.Name.ToUpper ()," MyMemberName ") - establece el formateador en un formato específico nodo.

Otras opciones para aumentar la flexibilidad:


  1. serializar una hoja que contiene una cadena json "tal cual" (formateador de cadena especial);
  2. establecer formateadores en grupos, es decir ramas enteras, en esta rama como esta, en otra de una manera diferente (por ejemplo, fechas aquí con tiempo y en esta sin tiempo).

En todas partes, las construcciones involucradas: omiten el árbol, la rama, la hoja y todo esto se puede escribir usando DSL Incluye.


DSL incluye


Como todos están familiarizados con EF Core, el significado de las siguientes expresiones debe capturarse de inmediato (este es un subconjunto de xpath).


  // DSL Includes Include<User> include1 = chain=> chain .IncludeAll(e => e.Groups) .IncludeAll(e => e.Roles) .ThenIncludeAll(e => e.Privileges) // EF Core syntax // https://docs.microsoft.com/en-us/ef/core/querying/related-data var users = context.Users .Include(blog => blog.Groups) .Include(blog => blog.Roles) .ThenInclude(blog => blog.Privileges); 

Aquí están los nodos "con navegación" - "ramas".
La respuesta a la pregunta de qué nodos "hojas" (campos / propiedades) se incluyen en el árbol así definido: ninguno. Para incluir hojas, debe enumerarlas explícitamente:


 Include<User> include2 = chain=> chain .Include(e => e.UserName) // leaf member .IncludeAll(e => e.Groups) .ThenInclude(e => e.GroupName) // leaf member .IncludeAll(e => e.Roles) .ThenInclude(e => e.RoleName) // leaf member .IncludeAll(e => e.Roles) .ThenIncludeAll(e => e.Privileges) .ThenInclude(e => e.PrivilegeName) // leaf member 

O agregue dinámicamente por la regla, a través de un intérprete especializado:


 // Func<ChainNode, MemberInfo> rule = ... var include2 = IncludeExtensions.AppendLeafs(include1, rule); 

Aquí la regla es una regla que puede seleccionarse mediante ChainNode. por tipo de expresión devuelta por el nodo (ChainNode - representación interna de DSL incluye, que se discutirá más adelante) propiedades (MemberInfo) para participar en la serialización, p. solo la propiedad, o solo la propiedad de lectura / escritura, o solo aquellas para las que hay un formateador, puede seleccionar de una lista de tipos, e incluso la expresión de inclusión en sí misma puede establecer una regla (si enumera nodos de hoja, es decir, la forma de unión de árbol) .


O ... déjelo a discreción del intérprete de usuario, quien decide qué hacer con los nodos. DSL incluye es solo un registro de metadatos; la forma de interpretar este registro depende del intérprete. Puede interpretar los metadatos como quiera, hasta ignorarlos. Algunos intérpretes realizarán la acción ellos mismos, mientras que otros construirán una función lista para realizarlos (a través de Expression Tree, o incluso Reflection.Emit). Un buen DSL interno está diseñado para uso universal y la existencia de muchos intérpretes, cada uno de los cuales tiene sus propios detalles, sus propias filtraciones de abstracción.
El código que usa DSL interno puede ser muy diferente de lo que era antes.


Fuera de la caja


Integración con EF Core.
La tarea en ejecución es "cortar los enlaces cíclicos", para iniciar solo lo que se especifica en la expresión de inclusión:


 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 acepta la secuencia de navegación, cuando se serializa, la usa (selecciona las hojas según la regla "predeterminada para EF Core", es decir, la propiedad de lectura / escritura pública), está interesado en el modelo, donde string / json usa formateadores de campo para insertar como está por defecto (byte [] por cadena, fecha y hora en ISO, etc.). Por lo tanto, debe realizar IQuardable por debajo de sí mismo.


En el caso de que el resultado se transforme, las reglas cambian: no hay necesidad de usar DSL Incluye para especificar la navegación (si no hay reutilización de la regla), se usa otro intérprete y la configuración se realiza localmente:


 static CachedFormatter cachedFormatter1 = new CachedFormatter(); string GetJson() { using (var dbContext = GetEfCoreContext()) { var json = dbContext.ParentRecords // back to EF core includes // but .Include(include1) also possible .IncludeAll(e => e.Roles) .ThenIncludeAll(e => e.Privileges) .Select(e => new { FieldA: e.FieldA, FieldJson:"[1,2,3]", Role: e.Roles().First() }) .ToJson(cachedFormatter1, chain => chain.Include(e => e.Role), LeafRuleManager.DefaultEfCore, config: rules => rules .AddRule<string[]>(GetStringArrayFormatter) .SubTree( chain => chain.Include(e => e.FieldJson), stringAsJsonLiteral: true) // json as is .SubTree( chain => chain.Include(e => e.Role), subRules => subRules .AddRule<DateTime>( dateTimeFormat: "YYYMMDD", floatingPointFormat: "N2" ) ), ), useToString: false, // no default ToString for unknown leaf type (throw exception) dateTimeFormat: "YYMMDD", floatingPointFormat: "N2" } } 

Está claro que todos estos detalles, todo esto es "por defecto", pueden tenerse en cuenta solo si realmente lo necesita y / o si este es su propio intérprete. Por otro lado, volvemos nuevamente a las ventajas: el DTO no está manchado por el código, está especificado por una función específica, los intérpretes son universales. El código se está volviendo más pequeño, eso es bueno.


Es necesario advertir : aunque parece que el conocimiento preliminar siempre está disponible en ASP, y un serializador de transmisión no es algo muy necesario en el mundo de la web, donde incluso las bases de datos transmiten datos en json, pero el uso de DSL incluye en ASP MVC no es la historia más fácil . Cómo combinar la programación funcional con ASP MVC merece un estudio aparte.


En este artículo me limitaré a las complejidades de DSL Incluye, mostraré tanto la nueva funcionalidad como la fuga de abstracciones para mostrar que el problema del análisis de "costos y adquisiciones" es realmente agotable.


Más DSL incluye


 Include<Point> include = chain => chain.Include(e=>eX).Include(e=>eY); 

Esto difiere de EF Core Incluye funciones estáticas integradas que no pueden asignarse a variables y pasarse como parámetros. DSL incluye a sí mismo nació de la necesidad de pasar "incluir" en mi implementación de la plantilla de repositorio sin degradar la información de tipo que habría aparecido cuando se estandarizaron en cadenas.


La diferencia más dramática aún está en la cita. EF Core incluye - inclusión de propiedades de navegación (nodos de ramas), DSL incluye - registro de recorrido de un árbol de cálculos, asignando un nombre (ruta) al resultado de cada cálculo.


La representación interna de EF Core incluye es una lista de cadenas recibidas por MemberExpression.Member (La expresión especificada por e=>User.Name solo puede ser [MemberExpression] ( https://msdn.microsoft.com/en-us/library/system.linq.expressions. memberexpression (v = vs. 110) .aspx y en vistas internas solo se guarda la línea de Name ).


En DSL Incluye, la representación interna son las clases ChainNode y ChainMemberNode que guardan la expresión completa (por ejemplo, e=>User.Name ), que, tal como está, está integrada en el Árbol de expresiones. Es precisamente a partir de esto que DSL incluye admite campos y tipos de valores de usuario y llamadas a funciones:


Ejecución de funciones:


 Include<User> include = chain => chain .Include(i => i.UserName) .Include(i => i.Email.ToUpper(),"EAddress"); 

Qué hacer con esto depende del intérprete. CreateFormatter- devolverá {"UserName": "John", "EAddress": "JOHN@MAIL.COM"}


La ejecución también puede ser útil para establecer un recorrido sobre estructuras anulables.


 Include<StrangePointF> include = chain => chain .Include(e => e.NextPoint) // NextPoint is nullable struct .ThenIncluding(e => e.Value.X) .ThenInclude(e => e.Value.Y); // but not this way (abstraction leak) // Include<StrangePointF> include // = chain => chain // now this can throw an exception // .Include(e => e.NextPoint.Value) // .ThenIncluding(e => eX) // .ThenInclude(e => eY); 

DSL incluye también tiene una entrada corta para la solución de varios niveles ThenIncluding.


 Include<User> include = chain => chain .Include(i => i.UserName) .IncludeAll(i => i.Groups) // ING-form - doesn't change current node .ThenIncluding(e => e.GroupName) // leaf .ThenIncluding(e => e.GroupDescription) // leaf .ThenInclude(e => e.AdGroup); // leaf 

comparar con


 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); 

Y aquí también hay una fuga de abstracción. Si escribiera la navegación en este formulario, sabría cómo funciona un intérprete que llamará QuaryableExtensions. Y traduce las llamadas a Incluir y luego a Incluir para incluir "cadena". Qué puede importar (debes tenerlo en cuenta).


El álgebra incluye expresiones .


Incluir expresiones puede ser:


Comparar
 var b1 = InlcudeExtensions.IsEqualTo(include1, include2); var b2 = InlcudeExtensions.IsSubTreeOf(include1, include2); var b3 = InlcudeExtensions.IsSuperTreeOf(include1, include2); 

Clon
 var include2 = InlcudeExtensions.Clone(include1); 

Fusionar
 var include3 = InlcudeExtensions.Merge(include1, include2); 

Convertir a listas XPath - Todos los caminos a las hojas
 IReadOnlyCollection<string> paths1 = InlcudeExtensions.ListLeafXPaths(include); // as xpaths IReadOnlyCollection<string[]> paths2 = InlcudeExtensions.ListLeafKeyPaths(include); // as string[] 

etc.


La buena noticia es: no hay fugas de abstracciones, el nivel de abstracción pura se alcanza aquí. Hay metadatos y trabajo con metadatos.


Dialéctica


DSL incluye le permite alcanzar un nuevo nivel de abstracción, pero en el momento del logro, se forma una necesidad de pasar al siguiente nivel: generar expresiones de inclusión ellos mismos.


En este caso, no es necesario generar DSL como una cadena fluida, solo necesita crear estructuras de representación internas.


 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); // or there is number of extension methods eg: var child = root.AddChild("X"); Include<Point> include = ChainNodeExtensions.ComposeInclude<Point>(root); 

También puede pasar estructuras de presentación a intérpretes. ¿Por qué, entonces, incluye el registro DSL fluido? Esta es una pregunta puramente especulativa, la respuesta a la cual: porque en la práctica, para desarrollar una representación interna (y también se desarrolla) solo se obtiene con el desarrollo de DSL (es decir, un breve registro expresivo conveniente para el código estático). Una vez más, esto se dirá más cerca de la conclusión.


Copiar, clonar, igual


Todo lo anterior es cierto para los intérpretes de inclusión de expresiones que implementan copia , clonación , iteradores iguales .


Igual a

Comparación solo en hojas de la expresión Incluir.
Problema semántico oculto: evaluar o no ordenar en la 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); 

Clon

Pase a través de los nodos de expresión. Se copian las propiedades que coinciden con la regla.


 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); 

Puede haber un intérprete que seleccionará la hoja de incluye. ¿Por qué se hace a través de una regla separada? Lo que era similar a la semántica de ObjectExtensions.Copy


Copia

Pase a través de nodos: una rama de expresión e identificación por nodos hoja. Se copian las propiedades que coinciden con la regla (similar a Clonar).


 Include<User> include = chain=>chain.IncludeAll(e=>e.Groups); ObjectExtensions.Copy(user1, user2, include, supportedLeafsRule); ObjectExtensions.CopyAll(userList1, userList2, include, supportedLeafsRule); 

Puede haber un intérprete que seleccionará la hoja de incluye. ¿Por qué se hace a través de una regla separada? ObjectExtensions.Copy ( — include , supportedLeafsRule — ).


copy / clone :


  1. readonly , Tuple<,> Anonymous Type. , .
  2. (. IEnumerable ) — public .
  3. expression include-, — .
  4. " " .

DSL , .. . , Tuple<,> , .. c readonly , ValueTuple<,> c writabale ( ).


, ( Expression Trees) Includes — . Include DSL .


Detach, FindDifferences ..


run-time, .cs ?


.cs , , run-time :


  1. ( , , source control).
  2. , , , — .
  3. .
  4. " ". dev time , : "" "" , "" , , "" .

Roslyn', . Typescript ( DTO , .. ) — DSL Includes Roslyn' ( ) — typescript ( ). " " " " .cs ( Expression Trees).


: run time — , . ( Expression Trees).


Expression Trees


Internal DSL Expression Tree :


  1. LambdaExpression.Compile Lambda . , . , "" expression tree, CallExpression — LambdaExpression, (. LambdaExpression) ConstantExpression. , " /" — , Expression Trees.


  2. 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


MethodMeanErrorStdDevMinMaxMedianAllocated
dslComposeFormatter2.208 ms0.0093 ms0.0078 ms2.193 ms2.220 ms2.211 ms849.47 KB
JsonNet_Default2.902 ms0.0160 ms0.0150 ms2.883 ms2.934 ms2.899 ms658.63 KB
JsonNet_NullIgnore2.944 ms0.0089 ms0.0079 ms2.932 ms2.960 ms2.942 ms564.97 KB
JsonNet_DateFormatFF3.480 ms0.0121 ms0.0113 ms3.458 ms3.497 ms3.479 ms757.41 KB
JsonNet_DateFormatSS3.880 ms0.0139 ms0.0130 ms3.854 ms3.899 ms3.877 ms785.53 KB
ServiceStack_SerializeToString4.225 ms0.0120 ms0.0106 ms4.201 ms4.243 ms4.226 ms805.13 KB
fake_expressionManuallyConstruted54.396 ms0.1758 ms0.1644 ms54.104 ms54.629 ms54.383 ms7401.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 ( ) , ( , ).


Conclusión


DSL Includes DTO — ( json). , , , " ", . = .


Internal DSL , DSL, Internal DSL ( Expression) ( Expression Tree).


DSL Includes json ComposeFormatter DashboardCodes.Routines nuget GitHub.

Source: https://habr.com/ru/post/es419759/


All Articles