【问题标题】:Linq to entities extension method inner query (EF6)Linq 到实体扩展方法内部查询 (EF6)
【发布时间】:2017-01-30 03:08:47
【问题描述】:

有人可以向我解释为什么 EF 引擎在以下情况下会失败吗?

它适用于以下表达式:

var data = context.Programs
    .Select(d => new MyDataDto
    {
        ProgramId = d.ProgramId,
        ProgramName = d.ProgramName,
        ClientId = d.ClientId,
        Protocols = d.Protocols.Where(p => p.UserProtocols.Any(u => u.UserId == userId))
                .Count(pr => pr.Programs.Any(pg => pg.ProgramId == d.ProgramId))
    })
    .ToList();

但是如果我将一些封装成扩展方法:

public static IQueryable<Protocol> ForUser(this IQueryable<Protocol> protocols, int userId)
{
    return protocols.Where(p => p.UserProtocols.Any(u => u.UserId == userId));
}

结果查询:

var data = context.Programs
    .Select(d => new MyDataDto
    {
        ProgramId = d.ProgramId,
        ProgramName = d.ProgramName,
        ClientId = d.ClientId,
        Protocols = d.Protocols.ForUser(userId)
                .Count(pr => pr.Programs.Any(pg => pg.ProgramId == d.ProgramId))
    })
    .ToList();

失败并出现异常:LINQ to Entities 无法识别方法 'System.Linq.IQueryable1[DAL.Protocol] ForUser(System.Linq.IQueryable1[DAL.Protocol], Int32)' 方法,并且此方法不能翻译成商店表达式。

我希望 EF 引擎构建整个表达式树,链接必要的表达式,然后生成 SQL。为什么不这样做?

【问题讨论】:

  • stackoverflow.com/a/36736907/1625737stackoverflow.com/a/29960775/1625737,但不确定它是否适用于扩展方法
  • 感谢您的提示。我已经开始寻找这条路,但我需要一段时间来消化所有关于表达式和函数的内容。同时,有没有办法像 Expression> (这里的 T 可能是协议)重写我的扩展方法,以便它可以满足我的预期用途?
  • 我不确定 LinqKit 能否在这里提供帮助。我认为@StriplingWarrior 的回答是最明智的做法。
  • d.Protocols 似乎是一个导航属性,因此不可能是IQueryable&lt;Protocol&gt;(很可能是ICollection&lt;Protocol&gt;),对吧?
  • EF6 是否支持编译查询(或等效查询)? LINQ to SQL 和旧版本的 EF 将允许您创建这样的方法。否则,您也许可以使用linqkit

标签: c# entity-framework linq entity-framework-6 extension-methods


【解决方案1】:

发生这种情况是因为对 ForUser() 的调用是在 C# 编译器看到您传递给 Select 的 lambda 时构建的表达式树内部进行的。 Entity Framework 试图弄清楚如何将该函数转换为 SQL,但由于某些原因(例如 d.Protocols 目前不存在),它无法调用该函数。

适用于此类情况的最简单方法是让您的助手返回条件 lambda 表达式,然后自己将其传递给 .Where() 方法:

public static Expression<Func<Protocol, true>> ProtocolIsForUser(int userId)
{
    return p => p.UserProtocols.Any(u => u.UserId == userId);
}

...

var protocolCriteria = Helpers.ProtocolIsForUser(userId);
var data = context.Programs
    .Select(d => new MyDataDto
    {
        ProgramId = d.ProgramId,
        ProgramName = d.ProgramName,
        ClientId = d.ClientId,
        Protocols = d.Protocols.Count(protocolCriteria)
    })
    .ToList();

更多信息

当您在表达式树之外调用 LINQ 方法时(就像您对 context.Programs.Select(...) 所做的那样),实际上会调用 Queryable.Select() 扩展方法,并且它的实现会返回一个 IQueryable&lt;&gt;,它表示正在调用的扩展方法原来的IQueryable&lt;&gt;。下面是 Select 的实现,例如:

    public static IQueryable<TResult> Select<TSource,TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector) {
        if (source == null)
            throw Error.ArgumentNull("source");
        if (selector == null)
            throw Error.ArgumentNull("selector");
        return source.Provider.CreateQuery<TResult>( 
            Expression.Call(
                null,
                GetMethodInfo(Queryable.Select, source, selector),
                new Expression[] { source.Expression, Expression.Quote(selector) }
                ));
    }

当可查询的 Provider 必须从 IQueryable&lt;&gt; 生成实际数据时,它会分析表达式树并尝试找出如何解释这些方法调用。实体框架内置了many LINQ-related functions 的知识,如.Where().Select(),因此它知道如何将这些方法调用转换为SQL。但是,它不知道如何处理您编写的方法。

