Ajudando o Queryable Provider a classificar seqüências de caracteres interpoladas

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


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

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:


  1. Determine que foi o método Format que veio para o VisitMethodCall
  2. Substitua esse método pela concatenação de string
  3. Processar todas as sobrecargas do método Format que podem ser recebidas
  4. 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")) //  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[]))); 

Classe, agora podemos determinar que o método Format "veio" para MethodCallExpression.


Ao atravessar uma árvore, o VisitMethodCall pode "vir":


  1. Método de formatação com argumentos de objeto
  2. Método de formatação com argumento object []
  3. 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) { //   //(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; } 

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> { //    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) } }; 

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(); // correct // [Name: "Piter" Age: 19] 

Github

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


All Articles