问题描述:

Not sure if that's the correct term in the title, but had a question regarding a certain behavior described below.

Given:

public class FooBar

{

// etc

public virtual ICollection<Foo> Foos { get; set; }

public virtual ICollection<Bar> Bars { get; set; }

}

public class Foo

{

}

public class Bar

{

}

public class FooBarRepo

{

private readonly EntitiesContext _context;

public FooBarRepo()

{

this._context = new EntitiesContext();

}

public IQueryable<FooBar> GetIncludeFoo()

{

return this

._context

.FooBars

.Include(i => i.Foos);

}

public IQueryable<FooBar> GetIncludeBar()

{

return this

._context

.FooBars

.Include(i => i.Bars);

}

}

I don't have a test bed to confirm this behavior so wanted to make sure I was interpreting / remembering correctly - but if I were to throw an additional function in defined as such:

public IQueryable<FooBar> GetIncludeBarChainedWithGetIncludeFoo()

{

return this

.GetIncludeFoo()

.Include(i => i.Bars);

}

I seem to recall when calling GetIncludeBarChainedWithGetIncludeFoo(), I'm only getting my Bars, not the additional Foos I would expect from the call to GetIncludeFoo() being a part of the call.

Can this behavior be confirmed/explained?

网友答案:

Disclaimer: EF may have changed since I played with it. I'm writing at the level of EF 4.x. Later versions might have added some better support for propagating Include upwards the query, I actually never have checked. But I somewhat doubt in that. So, please do not take all of this for absolute truth, do try on your own. For example, I don't recall whether GroupBy really breaks the Inclusions, but I'm sure that Select which results in a projection to an anonymous type - does.


No, in your example, it would work.

In your example, when calling GetIncludeBarChainedWithGetIncludeFoo, your code actually calls/builds the following sequence/query:

    // from GetIncludeFoo
    return this
        ._context
        .FooBars
        .Include(i => i.Foos)
    // from GetIncludeBarChainedWithGetIncludeFoo
        .Include(i => i.Bars);

and this will 100%-ly behave as you thought: it will return FooBars with preloaded both Foos and Bars alike. Simple chaining is absolutely OK, just as you also could do:

    // from GetIncludeFoo
    return this
        ._context
        .FooBars
        .Include("Foos")
        .Include("Bars");

However, when you are splitting those two Includes over several methods like you did in your example, a way to a subtle pitfall opens. Let's look at:

    return this
        .GetIncludeFoo()       // let's say they all are extensions just for clarity
        .DoSomeMyOtherThings()
        .GetIncludeBar()

If the "DoSomeMyOtherThings" makes anything that will cause the query to change its shape and loose its tight binding to a specific table, like a projection, then the IncludeBar will fail even if it seemed ok. All includes (that define new sources to be pulled), should precede any other operations.

    return this
        .GetIncludeFoo()       // let's say they all are extensions just for clarity
        .GetIncludeBar()
        .DoSomeMyOtherThings()

It may be seem obvious, but when you start wrapping includes/wheres/selects/groupbys into pretty methods and when you then start mixing methods together, it's really easy to forget about that:

    return this
        .GetFooThatAre("Green")
        .GetBestBarForEachFoo()
        .GetBuzzFromBar()

The first method will be able to Include "Foos". The second method probably will succeed in Includeing "Bars". However the last one may be unable to Include "Buzz" because of the groupby and/or projection hidden in the second method.

Even when using pretty wrappers, the query still has to be:

    return this
        .GetIncludeFoo()
        .GetIncludeBar()
        .GetIncludeBuzz()
        .GetFooThatAre("Green")
        .GetBestBarForEachFoo()
        .GetBuzzFromBar()

or someting like that.

网友答案:

I must have had a slightly different scenario, had a ToList() somewhere in there, or something similar because I am unable to reproduce the scenario I had a question on:

public class FooBarRepo : IFooBarRepo
{

    private readonly Entities _context;

    public FooBarRepo()
    {
        this._context = new Entities();
    }

    public IQueryable<FooBar> Get()
    {
        return this._context.FooBar;
    }

    public IQueryable<FooBar> GetIncludeFoo()
    {
        return this.Get().Include(i => i.Foo);
    }

    public IQueryable<FooBar> GetIncludeBar()
    {
        return this.Get().Include(i => i.Bar);
    }

    public IQueryable<FooBar> GetIncludeFooAndBar()
    {
        return this
            .Get()
            .Include(i => i.Foo)
            .Include(i => i.Bar);
    }

    public IQueryable<FooBar> GetIncludeFooAndChainBar()
    {
        return this.GetIncludeBar().Include(i => i.Foo);
    }

    public void Dispose()
    {
        this._context.Dispose();
    }
}

class Program
{
    static void Main(string[] args)
    {

        IEnumerable<FooBar> get;
        IEnumerable<FooBar> getIncludeFoo;
        IEnumerable<FooBar> getIncludeBar;
        IEnumerable<FooBar> getIncludeFooAndBar;
        IEnumerable<FooBar> getIncludeFooAndChainBar;

        using (var context = new EntityFrameworkTesting.TestIncludeChaining.Repository.FooBarRepo())
        {
            get = context.Get().ToList();

            getIncludeFoo = context.GetIncludeFoo().ToList();

            getIncludeBar = context.GetIncludeBar().ToList();

            getIncludeFooAndBar = context.GetIncludeFooAndBar().ToList();

            getIncludeFooAndChainBar = context.GetIncludeFooAndChainBar().ToList();
        }
    }
}

generated SQL:

-- get
SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name]
FROM [dbo].[_FooBar] AS [Extent1]

-- getIncludeFoo
SELECT 
[Project1].[Id] AS [Id], 
[Project1].[Name] AS [Name], 
[Project1].[C1] AS [C1], 
[Project1].[Id1] AS [Id1], 
[Project1].[FooBarId] AS [FooBarId]
FROM ( SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[Id] AS [Id1], 
    [Extent2].[FooBarId] AS [FooBarId], 
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
    FROM  [dbo].[_FooBar] AS [Extent1]
    LEFT OUTER JOIN [dbo].[_Foo] AS [Extent2] ON [Extent1].[Id] = [Extent2].[FooBarId]
)  AS [Project1]
ORDER BY [Project1].[Id] ASC, [Project1].[C1] ASC

-- getIncludeBar
SELECT 
[Project1].[Id] AS [Id], 
[Project1].[Name] AS [Name], 
[Project1].[C1] AS [C1], 
[Project1].[Id1] AS [Id1], 
[Project1].[FooBarId] AS [FooBarId]
FROM ( SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[Id] AS [Id1], 
    [Extent2].[FooBarId] AS [FooBarId], 
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
    FROM  [dbo].[_FooBar] AS [Extent1]
    LEFT OUTER JOIN [dbo].[_Bar] AS [Extent2] ON [Extent1].[Id] = [Extent2].[FooBarId]
)  AS [Project1]
ORDER BY [Project1].[Id] ASC, [Project1].[C1] ASC

-- getIncludeFooAndBar
SELECT 
[UnionAll1].[Id] AS [C1], 
[UnionAll1].[Id1] AS [C2], 
[UnionAll1].[Name] AS [C3], 
[UnionAll1].[C1] AS [C4], 
[UnionAll1].[Id2] AS [C5], 
[UnionAll1].[FooBarId] AS [C6], 
[UnionAll1].[C2] AS [C7], 
[UnionAll1].[C3] AS [C8]
FROM  (SELECT 
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1], 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Id] AS [Id1], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[Id] AS [Id2], 
    [Extent2].[FooBarId] AS [FooBarId], 
    CAST(NULL AS int) AS [C2], 
    CAST(NULL AS int) AS [C3]
    FROM  [dbo].[_FooBar] AS [Extent1]
    LEFT OUTER JOIN [dbo].[_Foo] AS [Extent2] ON [Extent1].[Id] = [Extent2].[FooBarId]
UNION ALL
    SELECT 
    2 AS [C1], 
    [Extent3].[Id] AS [Id], 
    [Extent3].[Id] AS [Id1], 
    [Extent3].[Name] AS [Name], 
    CAST(NULL AS int) AS [C2], 
    CAST(NULL AS int) AS [C3], 
    [Extent4].[Id] AS [Id2], 
    [Extent4].[FooBarId] AS [FooBarId]
    FROM  [dbo].[_FooBar] AS [Extent3]
    INNER JOIN [dbo].[_Bar] AS [Extent4] ON [Extent3].[Id] = [Extent4].[FooBarId]) AS [UnionAll1]
ORDER BY [UnionAll1].[Id1] ASC, [UnionAll1].[C1] ASC

-- getIncludeFooAndChainBar
SELECT 
[UnionAll1].[Id] AS [C1], 
[UnionAll1].[Id1] AS [C2], 
[UnionAll1].[Name] AS [C3], 
[UnionAll1].[C1] AS [C4], 
[UnionAll1].[Id2] AS [C5], 
[UnionAll1].[FooBarId] AS [C6], 
[UnionAll1].[C2] AS [C7], 
[UnionAll1].[C3] AS [C8]
FROM  (SELECT 
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1], 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Id] AS [Id1], 
    [Extent1].[Name] AS [Name], 
    [Extent2].[Id] AS [Id2], 
    [Extent2].[FooBarId] AS [FooBarId], 
    CAST(NULL AS int) AS [C2], 
    CAST(NULL AS int) AS [C3]
    FROM  [dbo].[_FooBar] AS [Extent1]
    LEFT OUTER JOIN [dbo].[_Bar] AS [Extent2] ON [Extent1].[Id] = [Extent2].[FooBarId]
UNION ALL
    SELECT 
    2 AS [C1], 
    [Extent3].[Id] AS [Id], 
    [Extent3].[Id] AS [Id1], 
    [Extent3].[Name] AS [Name], 
    CAST(NULL AS int) AS [C2], 
    CAST(NULL AS int) AS [C3], 
    [Extent4].[Id] AS [Id2], 
    [Extent4].[FooBarId] AS [FooBarId]
    FROM  [dbo].[_FooBar] AS [Extent3]
    INNER JOIN [dbo].[_Foo] AS [Extent4] ON [Extent3].[Id] = [Extent4].[FooBarId]) AS [UnionAll1]
ORDER BY [UnionAll1].[Id1] ASC, [UnionAll1].[C1] ASC

If I happen across the actual scenario again which I was unable to find, I'll update/ask a new question. But in the interim, thanks for the sanity check.

相关阅读:
Top