简体   繁体   English

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

[英]Linq to entities extension method inner query (EF6)

Can someone explain to me why the EF Engine is failing in the following scenario? 有人可以向我解释为什么EF引擎在以下情况下失败了吗?

It works fine with the following expression: 它可以使用以下表达式正常工作:

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

But if I encapsulate some into an extension method: 但是,如果我将一些封装到扩展方法中:

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

The resulting query: 结果查询:

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

Fails with the exception: LINQ to Entities does not recognize the method 'System.Linq.IQueryable1[DAL.Protocol] ForUser(System.Linq.IQueryable1[DAL.Protocol], Int32)' method, and this method cannot be translated into a store expression. 失败但异常:LINQ to Entities无法识别方法'System.Linq.IQueryable1 [DAL.Protocol] ForUser(System.Linq.IQueryable1 [DAL.Protocol],Int32)'方法,并且此方法无法转换为商店表达。

I would expect the EF Engine to build the entire expression tree, chaining the necessary expressions and then generate the SQL. 我希望EF Engine能够构建整个表达式树,链接必要的表达式然后生成SQL。 Why doesn't it do that? 为什么不这样做?

This is happening because the call to ForUser() is being made inside of the expression tree that the C# compiler builds when it sees the lambda you pass into Select. 发生这种情况是因为对ForUser()的调用是在C#编译器看到传递给Select的lambda时构建的表达式树内部进行的。 Entity Framework tries to figure out how to convert that function into SQL, but it can't invoke the function for a few reasons (eg d.Protocols does not exist at the moment). 实体框架试图弄清楚如何将该函数转换为SQL,但由于d.Protocols原因它无法调用该函数(例如d.Protocols目前不存在)。

The simplest approach that works for a case like this is to have your helper return a criteria lambda expression, and then pass that into the .Where() method yourself: 适用于这种情况的最简单方法是让你的助手返回一个标准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();

More information 更多信息

When you invoke a LINQ method outside of an expression tree (like you do with context.Programs.Select(...) ), the Queryable.Select() extension method actually gets invoked, and its implementation returns an IQueryable<> that represents the extension method getting called on the original IQueryable<> . 当你在表达式树之外调用LINQ方法时(就像你使用context.Programs.Select(...) ),实际上会调用Queryable.Select()扩展方法,并且它的实现返回一个表示IQueryable<>在原始IQueryable<>上调用扩展方法。 Here's the implementation of Select, for instance: 这是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) }
                ));
    }

When the queryable's Provider has to generate actual data from the IQueryable<> , it analyzes the expression tree and tries to figure out how to interpret those method calls. 当queryable的Provider必须从IQueryable<>生成实际数据时,它会分析表达式树并尝试找出如何解释这些方法调用。 Entity Framework has built-in knowledge of many LINQ-related functions like .Where() and .Select() , so it knows how to translate those method calls into SQL. 实体框架具有许多与LINQ相关的函数的内置知识,如.Where().Select() ,因此它知道如何将这些方法调用转换为SQL。 However, it doesn't know what to do for methods that you write. 但是,它不知道如何处理您编写的方法。

So why does this work? 那么为什么这样呢?

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

The answer is that your ForUser method is not implemented like the Select method above: you are not adding an expression to the queryable to represent calling ForUser . 答案是您的ForUser方法没有像上面的Select方法那样实现:您没有在查询中添加表达式来表示调用ForUser Instead, you are returning the result of a .Where() call. 相反,您将返回.Where()调用的结果。 From the IQueryable<> 's perspective, it's as if Where() was called directly, and the call to ForUser() never happened. IQueryable<>的角度来看,就好像Where()被直接调用,并调用ForUser()从未发生过。

You can prove this by capturing the Expression property on the IQueryable<> : 您可以通过捕获IQueryable<>上的Expression属性来证明这一点:

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

... which will produce something like this: ...会产生这样的东西:

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

There's no call to ForUser() anywhere in that expression. 在该表达式中的任何地方都没有调用ForUser()

On the other hand, if you include the ForUser() call inside of an expression tree like this: 另一方面,如果在表达式树中包含ForUser()调用,如下所示:

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

... then the .ForUser() method never actually gets invoked, so it never returns an IQueryable<> that knows the .Where() method got called. ...然后.ForUser()方法实际上从未被调用过,因此它永远不会返回知道被调用的.Where()方法的IQueryable<> Instead, the expression tree for the queryable shows .ForUser() getting invoked . 相反,可查询的表达式树显示.ForUser()被调用 Outputting its expression tree would look something like this: 输出其表达式树看起来像这样:

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

Entity Framework has no idea what ForUser() is supposed to do. 实体框架不知道ForUser()应该做什么。 As far as it's concerned, you could have written ForUser() to do something that's impossible to do in SQL. 就它而言,你可以编写ForUser()来做一些在SQL中无法做到的事情。 So it tells you that's not a supported method. 所以它告诉你这不是一个受支持的方法。

As I mentioned in my comment above, I can't tell why the EF Engine is working the way it is. 正如我在上面的评论中提到的,我不知道为什么EF引擎按照它的方式工作。 Therefore, I've tried to find a way to re-write the query so I'll be able to make use of my extension methods. 因此,我试图找到一种方法来重新编写查询,这样我就可以使用我的扩展方法了。

The tables are: 表格是:

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

ProgramProtocol is just a join table and is not mapped in the model by Entity Framework. ProgramProtocol只是一个连接表,并未由Entity Framework在模型中映射。 The idea is simple: select "from left", select "from right" and then join the resulted sets for proper filtering: 这个想法很简单:选择“从左边”,选择“从右边”,然后加入结果集以进行适当的过滤:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM