私が遭遇した問題の説明から始めたいと思います。 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」はサポートされていません LINQtoEntitiesで。
表現
何が悪かったのか確認しましょう。
Whereメソッドは、Expression
式は構文木を記述します。それらがどのように構造化されているかをよりよく理解するために、名前が行と等しいことをチェックする式を検討してください。
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"
これで、コンパイル段階で«メソッド名が必要»エラーが表示されます。
問題は、式がデリゲートではなく構文ツリーのノードを表すクラスであり、直接呼び出すことができないことです。ここでの主なタスクは、別のパラメーターを渡す式を作成する方法を見つけることです。
訪問者
Googleで簡単に検索したところ、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で確認できます。
この記事はStackOverflowの返信に基づいています。