DEV Community

Adnan Shaukat
Adnan Shaukat

Posted on • Originally published at codewithadnanshaukat.blogspot.com

EF Core 10 Performance Tips — 12 Ways to Blazing Fast Queries

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();
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
        });
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

9. Raw SQL for Complex Reports

var report = await context.Database
    .SqlQuery<MonthlySalesDto>(
        $"EXEC GetMonthlySales {startDate}, {endDate}")
    .ToListAsync();
Enter fullscreen mode Exit fullscreen mode

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 });
    });
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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)