NPlusOne.EntityFrameworkCore
1.1.0
dotnet add package NPlusOne.EntityFrameworkCore --version 1.1.0
NuGet\Install-Package NPlusOne.EntityFrameworkCore -Version 1.1.0
<PackageReference Include="NPlusOne.EntityFrameworkCore" Version="1.1.0" />
<PackageVersion Include="NPlusOne.EntityFrameworkCore" Version="1.1.0" />
<PackageReference Include="NPlusOne.EntityFrameworkCore" />
paket add NPlusOne.EntityFrameworkCore --version 1.1.0
#r "nuget: NPlusOne.EntityFrameworkCore, 1.1.0"
#:package NPlusOne.EntityFrameworkCore@1.1.0
#addin nuget:?package=NPlusOne.EntityFrameworkCore&version=1.1.0
#tool nuget:?package=NPlusOne.EntityFrameworkCore&version=1.1.0
N+1
Finds N+1 query problems in .NET and EF Core code: at build time with a Roslyn analyzer, and in tests and requests with a runtime guard. Free and open source (MIT).
OrderService.cs(42,31): warning NP0001: 'IOrderRepository.GetByCustomerAsync' runs a query ('ToListAsync' in
OrderRepository.GetByCustomerAsync) once per element of the foreach loop over 'customers'. Load the rows for all
elements up front with one query, Where(o => keys.Contains(o.CustomerId)), and group them with ToLookup(o => o.CustomerId).
Documentation · Which package where · All packages on NuGet · Changelog
Packages
Three packages, one for each place an N+1 can be caught, released together under one version. Use the analyzer, the guard, or both.
| Package | Catches N+1 | Install |
|---|---|---|
| NPlusOne.Analyzers | at build time, in the IDE and in CI: the analyzer, its code fixes and the generator that follows calls across projects (a development dependency) | dotnet add package NPlusOne.Analyzers |
| NPlusOne.EntityFrameworkCore | in tests and any scope: the runtime guard, AssertNoNPlusOne, reports with normalized SQL and call sites, never parameter values |
dotnet add package NPlusOne.EntityFrameworkCore |
| NPlusOne.AspNetCore | in HTTP requests: one guard scope per request, [NPlusOneAssert] for MVC and .WithNPlusOneAssert() for minimal APIs (brings the guard with it) |
dotnet add package NPlusOne.AspNetCore |
Quick start
1. Add the analyzer to every project with EF Core code (EF Core 8, 9 or 10; any SDK since .NET 8):
dotnet add package NPlusOne.Analyzers
2. Build. N+1 queries show up as warnings, in the build and in the IDE, each with the rewrite that removes it:
dotnet build
3. Keep them out. Raise the rules to errors in .editorconfig:
[*.cs]
dotnet_diagnostic.NP0001.severity = error
dotnet_diagnostic.NP0002.severity = error
dotnet_diagnostic.NP0003.severity = error
dotnet_diagnostic.NP0005.severity = error
4. Catch the rest in tests. The runtime guard fails a test when the same query or lazy load repeats:
dotnet add package NPlusOne.EntityFrameworkCore
var options = new DbContextOptionsBuilder<ShopContext>().UseSqlite(connection).UseNPlusOneGuard().Options;
using var guard = NPlusOneGuard.Begin();
await new OrderService(new ShopContext(options)).ListAsync();
guard.AssertNoNPlusOne();
5. Watch requests in development with NPlusOne.AspNetCore: builder.Services.AddNPlusOneGuard() and
app.UseNPlusOneGuard().
The getting started guide walks through each step.
What it finds
N+1 hides in more places than a foreach with a query in it. The analyzer follows round trips through:
| Loops | for, foreach, await foreach, while, do, indexes and deconstructed elements |
| Callbacks | LINQ lambdas, method groups, List.ForEach, Parallel.ForEach, Task.WhenAll(ids.Select(...)), EF Core's ForEachAsync, FluentValidation RuleForEach, client-evaluated projections, your own iterating helpers |
| Calls | helpers, services, interfaces and virtual methods (to their implementations, across projects), extension and generic methods, delegates, compiled queries, getters and indexers, recursion |
| Libraries | MediatR and Mediator (handlers, pipeline behaviors, streams), AutoMapper, Mapster, Mapperly, ABP Framework (repositories, AsyncExecuter, application layers without EF Core) |
| Lazy loading | navigations read per element, checked against the query's Includes and AutoIncludes, also in ToString, Equals, conversions and patterns |
| Endpoints | MVC, minimal APIs, SignalR and serializers given lazily loaded entities, also inside pages and anonymous objects; OData and [JsonIgnore] respected |
| Writes | SaveChanges and transactions per element |
and it stays quiet where a loop is meant to make one round trip per iteration: batches, paging, polling, search loops, cached lookups, and anything EF Core translates into the same SQL statement.
Every one of the 143 shapes in the catalogue is a test that runs on SQLite and counts the SQL: a reported case must send more commands for more rows, a silent one the same number, on EF Core 8, 9 and 10.
Rules
| Rule | Default | |
|---|---|---|
| NP0001 | A query runs once per element (N+1 read) | Warning |
| NP0002 | A lazy-loaded navigation is read once per element (code fix: add the Include) |
Warning |
| NP0003 | SaveChanges runs once per element (code fix: move it after the loop) |
Warning |
| NP0004 | A recursive method runs a query on every call | Warning |
| NP0005 | An endpoint returns entities whose lazy navigations the serializer loads one by one | Warning |
| NP0006 | A data-access abstraction is called once per element and its implementation is not visible | Info |
| NP0007 | A mapper reads lazy navigations of each element by naming convention | Info |
Options (lazy loading, methods the analyzer cannot see into, call depth) are listed in configuration.
How it compares
Measured on the same proven cases, the analyzer finds 260 of 260 N+1 methods with no false warning; LinqContraband,
the static analyzer with the most N+1 rules we found, finds 55. At run time the guard here, EFCore.QueryGuard and DbEye
all see most cases; the guard alone also sees SQL that differs per element, transactions per element, and counts one
batched SaveChanges once. The corpus was written for this tool, so read the
comparison for what each number means.
Contributing
dotnet build and dotnet test (SDK pinned in global.json; the .NET 8, 9 and 10 runtimes run the tests). A missed
N+1 or a false warning starts as a corpus case; see
CONTRIBUTING.md and
how it works.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.0)
-
net8.0
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.10)
-
net9.0
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on NPlusOne.EntityFrameworkCore:
| Package | Downloads |
|---|---|
|
NPlusOne.AspNetCore
ASP.NET Core integration for the N+1 runtime guard: one scope per request, with a middleware and an MVC filter. |
GitHub repositories
This package is not used by any popular GitHub repositories.