Performance C# active any

Preload related data before iterating over navigation properties

perf-csharp-001

Intent

Accessing unloaded navigation properties inside a loop turns one query into many roundtrips and produces latency that scales with result size.

Applicability

Applies when the diff adds or changes EF Core queries that iterate over entity collections and read related entities or navigation-derived counts inside the loop. Return unknown if the surrounding code does not show whether the navigation data was already loaded or projected.

What to inspect

Queries that materialize entity lists, subsequent loops over those entities, navigation property access inside the loop body, and whether the original query uses Include, explicit projection, or another visible preload of the needed related data.

Pass criteria

The needed related data is loaded in the original query via projection or explicit eager loading, so the loop does not trigger additional per-entity database queries.

Fail criteria

The diff materializes a set of EF entities and then accesses an unloaded navigation property or related aggregate inside a loop, creating one database fetch per entity.

Do not flag

Do not flag loops over DTOs or anonymous projections that already contain the needed related values. Do not flag code where the diff clearly shows the navigation was eagerly loaded before the loop. Do not flag test code using in-memory collections rather than a real EF-backed query path.

Confidence guidance

HIGH when the query and the loop are both visible, and the loop reads a navigation property that was not projected or eagerly loaded. MEDIUM when the loop clearly reads related data per entity, but the query setup is partly outside the diff. LOW when the code suggests a potential N+1 pattern, but repository evidence is too incomplete to show whether lazy loading or prior loading actually occurs.

Remediation

Load the required related data in the original query with projection or explicit eager loading.

Pass example

var users = await context.Users
    .Select(u => new
    {
        u.Id,
        OrderCount = u.Orders.Count
    })
    .ToListAsync();

foreach (var user in users)
{
    Console.WriteLine(user.OrderCount);
}

Fail example

var users = await context.Users.ToListAsync();

foreach (var user in users)
{
    Console.WriteLine(user.Orders.Count);
}

Sources

  • EF Core performance docs — Vlad Mihalcea article