如何在 EF Core 中捕获 N+1 查询,防止它们进入生产环境
Source: Dev.to

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
Why this matters? This isn’t about premature optimization. It’s about catching issues like:
- N+1 queries
- unnecessary round‑trips
- query explosions after refactors
These problems don’t show up as failing tests — they show up as slow endpoints in production. Detecting them in EF Core tests often involves writing a custom DbCommandInterceptor, wiring it into the test host, collecting executed SQL, and 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 provides with assertNumQueries. So I wrapped the interceptor pattern into a small helper.
await using var guard = factory.TrackQueries();
var client = guard.CreateClient();
await client.GetAsync("/api/orders");
guard.AssertCount(atMost: 3);That’s it.
- No manual interceptor wiring
- No log parsing
- No test boilerplate
It simply 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. 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—bugs that:
- don’t break functionality
- don’t fail tests
- but hurt you in production
The helper is very small (~200 lines) and MIT licensed.
I’d be interested to know how others are solving this — or if you’re not testing for it at all.