クエリ可能なプロバイダーが補間された文字列を整理するのを支援する

クエリ可能なプロバイダーの微妙さ



クエリ可能なプロバイダーはこれを処理できません。







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(クライアントでの計算時の例外)をオンにした後、バグを修正するのは特に苦痛です。自動マッパーのすべてのプロファイルは、この補間を見つけるために厳密な分析を行う必要があります。 問題が何であるかを把握し、問題の解決策を提供しましょう。







修正します



Expression Treeの補間はこのように変換されます(これは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)))
      
      





Expression Tree、つまりMethodCallExpressionのノードを通過し、Formatメソッドを連結に置き換える訪問者を作成したいと思います。 Expression Treeに精通している場合、C#はツリーを走査するための訪問者-ExpressionVisitorを提供することを知っています。







VisitMethodCallメソッドのみをオーバーライドし、戻り値をわずかに変更するだけで十分です。 メソッドパラメータはMethodCallExpression型であり、メソッド自体および渡される引数に関する情報が含まれます。







タスクをいくつかの部分に分けましょう。







  1. VisitMethodCallに来たのはFormatメソッドであると判断する
  2. このメソッドを文字列連結に置き換えます
  3. 取得可能なFormatメソッドのすべてのオーバーロードを処理します
  4. ビジターが呼び出すExtensionメソッドを作成します


最初の部分は非常に単純で、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は「来る」可能性があります。







  1. オブジェクト引数を含むフォーマットメソッド
  2. オブジェクト[]引数を持つフォーマットメソッド
  3. 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を使用して、3つのケースのどれを扱うかを決定しますSelectorArgumentFuncは、Formatメソッドの引数を統一されたフォーム、ReturnFuncメソッドに変換するために必要です。







では、補間表現を連結に置き換えてみましょう。このため、このメソッドを使用します。







 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を実行します。







Extentionを書きましょう。補間を置き換えることができると呼びます。







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]
      
      





Github








All Articles