Subtilités du fournisseur interrogeable
Le fournisseur interrogeable ne peut pas gérer cela:
var result = _context.Humans .Select(x => $"Name: {x.Name} Age: {x.Age}") .Where(x => x != "") .ToList();
Il ne gèrera aucune expression qui utilisera la chaîne interpolée, mais il analysera cela sans difficulté:
var result = _context.Humans .Select(x => "Name " + x.Name + " Age " + x.Age) .Where(x => x != "") .ToList();
Il est particulièrement pénible de corriger les bogues après avoir activé ClientEvaluation (une exception lors du calcul sur le client), tous les profils du mappeur automatique doivent être soumis à une analyse rigoureuse pour trouver cette interpolation. Voyons quel est le problème et proposons notre solution au problème.
Nous corrigeons
L'interpolation dans l'arbre d'expression est traduite comme ceci (c'est le résultat de la méthode ExpressionStringBuilder.ExpressionToString, il a omis certains nœuds, mais pour nous, il
pas mortel):
Ou alors, quand 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 à gérer de tels cas, mais il pourrait lui apprendre à réduire ces cas au bon vieux ToString (), qui est trié comme suit:
((("Name: " + x.Name) + " Age: ") + Convert(x.Age, Object)))
Je veux écrire un visiteur qui passera par l'arbre d'expression, à savoir par les nœuds de MethodCallExpression et remplacera la méthode Format par la concaténation. Si vous êtes familier avec les arbres d'expression, alors vous savez que C # nous offre son visiteur pour parcourir l'arbre - ExpressionVisitor, pour ceux qui ne le sont pas, ce sera intéressant .
Il suffit de remplacer uniquement la méthode VisitMethodCall et de modifier légèrement sa valeur de retour. Le paramètre de méthode est de type MethodCallExpression, qui contient des informations sur la méthode elle-même et sur les arguments qui lui sont transmis.
Divisons la tâche en plusieurs parties:
- Déterminez que c'est la méthode Format qui est venue à VisitMethodCall
- Remplacez cette méthode par une concaténation de chaînes
- Traiter toutes les surcharges de la méthode Format qui peuvent être reçues
- Écrivez une méthode d'extension dans laquelle notre visiteur appellera
La première partie est assez simple, la méthode Format 4 a des surcharges à construire
dans l'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)
Nous utilisons la réflexion de leur MethodInfo
private IEnumerable<MethodInfo> FormatMethods => typeof(string).GetMethods().Where(x => x.Name.Contains("Format"))
Classe, nous pouvons maintenant déterminer que la méthode Format est "arrivée" à MethodCallExpression.
Lors de la traversée d'un arbre, VisitMethodCall peut "venir":
- Formater la méthode avec des arguments d'objet
- Formater la méthode avec l'argument object []
- Pas du tout la méthode Format
Un peu d'usinage de motifs personnalisé
Jusqu'à présent, seules 3 conditions peuvent être résolues à l'aide de if, mais nous, en supposant qu'à l'avenir nous devrons étendre cette méthode, mettons tous les cas dans une telle 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>()
À l'aide de FilterPredicate, nous déterminons lequel des 3 cas nous avons affaire. Le SelectorArgumentFunc est nécessaire pour amener les arguments de la méthode Format à une forme uniforme, la méthode ReturnFunc, qui nous renverra la nouvelle expression.
Essayons maintenant de remplacer la représentation d'interpolation par la concaténation, pour cela nous allons utiliser la méthode suivante:
private Expression InterpolationToStringConcat(MethodCallExpression node, IEnumerable<Expression> formatArguments) {
InterpolationToStringConcat sera appelé depuis Visitor, il est caché derrière ReturnFunc
(lorsque 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; }
Maintenant, nous devons écrire une logique pour gérer différentes surcharges de la méthode Format, elle est assez triviale et se trouve dans patternMachingList
patternMatchingList = new List<PatternMachingStructure> {
Par conséquent, dans la méthode VisitMethodCall, nous allons parcourir cette feuille jusqu'au premier FilterPredicate positif, puis convertir les arguments (SelectorArgumentFunc) et exécuter ReturnFunc.
Écrivons Extention, appelant que nous pouvons remplacer l'interpolation.
Nous pouvons obtenir l'expression, la transmettre à notre visiteur, puis appeler la méthode d'interface IQuryableProvider CreateQuery, qui remplacera l'arborescence d'expression d'origine par la 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 à Cast qu.Provider.CreateQuery (résultat) de type IQueryable dans IQueryable <T>, il s'agit généralement d'une pratique standard pour c # (regardez IEnumerable <T>), elle est survenue en raison de la nécessité de traiter toutes les interfaces génériques dans une classe, qui souhaite accepter IQueryable / IEnumerable et le traiter à l'aide de méthodes d'interface communes.
Cela aurait pu être évité en transtypant T dans la classe de base, cela est possible en utilisant la covariance, mais cela impose également quelques restrictions sur les méthodes d'interface (plus à ce sujet dans le prochain article).
Résumé
Appliquer la réécriture à l'expression au début de l'article
var result = _context.Humans .Select(x => $"Name: {x.Name} Age: {x.Age}") .Where(x => x != "") .ReWrite() .ToList();
Github