x != "") .ToList()...">

让我们帮助QueryProvider处理插值字符串

QueryProvider的细节


QueryProvider无法处理此问题:


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

它不能使用插值字符串处理任何句子,但是可以轻松处理:


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

最痛苦的事情是在打开ClientEvaluation (客户端计算例外)后修复错误,因为应严格分析所有Automapper配置文件的插值。 让我们找出是什么,然后提出解决问题的方案。


修理东西


表达式树中的插值是这样转换的(这是ExpressionStringBuilder.ExpressionToString方法的结果,它跳过了一些节点,但这没关系):


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

或类似这样,如果有三个以上的参数:


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

我们可以得出的结论是,根本没有教导提供者如何处理这些情况,但是可以教导它如何将这些情况与众所周知的ToString()一起处理,如下所示:


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

我想编写一个访问者 ,该访问者将遵循表达式树 (尤其是MethodCallExpression节点),并用串联替换Format方法。 如果您熟悉表达式树,则知道C#提供了自己的访问者来绕过ExpressionVisitor树。 对于那些感兴趣的更多信息


我们所需要做的就是重写VisitMethodCall方法并稍微修改返回的值。 method参数是MethodCallExpression类型,包含有关方法本身以及提供给它的参数的信息。


让我们将任务分为几个部分:


  1. 确定这是VisitMethodCall中使用的Format方法;
  2. 用字符串串联代替该方法;
  3. 处理我们可以拥有的Format方法的所有重载;
  4. 编写扩展方法以调用我们的访客。

第一部分很简单: Format方法在Expression Tree中有4个重载:


  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) 

让我们使用它们的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[]))); 

太好了 现在我们可以确定Format方法是否“进入” MethodCallExpression


绕过VisitMethodCall中的树时,可以使用以下方法:


  1. 带有对象参数的格式
  2. 使用object []参数格式化
  3. 完全其他的东西。

一点自定义模式匹配

由于我们只有3个条件,因此可以使用if来处理它们,但是由于我们假设将来需要扩展此方法,因此让我们将所有情况转移到此数据结构中:


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

使用FilterPredicate,我们可以确定要处理的3种情况中的哪一种。 需要SelectorArgumentFunc才能将Format方法的所有参数转换为统一的形状,即ReturnFunc方法,该方法将返回完整的Expression


现在让我们用级联替换插值,为此,我们需要此方法:


 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将从Visitor中调用,隐藏在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; } 

现在,我们需要编写逻辑来处理所有Format方法重载。 它相当琐碎,位于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) } }; 

因此,我们将在VisitMethodCall方法中关注该列表,直到第一个肯定的FilterPredicate为止,然后转换参数( SelectorArgumentFunc )并执行ReturnFunc


让我们写一个扩展方法,我们可以调用它来替换插值。


我们可以获取一个Expression ,将其交给Visitor ,然后调用CreateQuery方法,用我们的方法替换原始的Expression Tree


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

请注意转换在IQueryable <T>中具有IQueryable方法的qu.Provider.CreateQuery(结果) 它广泛用于C#(请看IEnumerable <T>接口!),它来自需要使用一个要获取IQueryable / IEnumerable的类处理所有通用接口,并使用通用接口方法对其进行处理的需求。


我们可以通过将T引入基类(通过协方差)来避免这种情况,但是它对接口方法设置了一些限制。


结果


将ReWrite应用于文章顶部的linq表达式:


 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/zh-CN454860/


All Articles