Nested F# lambdas do not work in Linq Expressions

Viewed 85

When using LINQ with F#, queries that have nested lambda expressions don't seem to work

/// helper from http://www.fssnip.net/g0/title/QuickLinq-Helpers
/// this works fine for a SelectMany without a nested lambda
type IQueryable<'T> with 
    ///  A version of Queryable.Select which does not require a type annotation on the argument when used from F#
    member x.SelectQ(selector : Expression<Func<'T,'TResult>>) = System.Linq.Queryable.Select(x,selector)

context.Members
    .SelectManyQ(fun m -> context.Roles.Where(fun r -> m.OrganizationId = r.OrganizationId).DefaultIfEmpty())
    .ToListAsync()

Or with query {} syntax (note that this doesn't need the SelectManyQ):

let q = query {
        for m in context.Members do
            for r in context.Roles.Where(fun r -> m.OrganizationId = r.OrganizationId).DefaultIfEmpty() do
                select (m, r.Id, r.Permission)
    }
q.ToListAsync()

Stack Trace

Unable to cast object of type 'System.Linq.Expressions.MethodCallExpression1' to type 'System.Linq.Expressions.LambdaExpression'.

  Stack Trace: 
ExpressionExtensions.UnwrapLambdaFromQuote(Expression expression)
NavigationExpandingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
MethodCallExpression.Accept(ExpressionVisitor visitor)
ExpressionVisitor.Visit(Expression node)
NavigationExpandingExpressionVisitor.ExpandNavigationsForSource(NavigationExpansionExpression source, Expression expression)
NavigationExpandingExpressionVisitor.ProcessSelectMany(NavigationExpansionExpression source, LambdaExpression collectionSelector, LambdaExpression resultSelector)
NavigationExpandingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
MethodCallExpression.Accept(ExpressionVisitor visitor)
ExpressionVisitor.Visit(Expression node)
NavigationExpandingExpressionVisitor.Expand(Expression query)
<5 more frames...>
CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)
IAsyncEnumerable<T>.GetAsyncEnumerator(CancellationToken cancellationToken)
ConfiguredCancelableAsyncEnumerable`1.GetAsyncEnumerator()
EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)

F# query that does work:

context.Members
    .SelectManyQ(fun m -> context.Roles.DefaultIfEmpty())
    .ToListAsync()

The equivalent C# works as expected

public static IQueryable<MemberWithRoleQueryResult> MemberQuery(UsersContext context, IQueryable<MemberRecord> members)
    => from m in members
       from maybeRole in context.Roles.Where(r => m.OrganizationId == r.OrganizationId).DefaultIfEmpty()
       select new MemberWithRoleQueryResult(
           m,
           maybeRole == null ? null : (Guid?)maybeRole.Id,
           maybeRole == null ? null : maybeRole.Permission,
       );

I've tried using LinqKit but that doesn't seem to detect this case, and using its workarounds like Compile (possibly due to this issue) didn't help. I also tried various explicit expressions but these resulted in the same error (or a different one involving using Func<,> in IQueryable)

type Expr = 
    static member Quote(e:Expression<System.Func<'T, IEnumerable<'TResult>>>) = e
    static member Quote'(e:Expression<System.Func<'T, 'TResult>>) = e

let exp = fun (pl: MemberRecord) -> Expr.Quote'(fun (r: RoleRecord) -> pl.OrganizationId = r.OrganizationId)
let expr = Expr.Quote(fun (pl: MemberRecord) -> context.Roles.Where(exp(pl).Compile()).DefaultIfEmpty())
context.Members
    .SelectMany(expr)
    .ToListAsync()

I suspect this is a bug with F#, possibly related to or duplicating this issue, though workarounds or fixes here would be appreciated

0 Answers
Related