NP0001: A query runs once per element (N+1 read)
Default severity: Warning
What it detects
A database query that runs once for every element of a collection: one query for the list, then N more. The analyzer reports it where the query runs, or where the call that leads to it is made:
- inside
for,foreach,await foreach,whileanddoloops (not in theforeachcollection, which runs once); - inside delegates that run once per element: LINQ to Objects lambdas (
Select,Where,ToDictionary, ...),List.ForEach,Array.ForEach,Parallel.For/ForEach/ForEachAsync, EF Core'sForEachAsync,Task.WhenAll(ids.Select(...)), method groups (ids.Select(Load)), FluentValidation rules chained onRuleForEach(Must,MustAsync,Custom,CustomAsync), and your own helpers that call a delegate in a loop; - through calls: private helpers, public services, interface and virtual methods whose implementation is in the
project or in a referenced project (a
base.Method(...)call runs only the base implementation) (through the summaries the package's generator writes into each assembly at build time), local functions, recursion, up tonplusone.max_call_depthcalls deep; - through mediators:
sender.Send(new GetOrderTotal(id))(MediatR'sISender/IMediator, and the source-generated Mediator library) follows theIRequestHandler<GetOrderTotal, ...>.Handleimplementations in the project (also for requests without a response and handlers that inheritHandlefrom a base class; for the Mediator library alsoICommandHandlerandIQueryHandler),PublishtheINotificationHandler<T>ones andCreateStreamtheIStreamRequestHandler<,>ones; MediatR'sSendalso runs every pipeline behavior (IPipelineBehavior<,>) the project registers (AddOpenBehavior(typeof(AuditBehavior<,>)),AddBehavior<...>(),typeofin a service registration) whose constraints accept the request; - through ABP Framework's repositories, also in an application layer that references no EF Core:
GetAsync,FindAsync,GetListAsync,GetCountAsync,GetPagedListAsync,DeleteAsync(id),EnsureExistsAsync,EnsurePropertyLoadedAsync, the repository operators (repository.FirstOrDefaultAsync(...),CountAsync, ...),AsyncExecutergiven the repository's query, andGetQueryableAsync()/GetDbSetAsync()run in the loop; - in a query's final projection, which EF Core evaluates on the client once per row: a method of the project called
there (
db.Customers.Select(c => new { c.Id, N = counter.Count(c.Id) })) that queries; - through AutoMapper value resolvers and type converters (
MapFrom<LineCountResolver>(),ConvertUsing<T>()) that query, when a sequence (or an element in a loop) is mapped: the resolver runs once per element; - through delegates kept in locals, fields or auto-properties (
Func<int, Task<int>> count = id => db.Orders.CountAsync(...), thenawait count(id)in the loop), whether they hold a lambda or a method group, and compiled queries (EF.CompileQuery(...),EF.CompileAsyncQuery(...)), which run their query on every call; - through property getters and indexers that query (
row.OrderCount,lookup[id]), read in the loop or in a method it calls; assigning a property runs only its setter; - on query fields and auto-properties the type sets in its constructor or initializers
(
_big = db.Orders.Where(...)); a field also assigned something in memory is not known and stays quiet.
A query is any EF Core round trip: an executing operator on an EF query (ToList, FirstOrDefaultAsync,
CountAsync, AnyAsync, ...), foreach over an EF query, Find/FindAsync, explicit loading (Entry(e).Reference(...).Load()),
Reload, raw SQL (ExecuteSql, FromSql, SqlQuery) and ExecuteUpdate/ExecuteDelete.
var customers = await db.Customers.ToListAsync();
foreach (var c in customers)
{
var orders = await db.Orders.Where(o => o.CustomerId == c.Id).ToListAsync(); // NP0001
}
Why it matters
Each element costs a round trip. With 10 rows in development it is invisible; with 5,000 rows in production it is 5,000 queries, connection-pool pressure and seconds of latency.
How to fix it
The message suggests the rewrite for the query's shape:
// Rows filtered by a key: load them all at once and group them.
var ids = customers.Select(c => c.Id).ToList();
var ordersByCustomer = (await db.Orders.Where(o => ids.Contains(o.CustomerId)).ToListAsync())
.ToLookup(o => o.CustomerId);
// Find by primary key: load them all at once and index them.
var byId = await db.Orders.Where(o => ids.Contains(o.Id)).ToDictionaryAsync(o => o.Id);
// Counts and sums: one grouped query.
var counts = await db.Orders.GroupBy(o => o.CustomerId)
.Select(g => new { g.Key, Count = g.Count() }).ToDictionaryAsync(x => x.Key, x => x.Count);
Often the cleanest fix is to fold the data into the first query with Include or a Select projection.
What it leaves alone
-
A loop over a set of different implementations (
foreach (var check in checks) await check.RunAsync()) when only some of them query: each runs once. -
Queries inside expression trees (
db.Customers.Select(c => c.Orders.Count())): EF Core translates them into the same SQL statement. -
foreachoverChunk(...): one query per batch is the fix, not the problem. -
for/while/doloops that page (OrderByandTake), poll (Task.Delay,Thread.Sleep), or search until the query answers (generate a code until it is unique) and walk no collection meanwhile. -
Load-once guards:
x ??= query,if (x is null) { x = query; }. -
LINQ to Objects, including
AsQueryable()over an in-memory list.
Configuration
| Key | Meaning |
|---|---|
nplusone.query_methods | Methods that run a query on every call, for code the analyzer cannot see into (MyApp.Domain.IOrderRepository.*). |
nplusone.ignored_methods | Methods never treated as running a query (cached lookups). |
nplusone.iterating_methods | Methods that call their delegate argument once per element. |
nplusone.max_call_depth | How many calls deep the analysis follows (default 4). |
What it cannot see
An implementation in an assembly the project does not reference (see NP0006), queries reached through
reflection or dynamic dispatch the compiler cannot resolve, and code deeper than max_call_depth. The
runtime guard catches these in tests.