S8733 — Potential Cartesian Explosion

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

Loading multiple sibling collection navigations in a single Entity Framework Core query causes the database to return the Cartesian product of those collections, duplicating the root entity’s data across many rows and inflating the query result.

Why is this an issue?

When a query includes two collection navigations on the same root entity, Entity Framework Core joins them in a single SQL statement. The database returns the Cartesian product of the two collections, and each returned row repeats the root entity’s columns.

Consider a Course with two Enrollments and two Announcements:


var courses = await ctx.Courses
    .Include(c => c.Enrollments)
    .Include(c => c.Announcements)
    .ToListAsync();

Entity Framework Core joins both collections onto the root entity in a single SQL statement:


SELECT c.CourseId, c.Title, e.EnrollmentId, e.Grade, a.AnnouncementId, a.Message
FROM Courses AS c
LEFT JOIN Enrollments AS e ON c.CourseId = e.CourseId
LEFT JOIN Announcements AS a ON c.CourseId = a.CourseId
ORDER BY c.CourseId

Because the two joins are unrelated to each other, the database returns every Enrollment/Announcement combination for each course - the Cartesian product of the two collections, with the course’s columns (CourseId, Title) repeated on every row:

c.CourseId c.Title e.EnrollmentId e.Grade a.AnnouncementId a.Message

1050

Chemistry

1

A

1

Midterm moved to Friday

1050

Chemistry

1

A

2

Lab session canceled

1050

Chemistry

2

B

1

Midterm moved to Friday

1050

Chemistry

2

B

2

Lab session canceled

Two Enrollments and two Announcements produce four rows instead of two. With 10 of each, the same query returns 100 rows, and each additional sibling collection multiplies the row count again - so a query that should return a few hundred rows can return hundreds of thousands.

Entity Framework Core also flags this situation at runtime through MultipleCollectionIncludeWarning.

Exceptions

The rule does not apply when:

How to fix it

Call AsSplitQuery on the query so that Entity Framework Core loads each collection in its own SQL statement.

Code examples

Noncompliant code example


var courses = await ctx.Courses
    .Include(c => c.Enrollments)
    .Include(c => c.Announcements)       // Noncompliant: second sibling collection causes a Cartesian explosion
    .ToListAsync();

Compliant solution


var courses = await ctx.Courses
    .Include(c => c.Enrollments)
    .Include(c => c.Announcements)
    .AsSplitQuery()
    .ToListAsync();

How does this work?

AsSplitQuery tells Entity Framework Core to run one SQL statement per collection navigation. Each statement returns only the rows of its own table, so the Cartesian product disappears and the number of rows transferred drops from a product to a sum.

Pitfalls

Split queries run as separate database round trips. If the data changes between them, the returned object graph may be inconsistent. When the application requires a consistent view, wrap the query in a serializable or snapshot transaction.

Combining Skip or Take with AsSplitQuery on Entity Framework Core versions prior to 10 requires an ordering that uniquely identifies each row. Otherwise the split queries may materialize different pages and return mismatched results.

How to opt out

Call AsSingleQuery instead to keep the single join and explicitly accept the Cartesian explosion. Unlike AsSplitQuery, it does not change how the query runs - it only signals, in a way that both this rule and Entity Framework Core’s own runtime warning recognize, that the trade-off was reviewed and accepted rather than overlooked.

Resources

Documentation

↑ Back to top