Documentation

EF Core Query Performance Checklist

A practical EF Core query performance checklist for reviewing LINQ queries, N+1 risks, raw SQL safety, tracking modes, and CI enforcement with LinqContraband.

EF Core Query Performance Checklist

Use this EF Core query performance checklist when reviewing pull requests, tightening team standards, or adding compile-time query analysis to CI. It maps common review questions to LinqContraband rules so teams can move repeatable feedback out of human memory and into analyzer diagnostics.

Install the official analyzer package:

dotnet add package LinqContraband

Pull Request Checklist

Check Why it matters LinqContraband signal
Avoid database calls inside loops. Loop execution can turn one request into many database roundtrips. LC007: database execution inside loop
Batch SaveChanges outside loops unless each item needs its own commit boundary. Repeated saves can turn one unit of work into many transactions and partial-progress states. LC010: SaveChanges inside loop
Keep query logic translatable to SQL. Local helpers, provider-sensitive string overloads, and complex grouping can fail translation or push work into memory. LC001: local method, EF Core client-side evaluation analyzer
Order before pagination or positional access. Unordered Skip, Take, Last, ElementAt, or Chunk queries can return unstable rows. LC015: missing OrderBy, EF Core pagination OrderBy analyzer
Materialize only after filtering, ordering, and projection. Early ToList, AsEnumerable, or ToArray can move work from SQL into memory. LC002: premature materialization, EF Core premature materialization analyzer
Use projection when only a few fields are needed. Loading whole entities increases network, memory, and tracking cost. LC017: whole entity projection, LC041: single entity scalar projection, EF Core projection analyzer
Make related data loading explicit. Missing includes can produce null navigation data, lazy-loading churn, or hidden N+1 behaviour. LC045: missing include
Keep eager loading bounded. Overusing Include can create cartesian explosion or very wide result graphs. LC006: cartesian explosion, LC038: excessive eager loading
Prefer async EF Core APIs in async methods. Synchronous EF Core calls in async paths block request threads. LC008: sync-over-async, EF Core async query analyzer
Pass cancellation tokens through async query APIs. Long-running queries should respect request cancellation and shutdown paths. LC026: missing cancellation token, EF Core CancellationToken analyzer
Keep DbContext lifetimes scoped and single-threaded. Long-lived or cross-thread contexts can leak tracked state, cross request boundaries, or throw at runtime. LC030: DbContext lifetime mismatch, LC036: DbContext captured across threads, EF Core DbContext lifetime analyzer
Use read-only tracking intentionally. Tracking every read increases memory and can create confusing mixed-mode behaviour. LC009: missing AsNoTracking, LC040: mixed tracking modes
Keep raw SQL parameterized. Interpolation and string construction can turn EF Core raw SQL into injection risk. LC018: interpolated raw SQL, LC034: interpolated command SQL, LC037: constructed raw SQL strings
Review global filter bypasses. IgnoreQueryFilters can skip tenant, soft-delete, or security boundaries. LC021: IgnoreQueryFilters
Bound destructive set-based writes. Bulk delete/update without a filter can affect more rows than intended. LC035: missing Where before bulk execute
Prefer reviewed set-based writes for large batches. Tracked loops and RemoveRange can load rows that EF Core can update or delete directly in SQL. LC032: ExecuteUpdate, LC012: ExecuteDelete over RemoveRange
Tag complex queries before they reach production. Query tags make expensive SQL easier to trace in database telemetry. LC042: missing query tags

Quick Severity Policy

Start with analyzer warnings, then promote a focused rule set to errors once the team has cleared existing findings:

[*.cs]

# Expensive or risky query shapes
dotnet_diagnostic.LC001.severity = warning
dotnet_diagnostic.LC002.severity = warning
dotnet_diagnostic.LC014.severity = warning
dotnet_diagnostic.LC015.severity = warning
dotnet_diagnostic.LC020.severity = warning
dotnet_diagnostic.LC024.severity = warning
dotnet_diagnostic.LC007.severity = error
dotnet_diagnostic.LC045.severity = warning
dotnet_diagnostic.LC036.severity = warning

# Async EF Core execution
dotnet_diagnostic.LC008.severity = warning
dotnet_diagnostic.LC026.severity = suggestion
dotnet_diagnostic.LC043.severity = suggestion

# Raw SQL and security-sensitive paths
dotnet_diagnostic.LC018.severity = error
dotnet_diagnostic.LC034.severity = error
dotnet_diagnostic.LC037.severity = error
dotnet_diagnostic.LC021.severity = warning

Use project-wide error severities for rules that represent team policy. Use narrow pragmas or SuppressMessage attributes only when a reviewer has accepted a specific exception.

How To Use This Checklist