2

Closed

EF loads entire child collection when item is added to context

description

I've found that when using lazy child collections, the collection is loaded when an item is added directly to the context. This is a problem because the collection may be very large. I think EF wants to update the in memory collection with the new item, but it should realize that Í didn't request the collection.

One workaround is to make the property non virtual. Another is to make the parent class internal (WTF!?)

Here's code that reproduces the issue. Put a breakpoint in the last Add() call and confirm with SQL Server Profiler that all citizens (possible million, depends on the country) are loaded.

public class Country // make internal to avoid spurious select
{
public virtual int CountryID { get; set; }

public virtual ICollection<Citizen> Countries { get; set; } // make non virtual to avoid select
}
public class Citizen
{
public virtual int CitizenID { get; set; }

public virtual int CountryID { get; set; }
}
class Context : DbContext
{
public DbSet<Country> Countries { get; set; }
public DbSet<Citizen> Citizens { get; set; }
}
class Program
{
static void Main()
{
    using (var db = new Context())
    {
        db.Countries.Add(new Country());
        db.SaveChanges();
    }
    using (var db = new Context())
    {
        var c = db.Countries.FirstOrDefault();
        db.Citizens.Add(new Citizen { CountryID = c.CountryID, }); // loads all citizens of the country
    }
}
}

file attachments

Closed Jan 28 at 8:28 PM by BriceLambson

comments

ajcvickers wrote Dec 17, 2012 at 10:28 PM

We have not been able to reproduce this issue with the code you provided. Assuming the database has multiple citizens for the given country, if I then execute the code you provided to add a new citizen the navigation property only contains one item. (It contains one item because EF does fixup on the navigation property.) It does not contain all the items that are in the database but not loaded.

There are cases where the entire navigation property collection will be loaded, including any time the navigation property is accessed explicitly for the first time, but it is hard to know how to advise the best way to approach these without a working repro of the actual problem you are having.

Thanks,
Arthur

ajcvickers wrote Dec 17, 2012 at 10:29 PM

Unassigned following investigation. Will need to re-triage if we get a valid repro.

br1 wrote Dec 18, 2012 at 12:07 PM

Try this revised code
static void Main()
{
    using (var db = new Context())
    {
        db.Database.Delete();
        var country = new Country();
        db.Countries.Add(country);
        for (int i = 0; i < 10; ++i)
        {
            db.Citizens.Add(new Citizen { CountryID = country.CountryID, });
        }
        db.SaveChanges();
    }
    using (var db = new Context())
    {
        var country = db.Countries.Single();
        db.Citizens.Add(new Citizen { CountryID = country.CountryID, }); // loads all citizens of the country
    }
}
The last line runs this query:

exec sp_executesql N'SELECT
[Extent1].[CitizenID] AS [CitizenID],
[Extent1].[CountryID] AS [CountryID]
FROM [dbo].[Citizens] AS [Extent1]
WHERE [Extent1].[CountryID] = @EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=1

which retrieves the 10 citizens added in the previous session.

br1 wrote Dec 18, 2012 at 12:08 PM

I've also attached the sql profiler trace.

ajcvickers wrote Dec 18, 2012 at 9:29 PM

The reason you are seeing this behavior is because you are using change tracking proxies. Change tracking proxies perform fixup for you when possible. That is, when you set the FK property "CountryID" the proxy will find that the appropriate Country is being tracked and will add it to the navigation property. This causes access of the navigation property which in turn causes lazy loading to happen.

We will consider changing the behavior to not cause the collection to load in this case.

For now, there are various workarounds:
  1. Don't use change tracking proxies. They can be useful, but more often than not they just cause problems like the one above. See http://blog.oneunicorn.com/2011/12/05/should-you-use-entity-framework-change-tracking-proxies/ for more details. The easiest way to stop using change tracking proxies is to make non-navigation properties (i.e. the keys in your example) not virtual.
  2. Switch off lazy loading when setting the FK. Use context.Configuration.LazyLoadingEnabled.
  3. Just load the FK from the database (e.g. db.Countries.Select(e => e.CountryID).Single();) such that the country entity isn't tracked and fixup will not happen.

br1 wrote Dec 19, 2012 at 12:17 AM

I'm glad that you'll consider improving tracking proxies in the future. I've already de-virtualized my properties as you suggest. Now, could you share why the visibility of Country affects this feature? I'm just curious.

br1 wrote Dec 19, 2012 at 12:20 AM

Never mind, I just read in your blog that classes must be public to allow tracking proxies. Thanks again.

glennc wrote Dec 19, 2012 at 10:54 PM

Consider improving in EF6

ajcvickers wrote Feb 6, 2013 at 4:07 PM

Results of investigation: I have prototyped a fix for this issue that disables lazy loading when accessing navigation properties internally for fixup. All current tests pass with the prototyped fix. However, the fix is somewhat risky for a couple of reasons:
  • It is somewhat of a breaking change since code that relies on the side effect and assumes that a collection is loaded will now fail. This is mitigated by the fact that in almost all cases a subsequent access of the property (or its inverse) will cause the load to happen anyway.
  • The code that accesses navigation properties is called from a myriad of code paths, not all of which are used solely for fixup. There is a chance that by making a change like this we will disable lazy loading in scenarios where it is currently happening appropriately.

RoMiller wrote Feb 7, 2013 at 10:28 PM

EF Team Triage: Given that this is a risky change we aren't going to take it in EF6 but we do think we should take it in the future.

AndriySvyryd wrote Dec 3, 2013 at 6:51 PM

https://entityframework.codeplex.com/workitem/1874 should also be fixed by fising this.

ajcvickers wrote Jan 10 at 5:40 PM

Poaching

ajcvickers wrote Jan 10 at 8:34 PM

Fixed in 7cb5161381d4

No children in bar... (683: EF loads entire child collection when item is added to context)

The fix is to disables lazy loading when accessing navigation properties internally for fixup. Note that this is technically a breaking change since some application may be relying on the behavior that related entities are loaded at this time instead of later when the navigation property or state manager is accessed to get them. However, this seems unlikely and easy to fix so we have decided to take the change.

BriceLambson wrote Jan 28 at 8:28 PM

Verified