S9023 — "Include" calls discarded by query reshaping should be fixed

Language
C#
Type
Bug
Severity
Critical
Tags
entity-framework-core, entity-framework, orm, database

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:

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

Related rules

↑ Back to top