An Include or ThenInclude call can be entirely inert: Entity Framework Core generates the exact same SQL with or without
it.
Why is this an issue?
Include and ThenInclude tell Entity Framework Core to eagerly load a related navigation together with the entity that
owns it. In each of the following cases, that instruction has no effect on the generated SQL — it adds nothing but noise, and can safely be
removed:
- it repeats a navigation path already included elsewhere in the query, without adding a
ThenIncludethat goes deeper than the earlier call. Entity Framework Core resolves the navigation path of everyInclude/ThenIncludecall and merges calls that resolve to the same path into a single join. - a
Select,SelectMany, orJoinfurther downstream projects the same navigation directly — bare, or wrapped in an anonymous type or constructor call. Entity Framework Core materializes that navigation because the projection asks for it, independently of anyInclude. - a
Select,SelectMany, orJoinfurther downstream dereferences into the navigation without returning it directly — a nested property, a value derived from its elements, or a boolean fact about it (a null check, for example). Entity Framework Core still needs the same join to compute that value, independently of anyInclude. - the query ends in an aggregate or scalar operator — such as
Count,Sum,Any,All,Average, orMax/Minwith a selector, and their async equivalents — that never returns the entity at all. AnIncludeonly affects entities the query actually returns. - the navigation’s type is annotated with
[Owned]. Entity Framework Core always loads an owned entity together with its owner, whether the navigation is a single owned reference (OwnsOne) or an owned collection (OwnsMany), so anIncludetargeting it changes nothing.
In every case, it can also mislead a reader into thinking it changes what is loaded.
Exceptions
The rule does not apply when:
- the repeated
Includecall re-anchors the chain before a differentThenInclude. Entity Framework Core has no syntax for attaching two differentThenIncludecontinuations to oneInclude, so repeating it is the idiomatic way to load two independent nested navigations or collections. - one of the calls applies a filter (through
Where,Skip,Take, or similar) that the other does not carry, regardless of which one is written first — removing the filtered call would change which rows are loaded. - the
Includecarries a customization (Where,OrderBy,Skip,Take, or similar) that the downstream projection does not reproduce — see S9023, which covers that case. - the navigation is configured as owned only through the Fluent API (
OwnsOne/OwnsManyinOnModelCreating), without an[Owned]attribute on the type. That configuration isn’t visible to static analysis, so the rule does not flag it — even though the same always-loaded behavior applies.
How to fix it
Remove the Include (or ThenInclude) call that has no effect.
Code examples
Noncompliant code example
var students = await context.Students
.Include(s => s.Enrollments) // Noncompliant: "Enrollments" is already included below
.Include(s => s.Enrollments)
.ToListAsync();
Compliant solution
var students = await context.Students
.Include(s => s.Enrollments)
.ToListAsync();
This repeated Include is not redundant — it branches into two different ThenInclude calls:
var students = await context.Students
.Include(s => s.Enrollments).ThenInclude(e => e.Course)
.Include(s => s.Enrollments).ThenInclude(e => e.Student) // Compliant: re-anchors before a different ThenInclude
.ToListAsync();
An Include is redundant the same way when a projection further down the query already returns the navigation directly:
var students = await context.Students
.Include(s => s.Enrollments) // Noncompliant: "Enrollments" is already required by the projection below
.Select(s => new { s.LastName, s.Enrollments })
.ToListAsync();
var students = await context.Students
.Select(s => new { s.LastName, s.Enrollments })
.ToListAsync();
A projection that only dereferences into the navigation — instead of returning it, as in the example above — makes the Include
redundant just the same, because Entity Framework Core still needs the same join to compute that value:
var principals = await context.Students
.Include(s => s.School) // Noncompliant: "School" is already required by the projection below
.Select(s => s.School.Principal)
.ToListAsync();
var principals = await context.Students
.Select(s => s.School.Principal)
.ToListAsync();
It is also redundant when the query never returns the entity at all:
var studentCount = await context.Students
.Include(s => s.Enrollments) // Noncompliant: this "Include" has no effect; the following call to "CountAsync" never returns the entity
.CountAsync();
var studentCount = await context.Students
.CountAsync();
An Include is also redundant when it targets an owned entity — Entity Framework Core loads it together with its owner regardless:
var students = await context.Students
.Include(s => s.HomeAddress) // Noncompliant: "HomeAddress" is an owned entity and is always loaded with its owner
.ToListAsync();
var students = await context.Students
.ToListAsync();
Resources
Documentation
- Microsoft Learn - Loading related data - Eager loading