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

时间:2016-09-21 18:16:32

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

有人可以向我解释为什么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无法识别方法&#39; System.Linq.IQueryable1 [DAL.Protocol] ForUser(System.Linq.IQueryable1 [DAL.Protocol],Int32)&#39;方法,并且此方法无法转换为商店表达式。

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

2 个答案:

答案 0 :(得分: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);
}

...

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<>的{​​{1}}在原始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<>生成实际数据时,它会分析表达式树并尝试找出如何解释这些方法调用。实体框架具有many LINQ-related functions的内置知识,如.Where().Select(),因此它知道如何将这些方法调用转换为SQL。但是,它并不知道如何处理您编写的方法。

那为什么会这样呢?

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

答案是您的ForUser方法没有像上面的Select方法那样实现:您没有在查询中添加表达式来表示调用ForUser。相反,您将返回.Where()电话的结果。从IQueryable<>的角度来看,它就像直接调用Where()一样,并且对ForUser()的调用从未发生过。

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

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()方法实际上从未被调用过,因此它永远不会返回知道调用IQueryable<>方法的.Where()。相反,可查询的表达式树显示.ForUser()被调用。输出其表达式树看起来像这样:

  

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

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

答案 1 :(得分:0)

正如我在上面的评论中所提到的,我无法说明为什么EF引擎按照它的方式工作。因此,我试图找到一种重新编写查询的方法,以便我能够使用我的扩展方法。

表格是:

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

ProgramProtocol只是一个连接表,并未由Entity Framework在模型中映射。 这个想法很简单:选择&#34;从左边&#34;,选择&#34;从右边&#34;然后加入结果集以进行适当的过滤:

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