Skip to content

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

@dster2

Description

@dster2

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions