Issue with Entity Framework Core: .Include() and .AsNoTracking() not displaying expected related entities

48 Views Asked by At

Since upgrading from .NET Framework to .NET Core, I've encountered an unexpected behavior in Entity Framework Core related to the use of .Include() and .AsNoTracking() methods.

Consider the following entities:

public class Entity1
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Entity2Id { get; set; }
    public Entity2 Entity2 { get; set; }
}

public class Entity2
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Entity1> Entity1s { get; set; }
 }

When I execute the query

Context.Entity1s.AsNoTracking().Include(x => x.Entity2)

and then attempt to access Entity2.Entity1s on the result, only a single element (the parent) is displayed instead of the entire list.

However, removing .AsNoTracking() results in the expected behavior, displaying the complete list of Entity1 elements that are linked to Entity2.

I'm trying to understand why this discrepancy occurs and why it behaves differently in EF Core.

2

There are 2 best solutions below

0
Guru Stron On

This is expected behavior, use AsNoTrackingWithIdentityResolution instead of just AsNoTracking:

Context.Entity1s
   .AsNoTrackingWithIdentityResolution()
   .Include(x => x.Entity2)

Which should result in the desired output (if all the needed entities where fetched in the query).

From the Identity resolution section of the docs:

Since a tracking query uses the change tracker, EF Core does identity resolution in a tracking query. When materializing an entity, EF Core returns the same entity instance from the change tracker if it's already being tracked. If the result contains the same entity multiple times, the same instance is returned for each occurrence. No-tracking queries:

  • Don't use the change tracker and don't do identity resolution.
  • Return a new instance of the entity even when the same entity is contained in the result multiple times.
0
Steve Py On

To expand on Guru's answer, When you specify AsNoTracking() EF populates entities based on only what comes back from the database, it will not go to the tracking cache to fill in references, and the returned entities will not work with lazy loading which would otherwise fill in non-included references if you have lazy loading enabled.

In the case where you want to load all entity1's then using AsNoTrackingWithIdentityResolution should be enough. However, if you put any kind of filter on which Entity1's you load initially then you should eager load those as well, as per cleftharis's comment, but also use AsNoTrackingWithIdentityResolution to ensure that the Entity2.Entity1 referencing back to the initial one you load point to the same reference:

var entities = Context.Entity1s
    .Where(x => /* some condition */)
    .Include(x => x.Entity2)
    .ThenInclude(x => x.Entity1s)
    .AsNoTrackingWithIdentityResolution();

The reason is that otherwise AsNoTrackingWithIdentityResolution will only populate the Entity1 references in the Entity2s based on which Entity1s were loaded. So if an entity2 had a reference to an entity 1 that wasn't returned in the root query's conditions, it won't be populated in the results.

Alternatively, If you instead use:

var entities = Context.Entity1s // with, or without a Where condition...
    .Include(x => x.Entity2)
    .ThenInclude(x => x.Entity1s)
    .AsNoTracking();

... then the results will appear to work, but a big problem is that:

var first = entities.First();
var childsParent = first.Entity2.Entity1s.Single(x => x.Id == first.Id);
bool sameReference = object.ReferenceEquals(first, childsParent);

"sameReference" in this case would be false. While they contain the same row they would be different, separate instances of an entity class. Ensuring the references to the same row use the same instance is the primary purpose for using AsNoTrackingWithIdentityResolution.