An Include call can compile and run without error while Entity Framework Core silently drops what it requested — sometimes the related
data itself, sometimes just a filtering, ordering, or paging customization applied to it.
Why is this an issue?
Entity Framework Core tracks pending Include calls against the entity type currently flowing through the query. A reshaping operation
downstream can invalidate that tracking in two ways:
- An operation such as
SelectManythat reshapes the query to a different entity type drops anyIncluderequested before it, without an exception or a warning — the missing data goes unnoticed until something depending on it breaks. - A
Selectthat reprojects the same navigation property directly keeps the navigation itself, but drops any customization chained onto it inside theInclude(Where,OrderBy,Skip,Take, or similar). The projection re-derives the navigation on its own, independently of theInclude, so it returns the full, uncustomized collection instead of the one theIncludeasked for.
What is the potential impact?
A dropped Include defeats the purpose of asking for eager loading. What happens next depends on whether lazy loading is enabled. Without it, the navigation stays unloaded —
null for a reference, empty for a collection — surfacing as a NullReferenceException or as silently incomplete data far from
the query that caused it. With it, EF Core fetches the navigation on first access instead, trading the one intended query for many.
When it’s the customization that’s lost instead, the navigation is still there, but with more rows than expected: a "5 most recent comments" widget quietly shows every comment ever posted, or an "approved only" filter quietly lets unapproved rows through too. Because the query still runs and still returns data, this surfaces as a subtly wrong result rather than an exception — easy to miss in review, and easy to miss in testing with a small enough dataset.
How to fix it
Move the Include to after the reshaping operation, anchored on the new entity type — see the compliant example below, where the
Include moves from Course to Enrollment once the query is reshaped by SelectMany. This requires
Enrollment to expose a navigation property back to Course, as it does here.
When the reshape instead reprojects the navigation directly and only a customization on it is lost, move that customization into the projection
itself — see the second example below, where the filter moves from the Include into the Select.
Code examples
Noncompliant code example
var enrollments = await context.Courses
.Include(c => c.Announcements) // Noncompliant: dropped once SelectMany reshapes the query to Enrollment
.SelectMany(c => c.Enrollments)
.ToListAsync();
A Select that reprojects a navigation directly loses less, but still loses something: the navigation itself survives, while any
customization chained onto it inside the Include does not:
var posts = await context.Posts
.Include(p => p.Comments.Where(c => c.IsApproved)) // Noncompliant: filtering is lost once Select reprojects Comments directly
.Select(p => new { p.Title, p.Comments })
.ToListAsync();
Compliant solution
var enrollments = await context.Courses
.SelectMany(c => c.Enrollments)
.Include(e => e.Course).ThenInclude(c => c.Announcements)
.ToListAsync();
var posts = await context.Posts
.Select(p => new { p.Title, Comments = p.Comments.Where(c => c.IsApproved).ToList() })
.ToListAsync();
The same applies to OrderBy, Skip, and Take chained the same way — moving them into the projection is the
fix in every case.
Resources
Documentation
- Microsoft Learn - Loading related data - Eager loading
- Microsoft Learn - Loading related data - Lazy loading