Entity Framework Core is the most popular ORM in .NET — and one
of the most misused. I have reviewed dozens of ASP.NET Core backends
where EF Core was the biggest performance bottleneck. Benchmarks show
25–50% faster query execution just from upgrading to .NET 10.
Here are 12 tips that will make a real difference in production.
1. Use AsNoTracking for Read-Only Queries
By default EF Core tracks every entity it loads. For read-only
queries this is completely wasted work.
// ❌ Bad — tracking enabled, wastes memory
var products = await context.Products
.Where(p => p.IsActive)
.ToListAsync();
// ✅ Good — no tracking, faster and less memory
var products = await context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
Performance impact: 20–40% faster on large result sets.
2. Bulk Updates with ExecuteUpdateAsync
The old pattern generates one SQL UPDATE per entity. For 10,000
rows that is 10,000 round trips.
// ❌ Bad — 10,000 UPDATE statements
var users = await context.Users
.Where(u => u.LastLogin < DateTime.UtcNow.AddYears(-2))
.ToListAsync();
foreach (var user in users) user.IsActive = false;
await context.SaveChangesAsync();
// ✅ Good — ONE SQL UPDATE statement
await context.Users
.Where(u => u.LastLogin < DateTime.UtcNow.AddYears(-2))
.ExecuteUpdateAsync(u =>
u.SetProperty(x => x.IsActive, false));
3. Compiled Queries for Hot Paths
Every LINQ query goes through expression tree compilation.
Pre-compile it once and reuse on every call.
public static readonly Func<AppDbContext, Guid, Task<Product?>>
GetProductById =
EF.CompileAsyncQuery((AppDbContext ctx, Guid id) =>
ctx.Products
.AsNoTracking()
.FirstOrDefault(p => p.Id == id));
4. Select Only What You Need
Loading a full entity when you need two fields wastes bandwidth
and CPU.
// ❌ Bad — SELECT * FROM Products
var products = await context.Products
.Where(p => p.IsActive)
.ToListAsync();
// ✅ Good — SELECT Id, Name, Price FROM Products
var products = await context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.Select(p => new ProductSummaryDto(p.Id, p.Name, p.Price))
.ToListAsync();
5. Fix the N+1 Query Problem
Loading 500 orders then lazy-loading each customer = 501 queries.
// ❌ Bad — 501 queries for 500 orders
var orders = await context.Orders.ToListAsync();
foreach (var order in orders)
Console.WriteLine(order.Customer.Name); // lazy load!
// ✅ Good — 1 query with JOIN
var orders = await context.Orders
.AsNoTracking()
.Include(o => o.Customer)
.ToListAsync();
6. Always Paginate — Never Load All Rows
var page = await context.Products
.AsNoTracking()
.OrderBy(p => p.CreatedAt)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.Select(p => new ProductSummaryDto(p.Id, p.Name, p.Price))
.ToListAsync();
7. LeftJoin — New First-Class Operator in EF Core 10
No more GroupJoin + SelectMany workaround.
// ✅ Clean EF Core 10 LeftJoin
var results = context.Products
.AsNoTracking()
.LeftJoin(
context.Orders,
p => p.Id,
o => o.ProductId,
(p, o) => new
{
ProductName = p.Name,
OrderTotal = o != null ? o.Total : (decimal?)0
});
8. AsSplitQuery for Collection Includes
Multiple includes on collections causes cartesian explosion —
50 items × 3 payments = 150 rows instead of 53.
var orders = await context.Orders
.AsNoTracking()
.Include(o => o.OrderItems)
.Include(o => o.Payments)
.AsSplitQuery() // 3 clean SQL queries instead of 1 exploded JOIN
.ToListAsync();
9. Raw SQL for Complex Reports
var report = await context.Database
.SqlQuery<MonthlySalesDto>(
$"EXEC GetMonthlySales {startDate}, {endDate}")
.ToListAsync();
10. Add Database Indexes
All EF Core optimisations will not help if your table has no indexes.
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Product>(e =>
{
e.HasIndex(p => p.Code).IsUnique();
e.HasIndex(p => new { p.CategoryId, p.IsActive, p.Price });
});
}
11. Native JSON Columns — New in EF Core 10
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Blog>()
.OwnsOne(b => b.Details, d => d.ToJson());
}
12. Vector Search for AI Workloads — New in EF Core 10
var similarPosts = await context.BlogPosts
.AsNoTracking()
.OrderBy(b => EF.Functions.VectorDistance(
"cosine", b.Embedding, queryEmbedding))
.Take(5)
.ToListAsync();
Quick Reference Checklist
- ✅ AsNoTracking() on all read-only queries
- ✅ ExecuteUpdateAsync / ExecuteDeleteAsync for bulk ops
- ✅ Compiled queries for hot paths
- ✅ Project to DTOs with Select()
- ✅ Fix N+1 with Include() or projections
- ✅ Always paginate
- ✅ AsSplitQuery() for collection includes
- ✅ LeftJoin/RightJoin operators (EF Core 10)
- ✅ Index every WHERE, ORDER BY, JOIN column
- ✅ Upgrade to .NET 10 for free 25–50% baseline gain
Read the full guide with complete code examples on my blog →
https://codewithadnanshaukat.blogspot.com/2026/05/ef-core-10-performance-tips-complete-guide.html
Top comments (0)