式を使用してデータベースからデータをフィルタリングする

この記事はStackOverflowの応答に基づいています。 まず、直面している問題の説明から始めます。 データベースには、UIのテーブルに表示する必要があるエンティティがいくつかあります。 Entity Frameworkは、データベースへのアクセスに使用されます。 これらのエンティティのフィールドによるこれらのテーブルのフィルターがあります。 パラメータでエンティティをフィルタリングするコードを記述する必要があります。



たとえば、UserとProductという2つのエンティティがあります。



public class User { public int Id { get; set; } public string Name { get; set; } } public class Product { public int Id { get; set; } public string Name { get; set; } }
      
      





ユーザーを名前でフィルタリングし、製品を名前でフィルタリングする必要があるとしましょう。 各エンティティをフィルタリングするメソッドを作成します。



 public IQueryable<User> FilterUsersByName(IQueryable<User> users, string text) { return users.Where(user => user.Name.Contains(text)); } public IQueryable<Product> FilterProductsByName(IQueryable<Product> products, string text) { return products.Where(product => product.Name.Contains(text)); }
      
      





これらの2つのメソッドはほぼ同一であり、データがフィルター処理されるエンティティのプロパティのみが異なることにすぐに気付きます。 フィルタリングが必要なフィールドがそれぞれ何十もあるエンティティが数十ある場合、これはいくつかの困難につながります:コードの維持の複雑さ、軽率なコピー、その結果、開発が遅くエラーの可能性が高くなります。 言い換えれば、ファウラーは匂いを発し始めます 。 コードを複製する代わりに、もっと普遍的なものを書きたいと思います。 例:



 public IQueryable<User> FilterUsersByName(IQueryable<User> users, string text) { return FilterContainsText(users, user => user.Name, text); } public IQueryable<Product> FilterProductsByName(IQueryable<Product> products, string text) { return FilterContainsText(products, propduct => propduct.Name, text); } public IQueryable<TEntity> FilterContainsText<TEntity>(IQueryable<TEntity> entities, Func<TEntity, string> getProperty, string text) { return entities.Where(entity => getProperty(entity).Contains(text)); }
      
      





残念ながら、フィルタリングしようとすると



 public void TestFilter() { using (var context = new Context()) { var filteredProducts = FilterProductsByName(context.Products, "name").ToArray(); } }
      
      





次に、「テストメソッドExpressionTests.ExpressionTest.TestFilterが例外をスローしました:

System.NotSupportedException :LINQ式ノードタイプ'Invoke'は 、LINQ to Entities ではサポートされていません 。 なぜなら







表現



何がうまくいかなかったかを理解してみましょう。



メソッドは、Expression <Func <TEntity、bool >>型のパラメータを受け入れます。 つまり Linqはデリゲートでは機能せず、SQLクエリを構築する式ツリーで機能します。



式は、 構文ツリーノードを記述します 。 それらがどのように機能するかをより良く理解するために、名前が文字列と等しいことをチェックする式を考えてください



 Expression<Func<Product, bool>> expected = product => product.Name == "target";
      
      





デバッグ時には、この式の構造を確認できます(キープロパティは赤でマークされています)







この木のようなものになります







実際、デリゲートをパラメーターとして渡すと、エンティティプロパティにアクセスする代わりに、パラメーター(デリゲート)でInvokeメソッドが呼び出される別のツリーが形成されます。 LinqがこのツリーでSQLクエリを構築しようとすると、Invokeメソッドの解釈方法がわからず、NotSupportedExceptionがスローされます。



したがって、私たちの仕事は、エンティティのプロパティ(赤で強調表示されているツリーの部分)の呼び出しを、パラメータを介して渡された式に置き換えることです。 試してみましょう:



 Expression<Func<Product, string>> propertyGetter = product => product.Name; Expression<Func<Product, bool>> filter = product => propertyGetter(product) == "target"
      
      





これで、コンパイル段階で既に「メソッド名が必要です」というエラーが表示されます。







問題は、式がデリゲート自体ではなく、 構文ツリーのノードを表すクラスであり、直接呼び出すことができないことです。 ここでの主なタスクは、式を作成する方法を見つけ、別のパラメーターをパラメーターとして渡すことです。



訪問者



短いグーグル検索の後、 StackOverflowで同様の問題の解決策を見つけました。



を操作するために、 Visitorパターンを使用する特別なExpressionVisitorクラスがあります。 その本質は、構文ツリーを解析する順序で式ツリーのすべてのノードをトラバースし、それらを変更するか、代わりに別のノードを返すことができるということです。 ノード自体もその子ノードも変更されていない場合、元の式が返されます。



つまり ExpressionVisitorクラスを継承して、任意のツリーノードをパラメーターを介して渡す式に置き換えることができます。 したがって、何らかのラベルノードをツリーに配置する必要があります。これは、トラバース中にパラメーターに置き換えられます。 これを行うために、式呼び出しをシミュレートし、ラベルになる拡張メソッドを作成します。



 public static class ExpressionExtension { public static TFunc Call<TFunc>(this Expression<TFunc> expression) { throw new InvalidOperationException("This method should never be called. It is a marker for replacing."); } }
      
      





