Sutilezas do provedor consultável
O provedor de consulta 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 lidará com nenhuma expressão que usará a sequência interpolada, mas analisará isso sem dificuldade:
var result = _context.Humans .Select(x => "Name " + x.Name + " Age " + x.Age) .Where(x => x != "") .ToList();
É especialmente doloroso corrigir bugs após ativar a Avaliação do Cliente (uma exceção ao calcular no cliente), todos os perfis do mapeador automático devem ser submetidos a uma análise rigorosa para encontrar essa interpolação. Vamos descobrir qual é o problema e oferecer nossa solução para o problema.
Nós corrigimos
A interpolação na Expression Tree é traduzida assim (este é o resultado do método ExpressionStringBuilder.ExpressionToString, ele omitiu alguns nós, mas para nós
não fatal):
Ou então, quando 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 aprendeu a lidar com esses casos, mas eles poderiam ensiná-lo a reduzi-los ao bom e velho ToString (), que é classificado assim:
((("Name: " + x.Name) + " Age: ") + Convert(x.Age, Object)))
Quero escrever um Visitor que percorra a Árvore de Expressão, ou seja, através dos nós da MethodCallExpression e substitua o método Format pela concatenação. Se você está familiarizado com as Árvores de Expressão, sabe que o C # nos oferece seu visitante por percorrer a árvore - ExpressionVisitor, para quem não estiver familiarizado, será interessante .
Basta substituir apenas o método VisitMethodCall e modificar levemente seu valor de retorno. O parâmetro method é do tipo MethodCallExpression, que contém informações sobre o próprio método e sobre os argumentos que são passados para ele.
Vamos dividir a tarefa em várias partes:
- Determine que foi o método Format que veio para o VisitMethodCall
- Substitua esse método pela concatenação de string
- Processar todas as sobrecargas do método Format que podem ser recebidas
- Escreva um método de extensão no qual nosso visitante ligará
A primeira parte é bem simples, o método Format 4 possui sobrecargas a serem construídas
na árvore de expressões
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)
Nós começamos a usar o reflexo de seu MethodInfo
private IEnumerable<MethodInfo> FormatMethods => typeof(string).GetMethods().Where(x => x.Name.Contains("Format"))
Classe, agora podemos determinar que o método Format "veio" para MethodCallExpression.
Ao atravessar uma árvore, o VisitMethodCall pode "vir":
- Método de formatação com argumentos de objeto
- Método de formatação com argumento object []
- Nem o método Format
Um pouco personalizado Maching Padrão
Até agora, apenas três condições podem ser resolvidas com a ajuda de if, mas nós, assumindo que no futuro teremos que expandir esse método, colocamos todos os casos em uma 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.O SelectorArgumentFunc é necessário para trazer os argumentos do método Format para um formulário uniforme, o método ReturnFunc, que retornará a nova Expressão para nós.
Agora vamos tentar substituir a representação de interpolação por concatenação, para isso usaremos o seguinte método:
private Expression InterpolationToStringConcat(MethodCallExpression node, IEnumerable<Expression> formatArguments) {
InterpolationToStringConcat será chamado de Visitor, está oculto atrás de ReturnFunc
(quando 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; }
Agora precisamos escrever lógica para lidar com diferentes sobrecargas do método Format, é bastante trivial e está localizado em patternMachingList
patternMatchingList = new List<PatternMachingStructure> {
Assim, no método VisitMethodCall, percorreremos esta planilha até o primeiro FilterPredicate positivo, depois converteremos os argumentos (SelectorArgumentFunc) e executar ReturnFunc.
Vamos escrever Extention, chamando que podemos substituir a interpolação.
Podemos obter Expression, passá-lo para o Visitor e, em seguida, chamar o método da interface IQuryableProvider CreateQuery, que substituirá a árvore de expressão 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 ao Cast qu.Provider.CreateQuery (resultado) do tipo IQueryable em IQueryable <T>, isso geralmente é uma prática padrão para c # (veja IEnumerable <T>); surgiu devido à necessidade de processar todas as interfaces genéricas em uma classe, quem deseja aceitar IQueryable / IEnumerable e processá-lo usando métodos de interface comuns.
Isso poderia ter sido evitado ao converter T para a classe base, isso é possível usando covariância, mas também impõe algumas restrições aos métodos de interface (mais sobre isso no próximo artigo).
Sumário
Aplique ReWrite à expressão no início do artigo
var result = _context.Humans .Select(x => $"Name: {x.Name} Age: {x.Age}") .Where(x => x != "") .ReWrite() .ToList();
Github