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

字符串插值

QueryProvider的细节 (Specifics of QueryProvider)

QueryProvider can’t deal with this:

QueryProvider无法处理此问题:

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

It can’t deal with any sentence using an interpolated string, but it’ll easily deal with this:

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

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

The most painful thing is to fix bugs after turning on ClientEvaluation (exception for client-side calculation), since all Automapper profiles should be strictly analyzed for interpolation. Let’s find out what’s what and propose our solution to the problem.

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

修理东西 (Fixing things)

Interpolation in the Expression Tree is converted like this (this is a result of ExpressionStringBuilder.ExpressionToString method, it skipped some of the nodes but this is OK):

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

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

Or like this, if there are more than 3 arguments:

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

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

We can conclude that the provider simply was not taught to process these cases, but it might be taught to bring these cases with the well known ToString(), processed like this:

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

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

I want to write a Visitor that will follow the Expression Tree (in particular, the MethodCallExpression nodes) and replace the Format method with concatenation. If you’re familiar with expression trees, you know that C# provides its own visitor to bypass the tree — ExpressionVisitor. More info for those interested.

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

All we need is to overridethe VisitMethodCall method and slightly modify the returned value. The method parameter is of the MethodCallExpression type, containing information on the method itself and the arguments fed to it.

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

Let’s divide the task into several parts:

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

  1. Determine that it’s the Format method that came into the VisitMethodCall;

    确定是VisitMethodCall中的Format方法;

  2. Replace the method with concatenation of strings;

    用字符串串联代替该方法;
  3. Handle all overloads of the Format method we can have;

    处理我们可以拥有的Format方法的所有重载;

  4. Write the extension method to call our visitor.

    编写扩展方法以调用我们的访客。

The first part is simple: the Format method has 4 overloads built in an Expression Tree:

第一部分很简单: 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)

Let’s extract them, using their MethodInfo reflection:

让我们使用它们的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. Now we can determine if the Format method “came in” to MethodCallExpression.

优秀的。 现在我们可以确定Format方法是否“进入” MethodCallExpression

While bypassing the tree in VisitMethodCall, the following methods can come in:

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

  1. Format with object arguments

    带有对象参数的格式

  2. Format with object[] argument

    使用object []参数格式化

  3. Something else entirely.

    完全其他的东西。
一点自定义模式匹配 (A bit of custom pattern matching)

Since we only have 3 conditions, we can handle them using if, but since we assume we’ll need to expand this method in the future, let’s offload all cases into this data structure:

由于我们只有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>()

Using FilterPredicate we determine which of the 3 cases we’re dealing with. SelectorArgumentFunc is needed to bring all arguments of the Format method into a unified shape, the ReturnFunc method, which will return the full Expression.

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

Now let’s replace interpolation with concatenation, and for that we’ll need this method:

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

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 will be called from the Visitor, hidden behind ReturnFunc:

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

Now we need to write logic for handling all Format method overloads. It’s rather trivial and is located inside the patternMatchingList:

现在,我们需要编写逻辑来处理所有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)
     }
};

Accordingly, we’ll follow that list in the VisitMethodCall method until the first positive FilterPredicate, then convert the arguments (SelectorArgumentFunc) and execute ReturnFunc.

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

Let’s write an extension method which we can call to replace interpolation.

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

We can get an Expression, hand it off to the Visitor, and then call the CreateQuery method replacing the original Expression Tree with ours:

我们可以获取一个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; 
}

Pay your attention to cast qu.Provider.CreateQuery(result) that has the IQueryable method in IQueryable<T>. It is widely used for C# (look at IEnumerable<T> interface!), and it came from the need to handle all generic interfaces with one class that wants to get IQueryable/IEnumerable, and handle it using general interface methods.

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

We could’ve avoided that by bringing T up to a baseclass (through covariance), but it sets some limits on interface methods.

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

结果 (Result)

Apply ReWrite to the linq expression on the top of the article:

将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

的GitHub

翻译自: https://habr.com/en/post/454860/

字符串插值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值