An Include call followed by one or more ThenInclude calls can often be written as a single Include call with
a dotted navigation path, when every navigation before the last step of the chain is a reference (single-valued) navigation.
Why is this an issue?
A ThenInclude call continues the navigation path of the Include/ThenInclude call it is chained onto. When
every navigation before the last step of that path is a reference, the whole chain can be written as a single Include call with a dotted
lambda body instead — Entity Framework Core generates the same SQL either way, but the dotted form is one call instead of several, and reads as a
single access path instead of a multi-step chain.
A navigation earlier in the chain must keep using ThenInclude when it is itself a to-many (collection) navigation: Entity Framework
Core’s dotted Include syntax only supports reference navigations for every step but the last.
This is a purely syntactic simplification: once the pattern matches, merging the chain is always safe and never changes what the query loads or how it behaves.
How to fix it
Replace the Include/ThenInclude chain with a single Include call whose lambda body is the equivalent dotted
navigation path.
Code examples
Noncompliant code example
var students = await context.Students
.Include(s => s.School).ThenInclude(sc => sc.District) // Noncompliant: "School" is a reference navigation, this chain can be a single "Include" call
.ToListAsync();
Compliant solution
var students = await context.Students
.Include(s => s.School.District)
.ToListAsync();
A trailing collection navigation can still be folded into the dotted path, as long as every navigation before it is a reference:
var students = await context.Students
.Include(s => s.School.Classes) // "School" is a reference; "Classes" is a collection but it is the last step, so the dotted form is still valid
.ToListAsync();
Exceptions
The rule does not apply to merging across a collection (to-many) navigation — Entity Framework Core has no dotted syntax that crosses a collection navigation partway through the path:
var students = await context.Students
.Include(s => s.Enrollments).ThenInclude(e => e.Grades) // Compliant: "Enrollments" is a collection navigation before the last step, "ThenInclude" cannot be replaced by a dotted "Include"
.ToListAsync();
Multiple ThenInclude calls after the collection can still be merged with each other:
var students = await context.Students
.Include(s => s.Enrollments)
.ThenInclude(e => e.Teacher) // Noncompliant: "Teacher" and "Department" are references, this part of the chain can become "ThenInclude(e => e.Teacher.Department)"
.ThenInclude(t => t.Department)
.ToListAsync();
var students = await context.Students
.Include(s => s.Enrollments)
.ThenInclude(e => e.Teacher.Department)
.ToListAsync();
Each Include/ThenInclude run is evaluated on its own. Repeating Include to branch into two different
ThenInclude continuations does not prevent either branch from being simplified independently:
var students = await context.Students
.Include(s => s.School).ThenInclude(sc => sc.Principal) // Noncompliant: can become "Include(s => s.School.Principal)"
.Include(s => s.School).ThenInclude(sc => sc.VicePrincipal) // Noncompliant: can become "Include(s => s.School.VicePrincipal)", independently of the branch above
.ToListAsync();
Resources
Documentation
- Microsoft Learn - Loading related data - Eager loading, "Including multiple levels" section
Related rules
- {rule:csharpsquid:S8733} - covers loading multiple sibling collections in a single query.
- {rule:csharpsquid:S9022} - covers
Include/ThenIncludecalls that add nothing to the query at all. - {rule:csharpsquid:S9023} - covers
Includecalls that silently drop real data.