Ayudando al proveedor queryable a ordenar cadenas interpoladas

Sutilezas del proveedor consultable


El proveedor queryable no puede manejar esto:


var result = _context.Humans .Select(x => $"Name: {x.Name} Age: {x.Age}") .Where(x => x != "") .ToList(); 

No hará frente a ninguna expresión que use la cadena interpolada, pero analizará esto sin dificultad:


  var result = _context.Humans .Select(x => "Name " + x.Name + " Age " + x.Age) .Where(x => x != "") .ToList(); 

Es especialmente doloroso corregir errores después de activar ClientEvaluation (una excepción al calcular en el cliente), todos los perfiles del mapeador automático deben someterse a un análisis riguroso para encontrar esta interpolación. Vamos a averiguar cuál es el problema y ofrecer nuestra solución al problema.


Corregimos


La interpolación en el árbol de expresión se traduce así (este es el resultado del método ExpressionStringBuilder.ExpressionToString, omitió algunos nodos, pero para nosotros es
no fatal):


 //  x.Age  boxing Format("Name:{0} Age:{1}", x.Name, Convert(x.Age, Object))) 

O así, cuando hay más de 3 argumentos


 Format("Name:{0} Age:{1}", new [] {x.Name, Convert(x.Age, Object))) 

Podemos concluir que el proveedor simplemente no aprendió cómo manejar tales casos, pero podría enseñarle a reducir estos casos al viejo ToString (), que se ordena de la siguiente manera:


 ((("Name: " + x.Name) + " Age: ") + Convert(x.Age, Object))) 

Quiero escribir un visitante que pasará por el árbol de expresión, es decir, a través de los nodos de MethodCallExpression y reemplazará el método de formato con concatenación. Si está familiarizado con los árboles de expresión, entonces sabe que C # nos ofrece su visitante para atravesar el árbol: ExpressionVisitor, para aquellos que no están familiarizados será interesante .


Es suficiente anular solo el método VisitMethodCall y modificar ligeramente su valor de retorno. El parámetro del método es de tipo MethodCallExpression, que contiene información sobre el método en sí y sobre los argumentos que se le pasan.


Dividamos la tarea en varias partes:


  1. Determine que fue el método de formato el que vino a VisitMethodCall
  2. Reemplace este método con concatenación de cadenas
  3. Procesar todas las sobrecargas del método de formato que se pueden recibir
  4. Escriba un método de extensión en el que nuestro visitante llame

La primera parte es bastante simple, el método de formato 4 tiene sobrecargas para ser construido
en el árbol de expresión


  public static string Format(string format, object arg0) public static string Format(string format, object arg0,object arg1) public static string Format(string format, object arg0,object arg1,object arg2) public static string Format(string format, params object[] args) 

Obtenemos el reflejo de su MethodInfo


 private IEnumerable<MethodInfo> FormatMethods => typeof(string).GetMethods().Where(x => x.Name.Contains("Format")) //  private IEnumerable<MethodInfo> FormatMethodsWithObjects => FormatMethods .Where(x => x.GetParameters() .All(xx=> xx.ParameterType == typeof(string) || xx.ParameterType == typeof(object))); // private IEnumerable<MemberInfo> FormatMethodWithArrayParameter => FormatMethods .Where(x => x.GetParameters() .Any(xx => xx.ParameterType == typeof(object[]))); 

Clase, ahora podemos determinar que el método Format ha "llegado" a MethodCallExpression.


Al atravesar un árbol, VisitMethodCall puede "venir":


  1. Método de formato con argumentos de objeto
  2. Método de formato con argumento object []
  3. No es el método de formato en absoluto

Mecanizado de patrones un poco personalizado

Hasta ahora, solo se pueden resolver 3 condiciones con la ayuda de if, pero nosotros, suponiendo que en el futuro tendremos que expandir este método, colocamos todos los casos en dicha estructura de datos:


  public class PatternMachingStructure { public Func<MethodInfo, bool> FilterPredicate { get; set; } public Func<MethodCallExpression, IEnumerable<Expression>> SelectorArgumentsFunc { get; set; } public Func<MethodCallExpression, IEnumerable<Expression>, Expression> ReturnFunc { get; set; } } var patternMatchingList = new List<PatternMachingStructure>() 

