Skip to main content

In CI

N+1 belongs in the pull request, not in the incident review. Both layers run in a normal CI build.

Gate on the analyzer​

Raise the rules you want enforced to errors in .editorconfig (see Getting started): the build fails on a new N+1 like on any compiler error. Nothing else is needed: the analyzer runs in dotnet build.

Adopting it on an existing code base with many warnings? Raise the rules to errors and suppress the existing findings in a GlobalSuppressions.cs file (the IDE's "Suppress in suppression file" action writes it), then pay them down: new code is held to the rule from day one.

A .globalconfig with nplusone.query_methods for the clients your code calls (another service, a package without summaries) lets the analyzer report those too.

Gate on the guard​

Integration tests that run your services against a real database (SQLite, a container, Testcontainers) are where the guard pays off. Attach it once, in the fixture that builds the context, and assert in each test:

public sealed class DatabaseFixture
{
public ShopContext CreateContext()
=> new(new DbContextOptionsBuilder<ShopContext>()
.UseNpgsql(ConnectionString)
.UseNPlusOneGuard()
.Options);
}

[Fact]
public async Task Checkout_does_not_query_per_line()
{
await using var db = _fixture.CreateContext();
using var guard = NPlusOneGuard.Begin(o => o.IncludeWrites = true);

await new CheckoutService(db).CheckoutAsync(cartId);

guard.AssertNoNPlusOne();
}

For tests that go through HTTP (WebApplicationFactory), add [NPlusOneAssert] to the controllers or .WithNPlusOneAssert() to the endpoint groups under test: the request fails with the report when an N+1 happens.

GitHub Actions​

Nothing specific: dotnet build and dotnet test run both layers. The analyzer's warnings appear as annotations on the changed lines when you build with the default logger.

- run: dotnet build --configuration Release
- run: dotnet test --configuration Release --no-build