-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Description
Problem
In a large application, it can be difficult to know if a navigation collection is loaded or not deep in the code (say you have method that gets passed an entity and you need to access a navigation collection property on the entity). Especially if multiple callers of the method pass an entity loaded in different ways.
We tried to solve this problem by making navigation collections that aren't always loaded non-nullable, and "optional" navigation collections nullable. That way if it's null, we would load it then:
if (entity.CollectionProperty is null) {
await dbContext.Entry(entity).Collection(e => e.CollectionProperty).LoadAsync();
}Or in some cases we would assert that it's not null (if we had expected the caller to Include that collection.
Debug.Assert(entity.CollectionProperty is not null);However, as we learned, this leads to subtle bugs due to EF Core's automatic fixup feature. I wrote a blog post that demonstrates the issue. We can't rely on the nullability of a collection to know that it's loaded or not.
So the only viable solution is to always check the entry's IsLoaded property if we want to assert that it's loaded:
Debug.Assert(dbContext.Entry(entity).Collection(e => e.CollectionProperty).IsLoaded);Or if we're fine with lazy loading, we can do this
// Ensure the collection is loaded. This already checks `IsLoaded`.
await dbContext.Entry(entity).Collection(e => e.CollectionProperty).LoadAsync();Proposed Solution
Correct me if I'm wrong, but when EF evaluates a query and returns an entity, it knows at that time whether navigation properties are loaded or not. What I would like is to be able to apply an interface on a collection property and have EF populate the IsLoaded property of that interface. For example:
public interface IEntityCollection<T> : ICollection<T> {
bool IsLoaded { get; }
}Now suppose I have the following entity:
public class Blog {
public IEntityCollection<Post> Posts { get; set; } = null!;
}I could then run the following code with the following results:
var blog = await dbContext.Blog.FirstOrDefaultAsync(b => b.Id == 123);
Assert.False(blog.Posts.IsLoaded); // Passes
var blog2 = await dbContext.Blog.Include(b => b.Posts).FirstOrDefaultAsync(b => b.Id == 123);
Assert.True(blog.Posts.IsLoaded); // PassesIt might make sense to have IEntityList<T> too.
Thanks for your consideration.