Reputation: 10563
I'm trying to make a generic filtering class, that will receive a property accessor and check if it's within allowed values. So the signature of the filter would be:
class PropertyFilter<TItem, TProperty>
{
PropertyFilter(Expression<Func<TItem, TProperty>> accessor, IEnumerable<TProperty> allowedValues);
IQueryable<TItem> Filter(IQueryable<TItem> items);
}
and usage
var filter = new PropertyFilter<Entity, string>(e => e.Name, new [] { "NameA", "NameB" });
await filter.Filter(dbContext.Entities).ToListAsync();
The thing has to be IQueryable
compatible, so I need to compose an expression. From an expression of form x => x.Property
I need to create an expression Expression<Func<TItem, bool>>
equivalent to x => allowedValues.Contains(x.Property)
. From what I've seen I am able to do basicaly anything with the expression using Visitors and whatnot, but for one I don't know what are the rules of translating expression to SQL and what I cannot do or I break it, and also the use-case seems too simple to warrant that much code as would go into implementing my own visitors and testing all of that. Is there a short way to do this or maybe a reliable library that has already figured it out and is compatible with .NET Core 3.0 and EF Core 3.0 previews?
Upvotes: 2
Views: 168
Reputation: 1062620
Untested, but this should work?
static Expression<Func<TItem, bool>> Contains<TItem, TProperty>(
Expression<Func<TItem, TProperty>> accessor,
IEnumerable<TProperty> allowedValues)
{
var wrapped = new { allowedValues };
var body = Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains),
new[] { typeof(TProperty) },
Expression.PropertyOrField(Expression.Constant(wrapped), nameof(allowedValues)),
accessor.Body);
return Expression.Lambda<Func<TItem, bool>>(body, accessor.Parameters);
}
You should be able to pass the result of this to Queryable.Where
.
Note that wrapped
here is to add a layer of indirection that the receiving LINQ query parser might want.
Upvotes: 1
Reputation: 205579
First off, EF Core 3.0 previews are unreliable as any preview (beta) software. Moreover currently they are in a process of rewriting the LINQ query expression tree translation, so it's quite unstable and many things don't work, even though they work in the last stable EF Core 2.x.
So trying to workaround them at this moment doesn't make sense. It's better to stay on last stable 2.x and wait for official 3.0 release.
Anyway, what you are asking is called expression composition, and there are many post how you can do that, including some by me. The important thing is to use parameter replacing technique instead of Expression.Invoke
because the later was working in pre 3.0 versions, but the former is known to work with all query providers.
So you need a small helper expression utility like this
public static partial class ExpressionUtils
{
public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
=> new ParameterReplacer { source = source, target = target }.Visit(expression);
class ParameterReplacer : ExpressionVisitor
{
public ParameterExpression source;
public Expression target;
protected override Expression VisitParameter(ParameterExpression node)
=> node == source ? target : node;
}
}
and then a helper expression composing method like this:
public static partial class ExpressionUtils
{
public static Expression<Func<TOuter, TResult>> Select<TOuter, TInner, TResult>(
this Expression<Func<TOuter, TInner>> innerSelector,
Expression<Func<TInner, TResult>> resultSelector)
=> Expression.Lambda<Func<TOuter, TResult>>(
resultSelector.Body.ReplaceParameter(resultSelector.Parameters[0], innerSelector.Body),
innerSelector.Parameters);
}
With these helpers, the implementation of the expression in question (which luckily works in the 3.0 preview) would be simply:
return accessor.Select(value => allowedValues.Contains(value));
The benefit of composing the expression this way is that the result is exatly the same as if you were doing that at compile time, hence has a bigger chance to be supported.
Upvotes: 2