📜 ⬆️ ⬇️

We help Queryable Provider to deal with interpolated strings

Subtleties of Queryable Provider


Queryable Provider can't handle this:


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

It will not cope with any expression that will use the interpolated string, but will sort it out without difficulty:


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

It is especially painful to edit bugs after enabling ClientEvaluation (exception when calculating on the client), all auto-amp profiles must be subjected to rigorous analysis, to search for this interpolation itself. Let's see what's wrong and offer our solution to the problem.


Fix


Interpolation in Expression Tree is translated as follows (this is the result of the ExpressionStringBuilder.ExpressionToString method, it dropped some nodes, but for us it’s
not fatal):


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

Or so, when the arguments are more than 3


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

It can be concluded that the provider simply did not teach to handle such cases, but he could be taught to reduce these cases to the good old ToString (), which is interpreted like this:


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

I want to write a Visitor which will follow the Expression Tree, namely the MethodCallExpression nodes and replace the Format method with concatenation. If you are familiar with Expression Trees, then you know that C # offers us its visitor to bypass the tree - ExpressionVisitor, for those who are not familiar, it will be interesting .


It is enough to override only the method VisitMethodCall and slightly modify its return value. The method parameter has the type MethodCallExpression, which contains information about the method itself and about the arguments that are passed to it.


Let's break down the task into several parts:


  1. Determine that the Format method is "come" to VisitMethodCall
  2. Replace this method with string concatenation.
  3. Process all Format method overloads that can be received.
  4. Write an extension method in which our visitor will call

The first part is quite simple; Format 4 has overloads that will be built.
in Expression tree


  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) 

We will get using their reflection 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[]))); 

Class, we can now determine that the Format method "came" to MethodCallExpression.


When traversing a tree in VisitMethodCall can "come":


  1. Format method with object arguments
  2. Format method with object [] argument
  3. Not the Format method at all

Some custom pattern maching

So far, conditions of only 3 can be resolved all with the help of if, but we, assuming that in the future we will have to expand this method, we will render all cases into this data structure:


  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 are dealing with. SelectorArgumentFunc is needed in order to bring the Format method arguments to a uniform form, the ReturnFunc method, which will return us the new Expression.


Now we will try to replace the interpolation representation with concatenation, for this we will use the following method:


 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 will be called from Visitor, it is hidden behind ReturnFunc
(when 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; } 

Now we need to write logic to handle different overloads of the Format method, it is rather trivial and is in 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) } }; 

Accordingly, in the VisitMethodCall method, we will traverse this sheet before the first positive FilterPredicate, then convert the arguments (SelectorArgumentFunc) and execute ReturnFunc.


We write Extention, causing that we can replace interpolation.


We can get an Expression, pass it to our Visitor, and then call the IQuryableProvider CreateQuery method of the interface, which will replace the original expression tree with ours:


 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 attention to Cast qu.Provider.CreateQuery (result) of type IQueryable in IQueryable, this is generally standard practice for c # (look at IEnumerable), it arose because of the need to process all generic interfaces in one class that IQueryable / IEnumerable wants to accept and process it using common interface methods.
This could have been avoided by casting T to the base class, this is possible with the help of covariance, but it also imposes some restrictions on the methods of the interface (more about this in the next article).


Total


Apply ReWrite to the expression at the beginning of the 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/442460/


All Articles