DEV Community

Cover image for How to catch N+1 queries in EF Core before they hit production
Dmytro
Dmytro

Posted on

How to catch N+1 queries in EF Core before they hit production

Your API can stay functionally correct while quietly getting slower.

  • No tests fail.
  • No alerts fire.
  • Everything “works”.

And then one day you realize an endpoint that used to execute 2 queries is now doing 15.

In EF Core, it’s surprisingly easy to introduce query regressions:

  • a small refactor changes how a projection is built
  • a navigation property is accessed differently
  • part of the query gets materialized too early
  • includes / relationships evolve over time

None of this breaks correctness. Your integration tests still pass because:

  • the response is correct
  • the database state is correct

But the query shape has changed. And that’s the part we usually don’t test.

Why this matters ? This isn’t about premature optimization. It’s about catching issues like:

  • N+1 queries
  • unnecessary roundtrips
  • query explosions after refactors

These problems don’t show up as failing tests — they show up as slow endpoints in production. If you want to detect this in EF Core tests, you typically end up doing something like:

  • writing a custom DbCommandInterceptor
  • wiring it into your test host
  • collecting executed SQL
  • asserting on the count It works, but it’s repetitive and low-level. Most teams end up copy-pasting some version of this.

A simpler approach

I wanted something closer to what Django has with assertNumQueries. So I wrapped the interceptor pattern into a small helper.

Example:

await using var guard = factory.TrackQueries<Program, AppDbContext>();
var client = guard.CreateClient();

await client.GetAsync("/api/orders");

guard.AssertCount(atMost: 3);
Enter fullscreen mode Exit fullscreen mode

That’s it.

  • no manual interceptor wiring
  • no log parsing
  • no test boilerplate It just tracks the number of SQL queries executed during a request.

When to use this

I don’t think this belongs in every test. For most endpoints, correctness is enough. But it’s useful for:

  • list endpoints
  • dashboards / aggregates
  • endpoints with multiple relationships
  • any “hot path” where query shape matters Basically, places where going from 2 queries → 10 queries is a real problem.

The idea

This isn’t about replacing integration tests. It’s about adding a lightweight guard against performance regressions. Because those are the bugs that:

  • don’t break functionality
  • don’t fail tests
  • but hurt you in production

If you’re curious,I put the helper here:

https://github.com/KiwiDevelopment/KiwiQuery

It’s very small (~200 lines), MIT licensed.
Would be interested to know how others are solving this — or if you’re not testing for it at all.

Top comments (0)