S9129 — "Include" and "ThenInclude" chains of reference navigations should be merged into a single "Include" call

Language
C#
Type
Code smell
Severity
Minor
Tags
entity-framework-core, entity-framework, orm, database

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

Related rules

↑ Back to top