簡體   English   中英

Linq到實體擴展方法內部查詢(EF6)

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

有人可以向我解釋為什么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 Engine能夠構建整個表達式樹,鏈接必要的表達式然后生成SQL。 為什么不這樣做?

發生這種情況是因為對ForUser()的調用是在C#編譯器看到傳遞給Select的lambda時構建的表達式樹內部進行的。 實體框架試圖弄清楚如何將該函數轉換為SQL,但由於d.Protocols原因它無法調用該函數(例如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) }
                ));
    }

當queryable的Provider必須從IQueryable<>生成實際數據時,它會分析表達式樹並嘗試找出如何解釋這些方法調用。 實體框架具有許多與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))

實體框架不知道ForUser()應該做什么。 就它而言,你可以編寫ForUser()來做一些在SQL中無法做到的事情。 所以它告訴你這不是一個受支持的方法。

正如我在上面的評論中提到的,我不知道為什么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();

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM