可查询提供者的微妙之处
Queryable Provider无法处理此问题:
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之后(在客户端上进行计算时是一个例外)纠正错误特别痛苦,应该对自动映射器的所有配置文件进行严格的分析才能找到此插值。 让我们找出问题所在,并提供解决方案。
我们纠正
表达式树中的插值是这样翻译的(这是ExpressionStringBuilder.ExpressionToString方法的结果,它省略了一些节点,但是对我们来说
不致命):
// x.Age boxing Format("Name:{0} Age:{1}", x.Name, Convert(x.Age, Object)))
大约有3个以上的参数
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,其中包含有关方法本身以及传递给它的参数的信息。
让我们将任务分为几个部分:
- 确定是VisitMethodCall的Format方法
- 将此方法替换为字符串连接
- 处理可以接收的Format方法的所有重载
- 写一个扩展方法,访问者将在其中调用
第一部分很简单,格式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")) // 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[])));
类,现在我们可以确定Format方法已经“继承”了MethodCallExpression。
遍历树时,VisitMethodCall可能“出现”:
- 带对象参数的格式化方法
- 使用object []参数的格式化方法
- 根本不是Format方法
一点自定义模式处理
到目前为止,在if的帮助下只能解决3个条件,但是我们假定将来我们将不得不扩展此方法,请将所有情况放在这样的数据结构中:
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,我们可以确定要处理的三种情况中的哪一种,需要SelectorArgumentFunc才能将Format方法的参数统一化为ReturnFunc方法,该方法将向我们返回新的Expression。
现在让我们尝试用级联替换插值表示,为此,我们将使用以下方法:
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将从Visitor调用,它隐藏在ReturnFunc后面
(当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; }
现在我们需要编写逻辑来处理Format方法的不同重载,这很简单,并且位于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) } };
因此,在VisitMethodCall方法中,我们将遍历此工作表,直到第一个为正的FilterPredicate,然后转换参数(SelectorArgumentFunc)并执行ReturnFunc。
让我们写扩展,调用我们可以替换插值的方法。
我们可以获取Expression,将其传递给Visitor,然后调用IQuryableProvider CreateQuery接口方法,该方法将原始表达式树替换为我们的表达式树:
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类型的Cast qu.Provider.CreateQuery(结果),这通常是c#的标准做法(请查看IEnumerable <T>),这是因为需要在一个类中处理所有通用接口,谁想要接受IQueryable / IEnumerable,并使用常见的接口方法对其进行处理。
可以通过将T强制转换为基类来避免这种情况,使用协方差可以实现这一点,但是它也对接口方法施加了一些限制(在下一篇文章中对此有更多的限制)。
总结
将ReWrite应用于本文开头的表达式
var result = _context.Humans .Select(x => $"Name: {x.Name} Age: {x.Age}") .Where(x => x != "") .ReWrite() .ToList(); // correct // [Name: "Piter" Age: 19]