Linq到实体扩展方法内部查询(EF6)

Mih*_*iP. 7 c# linq extension-methods entity-framework entity-framework-6

有人可以向我解释为什么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();
Run Code Online (Sandbox Code Playgroud)

但是,如果我将一些封装到扩展方法中:

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

结果查询:

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();
Run Code Online (Sandbox Code Playgroud)

失败但异常:LINQ to Entities无法识别方法'System.Linq.IQueryable1 [DAL.Protocol] ForUser(System.Linq.IQueryable1 [DAL.Protocol],Int32)'方法,并且此方法无法转换为商店表达.

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

Str*_*ior 7

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

适用于这种情况的最简单方法是让你的助手返回一个标准lambda表达式,然后.Where()自己将其传递给方法:

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

...

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();
Run Code Online (Sandbox Code Playgroud)

更多信息

当你在表达式树之外调用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) }
                ));
    }
Run Code Online (Sandbox Code Playgroud)

当queryable的Provider必须从中生成实际数据时IQueryable<>,它会分析表达式树并尝试找出如何解释这些方法调用.实体框架已经内置的知识很多LINQ相关的功能.Where().Select(),因此它知道这些方法如何将调用到SQL.但是,它不知道如何处理您编写的方法.

那么为什么这样呢?

var data = context.Programs.ForUser(userId);
Run Code Online (Sandbox Code Playgroud)

答案是您的ForUser方法没有像Select上面的方法那样实现:您没有在查询中添加表达式来表示调用ForUser.相反,您将返回.Where()呼叫的结果.从这个IQueryable<>角度来看,它好像Where()被直接调用了,而这种呼唤ForUser()从未发生过.

您可以通过捕获以下Expression属性来证明这一点IQueryable<>:

Console.WriteLine(data.Expression.ToString());
Run Code Online (Sandbox Code Playgroud)

...会产生这样的东西:

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

ForUser()那个表达中没有任何地方可以打电话.

另一方面,如果您ForUser()在表达式树中包含调用,如下所示:

var data = context.Programs.Select(d => d.Protocols.ForUser(id));
Run Code Online (Sandbox Code Playgroud)

...然后该.ForUser()方法实际上永远不会被调用,因此它永远不会返回IQueryable<>知道被调用的.Where()方法的方法.相反,可查询的表达式树显示.ForUser()了被调用.输出其表达式树看起来像这样:

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

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