Reputation: 8937
In an app the uses EF Core, I am attempting to eliminate duplication of query code by creating a reusable library of predicate expressions. However, I am struggling with predicates that accept runtime parameters.
Let's assume 2 simple entity classes in a parent child relationship:
public class Parent
{
public double Salary { get; set; }
public ICollection<Child> Children { get; set; }
}
public class Child
{
public double Salary { get; set; }
}
I can retrieve all the parents with a child that earns more than them, using a conventional EF Core query like this:
return Context.Set<Parent>()
.Where(parent => parent.Children.Any(child =>
child.Salary > parent.Salary));
If I want to create a reusable predicate for the above query, I imagine it might look something like this:
private Expression<Func<Child, Parent, bool>> EarnsMoreThanParent = (Child child, Parent parent) => child.Salary > parent.Salary;
The above predicate compiles OK, but I haven't found any way to consume it. The problem is how to pass the parent entity into it at runtime. This doesn't compile:
return _context.Set<Parent>()
.Where(parent => parent.Children.Any(child =>
EarnsMoreThanParent(child, parent));
I understand that the I am conflating the Expression<>
and Func<>
syntax, but I assume that this is a relatively common requirement that must be possible.
Thanks
Upvotes: 4
Views: 2745
Reputation: 205819
This is interesting topic, mainly because for so many years C# does not provide a syntax for composing expressions (for instance something similar to string interpolation). Many 3rd party packages are trying to address the issue by providing custom extension method, which at the end transform the query expression tree by injecting the actual expressions needed by the query provider.
Since you have tagged you question with [predicatebuilder]
, I'm assuming you are using LINQKit package, which provides custom Invoke
method. It can be used to "invoke" your expression:
return _context.Set<Parent>()
.Where(parent => parent.Children.Any(child =>
EarnsMoreThanParent.Invoke(child, parent))
.AsExpandable();
The downside (besides the limitation that the "invoked" expression cannot be coming from property or method) is that you have to call AsExpandable()
for each query using such Invoke(...)
calls, otherwise the custom method won't take effect and you'll get "method not supported" runtime exception.
Recently I found a very useful package called DelegateDecompiler, which allows you to achieve code reuse in a more natural way. Instead of reusable expressions, you use the regular OOP reusable primitives like computed (get only) properties and instance/static/extension methods, and simply mark them with [Decompile]
attribute. e.g. in some public static class:
[Decompile]
public static bool EarnsMoreThanParent(this Child child, Parent parent) => child.Salary > parent.Salary;
or in the Child
class:
[Decompile]
public bool EarnsMoreThanParent(Parent parent) => this.Salary > parent.Salary;
Then all you need is to call Decompile()
custom extension method at some point of your query:
return _context.Set<Parent>()
.Where(parent => parent.Children.Any(child =>
child.EarnsMoreThanParent(parent))
.Decompile();
This looks much better than using expressions. The drawback is the same as other solution - the need of calling custom method for each query. In my answer to EF Core queries all columns in SQL when mapping to object in Select I've showed a solution for plugging the package into the EF Core 3.1 query processing pipeline (I'm expecting in the future similar public extension point in EF Core which will eliminate a lot of the boilerplate code needed there), which gives the best - using OOP features in expression trees and translating (expanding, replacing with the actual expressions) them transparently. e.g. with DelegateDecompiler
plugged in as explained there, the query is simply as it would have been written for LINQ to Objects:
return _context.Set<Parent>()
.Where(parent => parent.Children.Any(child =>
child.EarnsMoreThanParent(parent));
Upvotes: 1
Reputation: 7478
You can try to wrap the whole expression like this
private Expression<Func<Parent, bool>> EarnsMoreThanParent =
(Parent parent) => parent.Children.Any(child => child.Salary > parent.Salary)
and then use for the whole Where
return _context.Set<Parent>().Where(EarnsMoreThanParent);
The problem with your code is that in .Where(parent => parent.Children.Any(child => EarnsMoreThanParent(child, parent))
the whole expression inside is an expression. So your method EarnsMoreThanParent
won't be called as it's part of Where
expression and EF will try to parse it as expression and would fail.
But yeah, if you need to combine a couple of such condition with different OR
it might not work as it would be like Where(EanrsMoreThanParent).Where(SomeOtherCondition)
and all of them will be translated to AND
Upvotes: 5