Linq to Entities扩展方法内部查询 (EF6)

7
有人可以解释一下为什么在以下情况下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。为什么它不能这样做呢?


查看 http://stackoverflow.com/a/36736907/1625737 和 http://stackoverflow.com/a/29960775/1625737,不确定它是否适用于扩展方法。 - haim770
谢谢你的建议。我已经开始朝着那个方向看了,但是要消化关于表达式和函数的所有内容需要一段时间。同时,是否有一种方法可以像Expression<Func<T>>(这里T可能是协议)一样重新编写我的扩展方法,以便它可以与我的预期用法一起使用? - MihaiP.
我不确定 LinqKit 能否在这里发挥作用。我认为 @StriplingWarrior 的答案是最明智的方法。 - haim770
d.Protocols 似乎是一个导航属性,因此不能是 IQueryable<Protocol>(很可能是 ICollection<Protocol>),对吗? - Ivan Stoev
EF6是否支持编译查询(或等效)?LINQ to SQL和早期版本的EF可以让您创建这样的方法。否则,您可能可以使用linqkit。 - Jeff Mercado
2个回答

9
这是因为在您传递给Select的lambda表达式中,调用ForUser()的操作被包含在C#编译器构建的表达式树内。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<> 上调用扩展方法的 IQueryable<>。例如,这是 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) }
                ));
    }

当可查询的提供程序必须从 IQueryable<> 生成实际数据时,它会分析表达式树并尝试找出如何解释这些方法调用。Entity Framework 内置了对 许多与 LINQ 相关的函数(如 .Where().Select())的知识,因此它知道如何将这些方法调用转换为 SQL。但是,它不知道如何处理您编写的方法。
那么为什么这样做可以呢?
var data = context.Programs.ForUser(userId);

答案是,你的ForUser方法没有像上面的Select方法一样被实现:你没有添加一个表达式到可查询对象中来表示调用ForUser。相反,你返回了.Where()调用的结果。从IQueryable<>的角度来看,就好像Where()直接被调用了,而对ForUser()的调用从未发生过。
你可以通过捕获IQueryable<>的Expression属性来证明这一点:
Console.WriteLine(data.Expression.ToString());

...这将产生类似于以下内容:

Programs.Where(u => (u.UserId == value(Helpers<>c__DisplayClass1_0).userId))

在该表达式中没有调用ForUser()

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

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

如果从未实际调用.ForUser()方法,则其永远不会返回一个知道已调用.Where()方法的IQueryable<>。相反,可查询的表达式树显示调用了.ForUser()。输出其表达式树将如下所示:

Programs.Select(d => d.Protocols.ForUser(value(Repository<>c__DisplayClass1_0).userId))

Entity Framework不知道ForUser()应该执行什么操作。就其而言,您可能编写了一个在SQL中无法执行的ForUser()方法。因此,它告诉您这不是受支持的方法。

谢谢你的回答。请让我稍微重新表述一下我的问题。 我不明白的是,为什么引擎会区分Linq的_Where_扩展方法和我的_ForUser_扩展方法? 我期望它能递归解析扩展方法链,构建完整的表达式树,然后生成SQL。 你说d.Protocols还不存在。但是它不能像“我程序d的所有协议”这样被推断出来吗?这将被翻译成子查询。 - MihaiP.
看一下(谷歌)树表达式生成器。就像StriplingWarrior提到的那样,生成器表示它将在执行时评估它。如果我没记错的话,EF 7的树生成器有点不同,并在执行之前对其进行评估。 - Roelant M
如果将扩展方法转换为静态方法调用,则会更加清晰:var data = MyExtensions.ForUser(context.Programs, userId); 之所以有效,是因为该语句以 ForUser 调用开头,并且它只返回一个 IQueryable。在其他情况下,MyExtensions.ForUser(...) 是表达式中的“外来”CLR方法调用。 - Gert Arnold
1
@MihaiP。最终很明显你所要求的是不可能实现的。EF团队的决定与效率和子查询无关。仔细阅读StriplingWarrior的回答并进行逻辑思考。关键点是Queryable扩展方法是不会被执行的(DbFunctionsSqlFunctions同样)。它们只是已知签名(接口)的标识 - 名称和参数。EF实际上是在SQL中实现它们的。但是,显然没有人知道你的方法,也无法反编译它们做了什么,因此无法翻译。 - Ivan Stoev
@MihaiP.:假设你在你的ForUser扩展方法中放置了一个if语句,检查protocols查询是否有任何项,并在没有任何项时返回不同的值(或抛出异常)。此时,为了使ForUser函数正常工作,protocols查询需要由能够运行的真实查询支持。这在SQL语句中是不可能发生的,但对于任何给定的Program,直到在数据库上运行查询之前,你都不知道它的Protocols是什么。 - StriplingWarrior
显示剩余2条评论

0
正如我在上面的评论中提到的,我无法确定EF引擎的工作方式。因此,我尝试找到一种重新编写查询的方法,以便能够利用我的扩展方法。
表格如下:
Program -> 1..m -> ProgramProtocol -> m..1 -> Protocol

ProgramProtocol只是一个连接表,不会被Entity Framework映射到模型中。 这个想法很简单:选择“从左边”,选择“从右边”,然后将结果集合并以进行适当的过滤:

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

1
“我们”是谁?我已经详细解释了EF引擎为什么以这种方式工作,为什么它不可能按照你的要求工作。如果你还没有理解,那是因为你没有花时间思考我的解释。你的解决方案应该可以工作,但它似乎比必要的复杂得多,而且唯一节省的就是调用.Where() - StriplingWarrior
@StriplingWarrior - 我编辑了我的答案,以消除与其他人的任何意外关联。但也许是皇家的“我们”... :) - MihaiP.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接