Skip to main content

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, while and do loops (not in the foreach collection, 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's ForEachAsync, Task.WhenAll(ids.Select(...)), method groups (ids.Select(Load)), FluentValidation rules chained on RuleForEach (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 to nplusone.max_call_depth calls deep;
  • through mediators: sender.Send(new GetOrderTotal(id)) (MediatR's ISender/IMediator, and the source-generated Mediator library) follows the IRequestHandler<GetOrderTotal, ...>.Handle implementations in the project (also for requests without a response and handlers that inherit Handle from a base class; for the Mediator library also ICommandHandler and IQueryHandler), Publish the INotificationHandler<T> ones and CreateStream the IStreamRequestHandler<,> ones; MediatR's Send also runs every pipeline behavior (IPipelineBehavior<,>) the project registers (AddOpenBehavior(typeof(AuditBehavior<,>)), AddBehavior<...>(), typeof in 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, ...), AsyncExecuter given the repository's query, and GetQueryableAsync()/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(...), then await 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.

  • foreach over Chunk(...): one query per batch is the fix, not the problem.

  • for/while/do loops that page (OrderBy and Take), 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​

KeyMeaning
nplusone.query_methodsMethods that run a query on every call, for code the analyzer cannot see into (MyApp.Domain.IOrderRepository.*).
nplusone.ignored_methodsMethods never treated as running a query (cached lookups).
nplusone.iterating_methodsMethods that call their delegate argument once per element.
nplusone.max_call_depthHow 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.