-
Notifications
You must be signed in to change notification settings - Fork 170
Description
(Apologies if this has been covered elsewhere, I wasn't able to find discussion of this case)
Context
We have an extension function to enable cleaner queries to support conditional logic in function chains, with a corresponding [Expandable]
variant:
[Expandable(nameof(ConditionalWhereExpression))]
public static IQueryable<T> ConditionalWhere<T>(
this IQueryable<T> query, bool condition, Expression<Func<T, bool>> truePredicate) =>
condition ? query.Where(truePredicate) : query;
private static Expression<Func<IQueryable<T>, bool, Expression<Func<T, bool>>, IQueryable<T>>> ConditionalWhereExpression<T>() =>
(query, condition, truePredicate) => query.Where(t => !condition || truePredicate.Invoke(t));
The former works fine in regular EFCore usage, and the latter also works for their intended use case, EF Compiled Queries, in normal use cases where truePredicate
does not reference outside values, for example:
Expression<Func<DbContext, bool, User>> buildQueryExpression =
(DbContext dbContext, bool flag) =>
dbContext.Set<User>()
.ConditionalWhere(flag, u => u.Id <= 10)
.FirstOrDefault();
var queryAsync = EF.CompileAsyncQuery(buildQueryExpression.Expand());
var result = await queryAsync(_dbContext, true); // Success
Problem
However, if the truePredicate
does reference outside values then it fails, not just for EF Compiled Queries, but also just when Compile()
ing the Expand()
ed lambda:
Expression<Func<DbContext, bool, int, User>> buildQueryExpression =
(DbContext dbContext, bool flag, int maxId) =>
dbContext.Set<User>()
.ConditionalWhere(flag, u => u.Id <= maxId)
.FirstOrDefault();
var result1 = buildQueryExpression.Compile()(_dbContext, true, 10); // Success
var result2 = buildQueryExpression.Expand().Compile()(_dbContext, true, 10); // Failure
var queryAsync = EF.CompileAsyncQuery(buildQueryExpression.Expand());
var result = await queryAsync(_dbContext, true, 10); // Also failure, same error
We get this error:
System.InvalidOperationException: variable 'maxId' of type 'System.Int32' referenced from scope '', but it is not defined
Note that Expand()
is required so EF.CompileAsyncQuery()
can translate the query logic, since it doesn't understand ConditionalWhere
. Also if I alter ConditionalWhereExpression
to just return query.Where(truePredicate)
, it works (but obviously breaks the intended behavior), so the closure'd inner expression can work, it just seems to lose its closure when we wrap it in t => expr.Invoke(t)
then Expand()
the outer expression.
Is there a way to make this work, either changing the implementation of ConditionalWhereExpression
or the way I'm building the query lambda?
(Note that the implementation options of ConditionalWhereExpression
are limited because EF Compiled Queries are restricted in what kinds of expression trees they support, in particular the logic we use in ConditionalWhere
will not work, and you must pass a LambdaExpression
to Where()
, and not any other kind of Expression
even if they would return an appropriate delegate upon evaluation.)