Entity Framework Core - Deep Performance Optimization Guide

This article is a deep dive into optimizing performance in Entity Framework Core applications. Whether you're building APIs, microservices, or data-heavy platforms, these techniques will help you improve speed, reduce memory usage, and make your EF Core queries production-grade.

1. Use AsNoTracking for Read-Only Queries

By default, EF Core tracks changes on every entity. If you're only reading data and not updating it, disable tracking to improve performance.

var users = await context.Users
    .AsNoTracking()
    .ToListAsync();
  • Why: This reduces memory overhead and improves speed, especially when returning large result sets.
  • Tip: Use AsNoTrackingWithIdentityResolution if you want no tracking but still need navigation property resolution.

2. Avoid N+1 Query Problem

Lazy loading can result in multiple round-trips to the DB. Use eager loading instead:

var blogs = context.Blogs
    .Include(b => b.Posts)
    .Include(b => b.Owner)
    .ToList();
  • Why: Prevents one query per navigation property, which destroys performance under load.
  • Tool: EF Core logging or MiniProfiler can help you detect N+1 issues.

3. Project Only What You Need

Don’t fetch entire entities if you only need a few fields. Use Select to create lightweight DTOs.

var postSummaries = context.Posts
    .Select(p => new PostSummaryDto 
    { 
        Id = p.Id, 
        Title = p.Title 
    })
    .ToList();

Why: Reduces data transferred from DB and memory used in your app.

4. Use Compiled Queries

EF Core allows precompiling LINQ queries, reducing runtime overhead of query parsing.

private static readonly Func<AppDbContext, int, Task<User>> GetUserByIdQuery =
    EF.CompileAsyncQuery(
        (AppDbContext context, int id) =>
            context.Users.FirstOrDefault(u => u.Id == id)
    );

When: Use for high-frequency queries to minimize overhead.

5. Use EFCore.BulkExtensions

EF Core is not optimized for bulk insert/update/delete. Use 3rd-party libraries.

context.BulkInsert(listOfEntities);
context.BulkUpdate(listOfEntities);
context.BulkDelete(listOfEntities);
  • Why: Executes bulk operations in a single command instead of looping with SaveChanges().
  • Tool: EFCore.BulkExtensions, Z.EntityFramework.Plus

6. Reduce Number of Database Calls

  • Use.Any() instead of.Count() > 0.
  • Use.FirstOrDefault() instead of.Where().First().
  • Batch updates/deletes when possible.
bool exists = await context.Users
    .AnyAsync(u => u.Email == email);

7. Use Indexes in SQL for Fast Filtering

Ensure frequently queried columns (e.g., foreign keys, email, dates) are indexed.

modelBuilder.Entity<User>()
    .HasIndex(u => u.Email)
    .IsUnique();

Why: Indexes improve query performance dramatically, especially on large tables.

8. Avoid Lazy Loading in APIs

Turn off lazy loading in high-performance scenarios:

builder.Services.AddDbContext(options =>
    options.UseLazyLoadingProxies(false));

Tip: Prefer.Include() or manual projection instead of lazy loading in Web APIs.

9. Analyze Generated SQL with ToQueryString()

var query = context.Users
    .Where(u => u.IsActive);

Console.WriteLine(query.ToQueryString());

Why: Useful for debugging query issues, performance bottlenecks, and ensuring indexes are used.

10. Use Caching Where Appropriate

  • Use MemoryCache for in-process caching.
  • Use Redis or other distributed cache for multi-instance apps.
  • Use EFCoreSecondLevelCacheInterceptor for auto-caching EF queries.
var user = await cache.GetOrCreateAsync("user_1", entry =>
{
    entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
    return context.Users.FindAsync(1);
});

11. EF Core Query Pipeline Internals

EF Core translates LINQ into expression trees, which are parsed into SQL commands. Each LINQ method builds part of the expression tree. Understanding this helps avoid performance anti-patterns.

  • Use filter conditions early (e.g., before Include()).
  • Query splitting: Be cautious with deeply nested includes that cause Cartesian explosion.

12. DbContext Pooling

Use DbContext pooling to reuse context instances in high-load scenarios, reducing GC pressure.

builder.Services.AddDbContextPool(options =>
{
    options.UseSqlServer(config.GetConnectionString("Default"));
});

When: Web applications or APIs handle many requests per second.

13. EF Core vs Dapper Performance

EF Core is powerful and feature-rich, but Dapper excels in raw performance. Use Dapper for:

  • High-volume data reads.
  • Reporting, dashboards, or analytics queries.
  • Scenarios where control over SQL is important.
var result = connection.Query(
    "SELECT Id, Title FROM Posts WHERE IsPublished = 1"
);

Tradeoff: Dapper is fast but doesn't track entities or manage relationships.

14. Benchmarking with BenchmarkDotNet

Use BenchmarkDotNet to measure query and update performance across scenarios.

[MemoryDiagnoser]
public class EfBenchmark
{
    private AppDbContext _context;
    [GlobalSetup]
    public void Setup()
    {
        var options = new DbContextOptionsBuilder()
            .UseSqlServer("your_connection")
            .Options;

        _context = new AppDbContext(options);
    }
    [Benchmark]
    public async Task NoTrackingQuery() =>
        await _context.Users.AsNoTracking().ToListAsync();
}

Why: Get actual timings, memory allocations, and comparison metrics.

15. SQL View and Function Mapping

Map to existing SQL views or functions when LINQ is inefficient for complex joins.

modelBuilder.Entity()
    .ToView("View_ReportSummaries")
    .HasNoKey();

Why: Push heavy logic to SQL layer for performance.

16. EFCoreSecondLevelCacheInterceptor

Add caching to queries automatically using this library.

services.AddEFSecondLevelCache(options =>
{
    options
        .UseMemoryCacheProvider()
        .DisableLogging(true);
});

Then, annotate queries.

var users = context.Users
                   .Cacheable()
                   .ToList();

Benefit: Transparent caching of expensive queries across requests.

Up Next
    Ebook Download
    View all
    Learn
    View all