ある式を別の式に挿入できます



 Expression<Func<Product, string>> propertyGetter = product => product.Name; Expression<Func<Product, bool>> filter = product => propertyGetter.Call()(product) == "target";
      
      





式ツリーでCallメソッドをそのパラメーターで置き換えるビジターを記述することは残ります。



 public class SubstituteExpressionCallVisitor : ExpressionVisitor { private readonly MethodInfo _markerDesctiprion; public SubstituteExpressionCallVisitor() { _markerDesctiprion = typeof(ExpressionExtension).GetMethod(nameof(ExpressionExtension.Call)).GetGenericMethodDefinition(); } protected override Expression VisitMethodCall(MethodCallExpression node) { if (IsMarker(node)) { return Visit(ExtractExpression(node)); } return base.VisitMethodCall(node); } private LambdaExpression ExtractExpression(MethodCallExpression node) { var target = node.Arguments[0]; return (LambdaExpression)Expression.Lambda(target).Compile().DynamicInvoke(); } private bool IsMarker(MethodCallExpression node) { return node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == _markerDesctiprion; } }
      
      





これでマーカーを交換できます。



 public static Expression<TFunc> SubstituteMarker<TFunc>(this Expression<TFunc> expression) { var visitor = new SubstituteExpressionCallVisitor(); return (Expression<TFunc>)visitor.Visit(expression); } Expression<Func<Product, string>> propertyGetter = product => product.Name; Expression<Func<Product, bool>> filter = product => propertyGetter.Call()(product).Contains("123"); Expression<Func<Product, bool>> finalFilter = filter.SubstituteMarker();
      
      





デバッグでは、式が期待したものとはまったく異なることがわかります。 フィルターにはまだInvokeメソッドが含まれています。







ポイントは、parameterGetter式とfinalFilter式が2つの異なる引数を使用することです。 したがって、parameterGetterの引数をfinalFilterの引数に置き換える必要があります。 これを行うには、別の訪問者を作成します。







その結果、次のコードを取得します。



 public class SubstituteParameterVisitor : ExpressionVisitor { private readonly LambdaExpression _expressionToVisit; private readonly Dictionary<ParameterExpression, Expression> _substitutionByParameter; public SubstituteParameterVisitor(Expression[] parameterSubstitutions, LambdaExpression expressionToVisit) { _expressionToVisit = expressionToVisit; _substitutionByParameter = expressionToVisit .Parameters .Select((parameter, index) => new {Parameter = parameter, Index = index}) .ToDictionary(pair => pair.Parameter, pair => parameterSubstitutions[pair.Index]); } public Expression Replace() { return Visit(_expressionToVisit.Body); } protected override Expression VisitParameter(ParameterExpression node) { Expression substitution; if (_substitutionByParameter.TryGetValue(node, out substitution)) { return Visit(substitution); } return base.VisitParameter(node); } } public class SubstituteExpressionCallVisitor : ExpressionVisitor { private readonly MethodInfo _markerDesctiprion; public SubstituteExpressionCallVisitor() { _markerDesctiprion = typeof(ExpressionExtensions) .GetMethod(nameof(ExpressionExtensions.Call)) .GetGenericMethodDefinition(); } protected override Expression VisitInvocation(InvocationExpression node) { var isMarkerCall = node.Expression.NodeType == ExpressionType.Call && IsMarker((MethodCallExpression) node.Expression); if (isMarkerCall) { var parameterReplacer = new SubstituteParameterVisitor(node.Arguments.ToArray(), Unwrap((MethodCallExpression) node.Expression)); var target = parameterReplacer.Replace(); return Visit(target); } return base.VisitInvocation(node); } private LambdaExpression Unwrap(MethodCallExpression node) { var target = node.Arguments[0]; return (LambdaExpression)Expression.Lambda(target).Compile().DynamicInvoke(); } private bool IsMarker(MethodCallExpression node) { return node.Method.IsGenericMethod && node.Method.GetGenericMethodDefinition() == _markerDesctiprion; } }
      
      





これですべてが正常に機能し、最終的にフィルタリングメソッドを記述できます







 public IQueryable<TEntity> FilterContainsText<TEntity>(IQueryable<TEntity> entities, Expression<Func<TEntity, string>> getProperty, string text) { Expression<Func<TEntity, bool>> filter = entity => getProperty.Call()(entity).Contains(text); return entities.Where(filter.SubstituteMarker()); }
      
      





あとがき



式置換アプローチは、フィルタリングだけでなく、並べ替えや一般的なデータベースクエリにも使用できます。



このメソッドを使用すると、データベースクエリ自体とは別に式をビジネスロジックと一緒に格納することもできます。



githubでコードを完全に見ることができます。



All Articles