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 (1)

Collapse
 
iqtechsolutions profile image
Ivan Rossouw

Two production nuances worth adding: Skip/Take becomes progressively more expensive on deep pages because the database still walks the skipped rows. With a stable sort, keyset pagination—such as CreatedAt > lastCreatedAt, with Id as a tie-breaker and a matching composite index—usually scales better.

I’d also treat compiled queries and AsSplitQuery as measured options rather than defaults. EF already caches query shapes, while split queries exchange cartesian duplication for additional round trips and a possible consistency window. Capturing ToQueryString(), the actual database execution plan, returned row counts, and p95 latency before and after each change usually identifies the real bottleneck. Which provider, dataset size, and benchmark produced the quoted 25–50% upgrade improvement?