Diagnosing and Fixing EF Core Lazy‑Loading N+1 Query Problems in ASP.NET Core
Identify EF Core lazy‑loading N+1 query symptoms, verify proxy configuration, add missing Includes, and apply fallback strategies when performance does not improve.
16 Nov 2025, 16:19 UTC

Recognizable Condition
When an ASP.NET Core endpoint accesses a navigation property (e.g., order.Customer) inside a loop, response times rise and the database shows many identical SELECT statements for the same related entity.
Cause / Diagnostic Overview
| Symptom | Likely Cause |
|---|---|
| Repeated SELECTs for navigation property | Lazy loading enabled but missing Include/ThenInclude |
| Entity class sealed or navigation not virtual | Proxy cannot be created → lazy loading falls back to explicit load or throws |
Ordered Checks
- Enable EF Core logging at
Informationlevel inappsettings.Development.jsonor viaDbContextOptionsBuilder.LogTo. - Reproduce the request that triggers the loop and examine the log for repeated
SELECTstatements targeting the same table. - Verify that the entity type is not
sealedand that each navigation property is markedvirtual(required for proxy generation). - Confirm that
UseLazyLoadingProxiesis present in theDbContextOptionsregistration. - Check the LINQ query that loads the root entities for missing
IncludeorThenIncludecalls.
Fixes Tied to Findings
- Add missing eager loading: Modify the query to include the navigation, e.g.
var orders = ctx.Orders.Include(o => o.Customer).ToList(); - Disable lazy loading if not needed: Remove
UseLazyLoadingProxiesand use explicit loading or projection. Example:var orders = ctx.Orders.Select(o => new { o.Id, CustomerName = o.Customer.Name }).ToList(); - Enable proxy generation: Ensure the entity class is
publicand not sealed, and navigation properties arevirtual. If you cannot change the class, consider usingEF Core 5+UseLazyLoadingProxieswithILazyLoaderinjection instead of proxies.
Escalation Criteria
If after adding the appropriate Include the log still shows extra SELECTs:
- Consider rewriting the query to use projection (
Select) to fetch only needed columns. - Enable
AsNoTrackingfor read‑only scenarios to reduce overhead. - Evaluate batch loading or splitting the query into multiple queries (EF Core 6+
SplitQuery) when the relationship is one‑to‑many and would cause a Cartesian product. - Profile memory usage; excessive
Includecan inflate result sets.
Verification Steps
- Run the application with EF Core logging set to
LogLevel.Informationand confirm the number of SQL statements matches the expected count (e.g., one query for root entities plus one perInclude). - Use a benchmarking tool such as
BenchmarkDotNetto measure average response time before and after the change; look for fewer round‑trips and lower latency. - Inspect the generated SQL via a SQL profiler or by enabling
DbContext.Database.Logto ensure no stray SELECT statements for the navigation property remain.
Limitations and Practical Check
Adding Include may increase the size of the result set; verify that the additional columns do not cause excessive memory consumption by monitoring GC allocations or using a profiler. If the payload grows beyond acceptable limits, switch to a projection that selects only required fields.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.