Aidons QueryProvider à gérer les chaînes interpolées

Spécificités de QueryProvider


QueryProvider ne peut pas gérer cela:


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

Il ne peut traiter aucune phrase à l'aide d'une chaîne interpolée, mais il traitera facilement ceci:


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

La chose la plus pénible est de corriger les bogues après avoir activé ClientEvaluation (exception pour le calcul côté client), car tous les profils Automapper doivent être strictement analysés pour l'interpolation. Voyons ce qui est quoi et proposons notre solution au problème.


Réparer les choses


L'interpolation dans l' arborescence d'expression est convertie comme ceci (c'est le résultat de la méthode ExpressionStringBuilder.ExpressionToString , elle a ignoré certains des nœuds mais c'est OK):


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

Ou comme ça, s'il y a plus de 3 arguments:


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

Nous pouvons conclure que le fournisseur n'a tout simplement pas appris à traiter ces cas, mais il pourrait être enseigné à apporter ces cas avec le ToString () bien connu, traité comme ceci:


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

Je veux écrire un visiteur qui suivra l' arbre d'expression (en particulier, les nœuds MethodCallExpression ) et remplacera la méthode Format par la concaténation. Si vous connaissez les arborescences d'expression, vous savez que C # fournit son propre visiteur pour contourner l'arborescence - ExpressionVisitor . Plus d'informations pour les personnes intéressées .


Tout ce dont nous avons besoin est de remplacer la méthode VisitMethodCall et de modifier légèrement la valeur retournée. Le paramètre de méthode est du type MethodCallExpression , contenant des informations sur la méthode elle-même et les arguments qui lui sont fournis.


Divisons la tâche en plusieurs parties:


  1. Déterminez que c'est la méthode Format qui est entrée dans VisitMethodCall;
  2. Remplacez la méthode par la concaténation des chaînes;
  3. Gérer toutes les surcharges de la méthode Format que nous pouvons avoir;
  4. Écrivez la méthode d'extension pour appeler notre visiteur.

La première partie est simple: la méthode Format a 4 surcharges intégrées dans un arbre d'expression:


  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) 

Extrayons- les, en utilisant leur réflexion MethodInfo :


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

Excellent. Nous pouvons maintenant déterminer si la méthode Format «est entrée» dans MethodCallExpression .


En contournant l'arborescence dans VisitMethodCall , les méthodes suivantes peuvent entrer:


  1. Format avec des arguments d'objet
  2. Format avec l'argument object []
  3. Quelque chose d'autre entièrement.

Un peu de correspondance de motifs personnalisés

Puisque nous n'avons que 3 conditions, nous pouvons les gérer en utilisant if, mais comme nous supposons que nous devrons étendre cette méthode à l'avenir, déchargeons tous les cas dans cette structure de données:


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

En utilisant FilterPredicate, nous déterminons lequel des 3 cas nous traitons. SelectorArgumentFunc est nécessaire pour amener tous les arguments de la méthode Format dans une forme unifiée, la méthode ReturnFunc , qui renverra l' expression complète.


Remplaçons maintenant l'interpolation par la concaténation, et pour cela nous aurons besoin de cette méthode:


 private Expression InterpolationToStringConcat(MethodCallExpression node, IEnumerable<Expression> formatArguments) { //picking the first argument //(example : Format("Name: {0} Age: {1}", x.Name,x.Age) -> //"Name: {0} Age: {1}" var formatString = node.Arguments.First(); // going through the pattern from Format method and choosing every // line between the arguments and pass them to the ExpressionConstant method // example:->[Expression.Constant("Name: "),Expression.Constant(" Age: ")] var argumentStrings = Regex.Split(formatString.ToString(),RegexPattern) .Select(Expression.Constant); // merging them with the formatArguments values // example ->[ConstantExpression("Name: "),PropertyExpression(x.Name), // ConstantExpression("Age: "), // ConvertExpression(PropertyExpression(x.Age), Object)] var merge = argumentStrings.Merge(formatArguments, new ExpressionComparer()); // merging like QueryableProvider merges simple lines concatenation // 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 sera appelé depuis le visiteur , caché derrière 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; } 

Nous devons maintenant écrire une logique pour gérer toutes les surcharges de la méthode Format . C'est plutôt trivial et se trouve à l'intérieur du patternMatchingList :


 patternMatchingList = new List<PatternMachingStructure> { // first three Format overloads new PatternMachingStructure { FilterPredicate = x => FormatMethodsWithObjects.Contains(x), SelectorArgumentsFunc = x => x.Arguments.Skip(1), ReturnFunc = InterpolationToStringConcat }, // last Format overload receiving the array 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 conséquence, nous allons suivre cette liste dans la méthode VisitMethodCall jusqu'au premier FilterPredicate positif, puis convertir les arguments ( SelectorArgumentFunc ) et exécuter ReturnFunc .


Écrivons une méthode d'extension que nous pouvons appeler pour remplacer l'interpolation.


Nous pouvons obtenir une expression , la remettre au visiteur , puis appeler la méthode CreateQuery en remplaçant l' arbre d'expression d' origine par le nôtre:


 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; } 

Faites attention à transtyper qu.Provider.CreateQuery (result) qui a la méthode IQueryable dans IQueryable <T>. Il est largement utilisé pour C # (regardez l'interface IEnumerable <T> !), Et il est venu de la nécessité de gérer toutes les interfaces génériques avec une classe qui veut obtenir IQueryable / IEnumerable , et le gérer en utilisant des méthodes d'interface générales.


Nous aurions pu éviter cela en portant T à une classe de base (par covariance), mais cela fixe certaines limites aux méthodes d'interface.


Résultat


Appliquez ReWrite à l'expression linq en haut de l'article:


 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/fr454860/


All Articles