Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error expanding a lambda which calls Invoke() on an inner expression which references outer parameter #191

Open
dster2 opened this issue Oct 20, 2023 · 1 comment

Comments

@dster2
Copy link

dster2 commented Oct 20, 2023

(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.)

@PhenX
Copy link

PhenX commented Jun 6, 2024

I have the exact same problem, with EF core 8, where it generally works without AsExpandable, but in this case it does not change anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants