Especificações do QueryProvider
O QueryProvider não pode lidar com isso:
var result = _context.Humans .Select(x => $"Name: {x.Name} Age: {x.Age}") .Where(x => x != "") .ToList();
Ele não pode lidar com nenhuma sentença usando uma cadeia de caracteres interpolada, mas lidará facilmente com isso:
var result = _context.Humans .Select(x => "Name " + x.Name + " Age " + x.Age) .Where(x => x != "") .ToList();
O mais doloroso é corrigir os erros após ativar a Avaliação do cliente (exceção para o cálculo do lado do cliente), pois todos os perfis do Automapper devem ser rigorosamente analisados para interpolação. Vamos descobrir o que é o quê e propor a nossa solução para o problema.
Consertando coisas
A interpolação na Expression Tree é convertida desta maneira (isso é resultado do método ExpressionStringBuilder.ExpressionToString , ele pulou alguns dos nós, mas está OK):
Ou assim, se houver mais de 3 argumentos:
Format("Name:{0} Age:{1}", new [] {x.Name, Convert(x.Age, Object)))
Podemos concluir que o provedor simplesmente não foi ensinado a processar esses casos, mas pode ser ensinado a trazer esses casos com o conhecido ToString () , processado da seguinte forma:
((("Name: " + x.Name) + " Age: ") + Convert(x.Age, Object)))
Quero escrever um Visitor que siga a Expression Tree (em particular, os nós MethodCallExpression ) e substitua o método Format por concatenação. Se você conhece as árvores de expressão, sabe que o C # fornece seu próprio visitante para ignorar a árvore - ExpressionVisitor . Mais informações para os interessados .
Tudo o que precisamos é substituir o método VisitMethodCall e modificar levemente o valor retornado. O parâmetro method é do tipo MethodCallExpression , contendo informações sobre o próprio método e os argumentos fornecidos a ele.
Vamos dividir a tarefa em várias partes:
- Determine que é o método Format que entrou no VisitMethodCall;
- Substitua o método pela concatenação de strings;
- Lidar com todas as sobrecargas do método Format que podemos ter;
- Escreva o método de extensão para ligar para o nosso visitante.
A primeira parte é simples: o método Format possui 4 sobrecargas construídas em uma Expression Tree:
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 extraí-los, usando sua reflexão MethodInfo :
private IEnumerable<MethodInfo> FormatMethods => typeof(string).GetMethods().Where(x => x.Name.Contains("Format"))
Excelente. Agora podemos determinar se o método Format "entrou" em MethodCallExpression .
Ao ignorar a árvore no VisitMethodCall , os seguintes métodos podem aparecer:
- Formatar com argumentos de objeto
- Formatar com o argumento do objeto []
- Algo completamente diferente.
Um pouco de correspondência de padrões personalizados
Como temos apenas três condições, podemos tratá-las usando if, mas como assumimos que precisaremos expandir esse método no futuro, transferimos todos os casos para essa estrutura de dados:
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 com qual dos três casos estamos lidando. SelectorArgumentFunc é necessário para trazer todos os argumentos do método Format para uma forma unificada, o método ReturnFunc , que retornará a Expressão completa.
Agora vamos substituir a interpolação pela concatenação e, para isso, precisaremos deste método:
private Expression InterpolationToStringConcat(MethodCallExpression node, IEnumerable<Expression> formatArguments) {
InterpolationToStringConcat será chamado do Visitor , oculto por trá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; }
Agora precisamos escrever lógica para lidar com todas as sobrecargas do método Format . É bastante trivial e está localizado dentro do patternMatchingList :
patternMatchingList = new List<PatternMachingStructure> {
Assim, seguiremos essa lista no método VisitMethodCall até o primeiro FilterPredicate positivo, converteremos os argumentos ( SelectorArgumentFunc ) e executar ReturnFunc .
Vamos escrever um método de extensão que podemos chamar para substituir a interpolação.
Podemos obter uma expressão , entregá-la ao visitante e, em seguida, chamar o método CreateQuery substituindo a árvore de expressões original pela nossa:
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 atenção em converter qu.Provider.CreateQuery (resultado) que possui o método IQueryable em IQueryable <T>. É amplamente usado para C # (veja a interface IEnumerable <T> !), E surgiu da necessidade de lidar com todas as interfaces genéricas com uma classe que deseja obter IQueryable / IEnumerable e com métodos de interface geral.
Poderíamos ter evitado isso trazendo T para uma classe básica (por covariância), mas isso define alguns limites nos métodos de interface.
Resultado
Aplique ReWrite à expressão linq na parte superior do artigo:
var result = _context.Humans .Select(x => $"Name: {x.Name} Age: {x.Age}") .Where(x => x != "") .ReWrite() .ToList();
Github