使用 `groupjoin` 的查询无法翻译,尽管它被证明是受支持的

Jul*_*che 3 entity-framework-core

我不明白为什么这不翻译。这似乎正是这里描述的用例。

LINQ 表达式

DbSet<A>()
    .GroupJoin(
        inner: DbSet<B>(),
        outerKeySelector: a => a.AId,
        innerKeySelector: b => b.AId,
        resultSelector: (a, bs) => new {
            a = a,
            bs = bs
         })
Run Code Online (Sandbox Code Playgroud)

产生错误:

无法翻译。以可翻译的形式重写查询,或通过插入对“AsEnumerable”、“AsAsyncEnumerable”、“ToList”或“ToListAsync”的调用,显式切换到客户端评估。有关详细信息,请参阅https://go.microsoft.com/fwlink/?linkid=2101038

产生异常的 LINQ 代码是

DbSet<A>()
    .GroupJoin(
        inner: DbSet<B>(),
        outerKeySelector: a => a.AId,
        innerKeySelector: b => b.AId,
        resultSelector: (a, bs) => new {
            a = a,
            bs = bs
         })
Run Code Online (Sandbox Code Playgroud)

编辑:也许我误解了文档,这是一个不翻译的例子。

执行类似于以下示例的查询会生成 Blog & IEnumerable 的结果。由于数据库(尤其是关系数据库)无法表示客户端对象的集合,因此 GroupJoin 在许多情况下不会转换为服务器。它要求您从服务器获取所有数据以在没有特殊选择器的情况下执行 GroupJoin(下面的第一个查询)。但是,如果选择器限制选择的数据,那么从服务器获取所有数据可能会导致性能问题(下面的第二个查询)。这就是 EF Core 不翻译 GroupJoin 的原因。

但后来我的问题变成了:如何在不需要导航属性的情况下实现我正在寻找的结果?

Nuc*_*mer 11

EF Core 不允许您GroupJoin在不跟进 a 的情况下执行 aSelectMany以便使列表扁平化。在 SQL 中没有GroupJoin等效的实现,但是GroupJoin/SelectMany相当于内连接或左连接(取决于您是否使用DefaultIfEmpty),因此它可以正常工作:

context.Users.GroupJoin(
        context.UserRoles,
        u => u.UserId,
        r => r.UserId,
        (user, roles) => new { user, roles })

    //Will not work without this line
    .SelectMany(x => x.roles.DefaultIfEmpty(), (x, r) => new { x.user, role = r })
    .ToList();
Run Code Online (Sandbox Code Playgroud)

如果您确实希望对结果进行分组(而不是尝试进行左连接),您有以下几种选择:

  1. 您可以具体化左连接的结果,然后在内存中对结果进行分组(下面的代码使用LINQ 中的 LEFT OUTER JOINLeftJoin中显示的我的函数):

    context.Users.LeftJoin(
                context.UserRoles,
                u => u.UserId,
                r => r.UserId,
                (user, roles) => new { user, roles })
            .ToList()
            .GroupBy(x => x.user, (u, x) => new
            {
                User = u,
                Roles = x.Select(z => z.role).Where(r => r != null).ToList()
            })
            .ToList();
    
    Run Code Online (Sandbox Code Playgroud)
  2. 您可以使用子查询。请注意,EF 足够智能,可以在生成 SQL 时使用左连接:

    context.Users.Select(u => new
        {
            User = u,
            Roles = context.UserRoles.Where(r => r.UserId == u.UserId).ToList()
        })
        .ToList();
    
    Run Code Online (Sandbox Code Playgroud)

如果您更喜欢这种GroupJoin语法,但不想继续调用所有其他函数来展平、具体化,然后重新分组结果,则可以使用我的JoinMany()扩展方法。此方法使用子查询方法,但将其包装在一个看起来与该GroupJoin函数非常相似的通用方法中:

    context.Users.JoinMany(
            context.UserRoles,
            (u, r) => u.UserId == r.UserId,
            (user, roles) => new { user, roles })
        .ToList();
Run Code Online (Sandbox Code Playgroud)

支持代码:

public static class QueryableExtensions
{
    public static IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
           this IQueryable<TOuter> outer,
           IEnumerable<TInner> inner, Expression<Func<TOuter, TKey>> outerKeySelector,
           Expression<Func<TInner, TKey>> innerKeySelector,
           Expression<Func<TOuter, TInner, TResult>> resultSelector)
    {
        return outer
            .GroupJoin(inner, outerKeySelector, innerKeySelector, (o, i) => new { o, i })
            .SelectMany(o => o.i.DefaultIfEmpty(), (x, i) => new { x.o, i })
            .ApplySelector(x => x.o, x => x.i, resultSelector);
    }

    public static IQueryable<TResult> JoinMany<TOuter, TInner, TResult>(
        this IQueryable<TOuter> outers, IQueryable<TInner> inners,
        Expression<Func<TOuter, TInner, bool>> condition,
        Expression<Func<TOuter, IEnumerable<TInner>, TResult>> resultSelector)
    {
        //Use a placeholder "p => true" expression for the sub-query
        Expression<Func<TOuter, JoinResult<TOuter, IEnumerable<TInner>>>> joinSelector = o =>
            new JoinResult<TOuter, IEnumerable<TInner>> { Outer = o, Inner = inners.Where(p => true) };

        //Create the where-clause that will be used for the sub-query
        var whereClause = Expression.Lambda<Func<TInner, bool>>(
                        condition.Body.ReplaceParameter(condition.Parameters[0], joinSelector.Parameters[0]),
                        condition.Parameters[1]);

        //Replace the placeholder expression with our new where clause
        joinSelector = Expression.Lambda<Func<TOuter, JoinResult<TOuter, IEnumerable<TInner>>>>(
            joinSelector.Body.VisitExpression(node =>
                (node is LambdaExpression le && le.Parameters.Count == 1 && le.Parameters[0].Type == typeof(TInner)
                    && le.Body is ConstantExpression ce && ce.Value is bool b && b)
                    ? whereClause : null),
            joinSelector.Parameters[0]);

        return outers.Select(joinSelector).ApplySelector(x => x.Outer, x => x.Inner, resultSelector);
    }

    private static IQueryable<TResult> ApplySelector<TSource, TOuter, TInner, TResult>(
        this IQueryable<TSource> source,
        Expression<Func<TSource, TOuter>> outerProperty,
        Expression<Func<TSource, TInner>> innerProperty,
        Expression<Func<TOuter, TInner, TResult>> resultSelector)
    {
        var p = Expression.Parameter(typeof(TSource), $"param_{Guid.NewGuid()}".Replace("-", string.Empty));
        Expression body = resultSelector?.Body
            .ReplaceParameter(resultSelector.Parameters[0], outerProperty.Body.ReplaceParameter(outerProperty.Parameters[0], p))
            .ReplaceParameter(resultSelector.Parameters[1], innerProperty.Body.ReplaceParameter(innerProperty.Parameters[0], p));
        var selector = Expression.Lambda<Func<TSource, TResult>>(body, p);
        return source.Select(selector);
    }

    public class JoinResult<TOuter, TInner>
    {
        public TOuter Outer { get; set; }
        public TInner Inner { get; set; }
    }
}


public static class ExpressionExtensions
{
    public static Expression ReplaceParameter(this Expression source, ParameterExpression toReplace, Expression newExpression)
        => new ReplaceParameterExpressionVisitor(toReplace, newExpression).Visit(source);

    public static Expression VisitExpression(this Expression source, Func<Expression, Expression> onVisit)
        => new DelegateExpressionVisitor (onVisit).Visit(source);
}

public class DelegateExpressionVisitor : ExpressionVisitor
{
    Func<Expression, Expression> OnVisit { get; }

    public DelegateExpressionVisitor(Func<Expression, Expression> onVisit)
    {
        this.OnVisit = onVisit;
    }

    public override Expression Visit(Expression node)
    {
        return OnVisit(node) ?? base.Visit(node);
    }
}

public class ReplaceParameterExpressionVisitor : ExpressionVisitor
{
    public ParameterExpression ToReplace { get; }
    public Expression ReplacementExpression { get; }

    public ReplaceParameterExpressionVisitor(ParameterExpression toReplace, Expression replacement)
    {
        this.ToReplace = toReplace;
        this.ReplacementExpression = replacement;
    }

    protected override Expression VisitParameter(ParameterExpression node)
        => (node == ToReplace) ? ReplacementExpression : base.VisitParameter(node);
}
Run Code Online (Sandbox Code Playgroud)


Iva*_*oev 5

链接文档中的解释只是遵循 EF Core 团队的愿景并且很荒谬,因为它当然可以轻松翻译 - 我在这里团队进行了长时间的讨论Query with GroupBy 或 GroupJoin throws exception #17068并在此处继续Query: Support GroupJoin 当它是最终查询运算符 #19930 时,试图说服他们为什么应该支持它,无论参数如何都没有运气。

重点是(这就是当前的解决方法)它可以像相关子查询 ( SelectMany)一样被处理,它被正确地翻译和处理(即使查询结果形状没有 SQL 等效项。

无论如何,当前状态是“需要设计”(无论这意味着什么),解决方法是将连接替换为相关子查询(这是 EF Core 在查询转换期间“扩展”集合导航属性时在内部使用的)。

在你的情况下,更换

join b in ctx.Bs on a.aId equals b.aId into bs
Run Code Online (Sandbox Code Playgroud)

let bs = ctx.Bs.Where(b => a.aId == b.aId)
Run Code Online (Sandbox Code Playgroud)

但是,我强烈建议添加和使用导航属性。不知道为什么你“不能使用”它们,在不投影实体的 LINQ to Entities 中,它们只为关系提供元数据,从而自动生成必要的连接。通过不定义它们,您只会给自己带来不必要的限制(除了 EF Core 限制/错误之外)。一般来说,EF Core 在使用导航属性而不是手动连接时效果更好并支持更多功能。

  • 嗨,伊万,继续努力。GroupJoin 的翻译在 EF6 中完美运行,但当有人切换到 EF core 时,就必须使用解决方法,这似乎很奇怪。 (6认同)