Usando FilterPredicate, determinamos con cuál de los 3 casos estamos tratando. El SelectorArgumentFunc es necesario para llevar los argumentos del método Format a una forma uniforme, el método ReturnFunc, que nos devolverá la nueva Expresión.


Ahora intentemos reemplazar la representación de interpolación con concatenación, para esto usaremos el siguiente método:


 private Expression InterpolationToStringConcat(MethodCallExpression node, IEnumerable<Expression> formatArguments) { //   //(example : Format("Name: {0} Age: {1}", x.Name,x.Age) -> //"Name: {0} Age: {1}" var formatString = node.Arguments.First(); //      Format    //       ExpressionConstant // example:->[Expression.Constant("Name: "),Expression.Constant(" Age: ")] var argumentStrings = Regex.Split(formatString.ToString(),RegexPattern) .Select(Expression.Constant); //     formatArguments // example ->[ConstantExpression("Name: "),PropertyExpression(x.Name), // ConstantExpression("Age: "), // ConvertExpression(PropertyExpression(x.Age), Object)] var merge = argumentStrings.Merge(formatArguments, new ExpressionComparer()); //  ,  QueryableProvider     // example : -> MethodBinaryExpression //(("Name: " + x.Name) + "Age: " + Convert(PropertyExpression(x.Age),Object)) var result = merge.Aggregate((acc, cur) => Expression.Add(acc, cur, StringConcatMethod)); return result; } 

Se llamará a InterpolationToStringConcat desde Visitor, está oculto detrás de ReturnFunc
(cuando node.Method == string.Format)


 protected override Expression VisitMethodCall(MethodCallExpression node) { var pattern = patternMatchingList.First(x => x.FilterPredicate(node.Method)); var arguments = pattern.SelectorArgumentsFunc(node); var expression = pattern.ReturnFunc(node, arguments); return expression; } 

Ahora necesitamos escribir lógica para manejar diferentes sobrecargas del método Format, es bastante trivial y está ubicado en patternMachingList


 patternMatchingList = new List<PatternMachingStructure> { //    Format new PatternMachingStructure { FilterPredicate = x => FormatMethodsWithObjects.Contains(x), SelectorArgumentsFunc = x => x.Arguments.Skip(1), ReturnFunc = InterpolationToStringConcat }, //   Format,   new PatternMachingStructure { FilterPredicate = x => FormatMethodWithArrayParameter.Contains(x), SelectorArgumentsFunc = x => ((NewArrayExpression) x.Arguments.Last()) .Expressions, ReturnFunc = InterpolationToStringConcat }, // node.Method != Format new PatternMachingStructure() { FilterPredicate = x => FormatMethods.All(xx => xx != x), SelectorArgumentsFunc = x => x.Arguments, ReturnFunc = (node, _) => base.VisitMethodCall(node) } }; 

En consecuencia, en el método VisitMethodCall, revisaremos esta hoja hasta el primer FilterPredicate positivo, luego convertiremos los argumentos (SelectorArgumentFunc) y ejecutaremos ReturnFunc.


Escribamos Extender, llamando a que podemos reemplazar la interpolación.


Podemos obtener Expression, pasarlo a nuestro visitante y luego llamar al método de interfaz IQuryableProvider CreateQuery, que reemplazará el árbol de expresión original con el nuestro:


 public static IQueryable<T> ReWrite<T>(this IQueryable<T> qu) { var result = new InterpolationStringReplacer<T>().Visit(qu.Expression); var s = (IQueryable<T>) qu.Provider.CreateQuery(result); return s; } 

Preste atención a Cast qu.Provider.CreateQuery (resultado) de tipo IQueryable en IQueryable <T>, esta es generalmente una práctica estándar para c # (mire IEnumerable <T>), surgió debido a la necesidad de procesar todas las interfaces genéricas en una clase, quien quiere aceptar IQueryable / IEnumerable y procesarlo utilizando métodos de interfaz comunes.
Esto podría haberse evitado lanzando T a la clase base, esto es posible usando covarianza, pero también impone algunas restricciones en los métodos de interfaz (más sobre esto en el próximo artículo).


Resumen


Aplique ReWrite a la expresión al comienzo del artículo.


  var result = _context.Humans .Select(x => $"Name: {x.Name} Age: {x.Age}") .Where(x => x != "") .ReWrite() .ToList(); // correct // [Name: "Piter" Age: 19] 

Github

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


All Articles