那么为什么会这样呢?

var data = context.Programs.ForUser(userId);

答案是您的ForUser 方法没有像上面的Select 方法那样实现:您没有向可查询对象添加表达式来表示调用ForUser。相反,您将返回 .Where() 调用的结果。从IQueryable&lt;&gt; 的角度来看,就好像直接调用了Where(),而对ForUser() 的调用从未发生过。

您可以通过捕获IQueryable&lt;&gt; 上的Expression 属性来证明这一点:

Console.WriteLine(data.Expression.ToString());

... 这将产生如下内容:

Programs.Where(u =&gt; (u.UserId == value(Helpers&lt;&gt;c__DisplayClass1_0).userId))

在该表达式的任何地方都没有调用ForUser()

另一方面,如果您将 ForUser() 调用包含在这样的表达式树中:

var data = context.Programs.Select(d => d.Protocols.ForUser(id));

... 那么.ForUser() 方法实际上永远不会被调用,因此它永远不会返回知道.Where() 方法被调用的IQueryable&lt;&gt;。相反,可查询的表达式树显示.ForUser() 被调用。输出它的表达式树看起来像这样:

Programs.Select(d =&gt; d.Protocols.ForUser(value(Repository&lt;&gt;c__DisplayClass1_0).userId))

实体框架不知道ForUser() 应该做什么。就它而言,您可以编写ForUser() 来做一些在SQL 中不可能做的事情。所以它告诉你这不是一个受支持的方法。

【讨论】:

  • 感谢您的回答。请让我试着改写一下我的问题。我不明白的是为什么引擎区分 Linq 的 Where 扩展方法和我的 ForUser 扩展方法?我希望它递归地解析扩展方法链接,构建完整的表达式树,然后生成 SQL。你是说 d.Protocols 还不存在。但它不能像“我的程序 d 的所有协议”那样在子查询中翻译吗?
  • 看看(谷歌)树表达式构建器。正如 StriplingWarrior 提到的,建造者说它会在执行时评估它。如果我没记错的话,EF 7 的树构建器有点不同,并在执行之前对其进行评估。
  • @MihaiP.:我添加了更详细的解释。让我知道这是否有意义。
  • 如果您将扩展方法转换为静态方法调用,就会更加清楚:var data = MyExtensions.ForUser(context.Programs, userId); 有效,因为该语句以ForUser 调用开头,它只返回一个IQueryable。在其他情况下,MyExtensions.ForUser(...) 是表达式内的“外星人”CLR 方法调用。
  • @MihaiP。最后很清楚,您要求的内容是不可能实现的。 EF 团队决策与效率和子查询无关。仔细阅读 StriplingWarrior 的答案并逻辑思考。关键是Queryable 扩展方法未执行DbFunctionsSqlFunctions 相同)。它们只是已知的签名,例如合同(接口)-名称和参数。 EF 实际上是在 SQL 中实现它们。但是当然没有人知道你的方法,也没有人可以反编译它们的作用,所以显然它们不能被翻译。
【解决方案2】:

正如我在上面的评论中提到的,我不知道为什么 EF 引擎会这样工作。因此,我试图找到一种方法来重写查询,以便能够使用我的扩展方法。

这些表格是:

Program -> 1..m -> ProgramProtocol -> m..1 -> Protocol

ProgramProtocol 只是一个连接表,并没有被实体框架映射到模型中。 这个想法很简单:选择“从左”,选择“从右”,然后加入结果集以进行适当的过滤:

var data = context.Programs.ForUser(userId)
    .SelectMany(pm => pm.Protocols,
        (pm, pt) => new {pm.ProgramId, pm.ProgramName, pm.ClientId, pt.ProtocolId})
    .Join(context.Protocols.ForUser(userId), pm => pm.ProtocolId,
        pt => pt.ProtocolId, (pm, pt) => pm)
    .GroupBy(pm => new {pm.ProgramId, pm.ProgramName, pm.ClientId})
    .Select(d => new MyDataDto
    {
        ProgramName = d.Key.ProgramName,
        ProgramId = d.Key.ProgramId,
        ClientId = d.Key.ClientId,
        Protocols = d.Count()
    })
    .ToList();

【讨论】:

  • 谁是“我们”?我非常详细地解释了为什么 EF 引擎按它的方式工作——为什么它不可能按你想要的方式工作。如果你还没有理解,那是因为你没有花时间仔细考虑我的解释。你的解决方案应该可以工作,但它似乎比它需要的要复杂得多,它唯一可以节省你的是调用.Where()
  • @StriplingWarrior - 我编辑了我的答案以删除与其他人的任何意外关联。但也许是皇家的“我们”...... :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-12-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-25
  • 2021-11-05
  • 1970-01-01
相关资源
最近更新 更多