Detalles de QueryProvider
QueryProvider no puede lidiar con esto:
var result = _context.Humans .Select(x => $"Name: {x.Name} Age: {x.Age}") .Where(x => x != "") .ToList();
No puede lidiar con ninguna oración usando una cadena interpolada, pero se ocupará fácilmente de esto:
var result = _context.Humans .Select(x => "Name " + x.Name + " Age " + x.Age) .Where(x => x != "") .ToList();
Lo más doloroso es corregir errores después de activar ClientEvaluation (excepción para el cálculo del lado del cliente), ya que todos los perfiles de Automapper deben analizarse estrictamente para la interpolación. Veamos qué es qué y propongamos nuestra solución al problema.
Arreglando cosas
La interpolación en el árbol de expresión se convierte de esta manera (esto es resultado del método ExpressionStringBuilder.ExpressionToString , omitió algunos de los nodos pero esto está bien):
O así, si hay más de 3 argumentos:
Format("Name:{0} Age:{1}", new [] {x.Name, Convert(x.Age, Object)))
Podemos concluir que al proveedor simplemente no se le enseñó a procesar estos casos, pero se le podría enseñar a llevar estos casos con el conocido ToString () , procesado de esta manera:
((("Name: " + x.Name) + " Age: ") + Convert(x.Age, Object)))
Quiero escribir un visitante que siga el árbol de expresión (en particular, los nodos MethodCallExpression ) y reemplace el método de formato con concatenación. Si está familiarizado con los árboles de expresión, sabe que C # proporciona su propio visitante para omitir el árbol: ExpressionVisitor . Más información para los interesados .
Todo lo que necesitamos es anular el método VisitMethodCall y modificar ligeramente el valor devuelto. El parámetro del método es del tipo MethodCallExpression , que contiene información sobre el método en sí y los argumentos que se le proporcionan.
Dividamos la tarea en varias partes:
- Determine que es el método de formato que entró en VisitMethodCall;
- Reemplace el método con concatenación de cadenas;
- Manejar todas las sobrecargas del método de formato que podamos tener;
- Escriba el método de extensión para llamar a nuestro visitante.
La primera parte es simple: el método de formato tiene 4 sobrecargas integradas en un á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)
Vamos a extraerlos, usando su reflexión MethodInfo :
private IEnumerable<MethodInfo> FormatMethods => typeof(string).GetMethods().Where(x => x.Name.Contains("Format"))
Excelente Ahora podemos determinar si el método de formato "entró" a MethodCallExpression .
Al pasar por alto el árbol en VisitMethodCall , los siguientes métodos pueden entrar:
- Formato con argumentos de objeto
- Formato con argumento object []
- Algo completamente diferente.
Un poco de coincidencia de patrones personalizados
Como solo tenemos 3 condiciones, podemos manejarlas usando if, pero dado que suponemos que necesitaremos expandir este método en el futuro, descarguemos todos los casos en esta 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. Se necesita SelectorArgumentFunc para llevar todos los argumentos del método Format a una forma unificada, el método ReturnFunc , que devolverá la Expresión completa.
Ahora reemplacemos la interpolación con concatenación, y para eso necesitaremos este método:
private Expression InterpolationToStringConcat(MethodCallExpression node, IEnumerable<Expression> formatArguments) {
Se llamará a InterpolationToStringConcat desde el visitante , oculto detrás de ReturnFunc :
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 todas las sobrecargas de métodos de formato . Es bastante trivial y se encuentra dentro de patternMatchingList :
patternMatchingList = new List<PatternMachingStructure> {
En consecuencia, seguiremos esa lista en el método VisitMethodCall hasta el primer FilterPredicate positivo, luego convertiremos los argumentos ( SelectorArgumentFunc ) y ejecutaremos ReturnFunc .
Escribamos un método de extensión al que podamos llamar para reemplazar la interpolación.
Podemos obtener una expresión , entregarla al visitante y luego llamar al método CreateQuery reemplazando 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 al elenco qu.Provider.CreateQuery (resultado) que tiene el método IQueryable en IQueryable <T>. Es ampliamente utilizado para C # (¡mire la interfaz IEnumerable <T> !), Y surgió de la necesidad de manejar todas las interfaces genéricas con una clase que quiere obtener IQueryable / IEnumerable , y manejarlo usando métodos de interfaz generales.
Podríamos haber evitado eso llevando a T a una clase base (a través de la covarianza), pero establece algunos límites en los métodos de interfaz.
Resultado
Aplique ReWrite a la expresión de linq en la parte superior del artículo:
var result = _context.Humans .Select(x => $"Name: {x.Name} Age: {x.Age}") .Where(x => x != "") .ReWrite() .ToList();
Github