.NET  

What are the Best Practices for EF Core Performance Optimization?

โšก Introduction to EF Core Performance Optimization

Entity Framework Core (EF Core) is a powerful and flexible ORM for .NET, but if not used carefully, it can introduce performance bottlenecks. We'll explore best practices to improve EF Core performance, reduce query latency, and ensure efficient memory usage, all with practical C# examples.

๐Ÿš€ 1. Use AsNoTracking() for Read-Only Queries

EF Core tracks entities by default, which is unnecessary when you're only reading data. Disabling tracking speeds up queries and reduces memory usage.

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

โœ… When to Use

  • In read-only queries
  • In reporting or dashboard views
  • When tracking adds unnecessary overhead

๐Ÿง  2. Project Only Required Columns with Select

Avoid fetching entire entities when you only need a few fields.

var names = context.Users
    .Select(u => u.Name)
    .ToList();

โœ… Benefits

  • Reduces payload size
  • Improves query performance
  • Avoids unnecessary joins and data hydration

๐Ÿ”„ 3. Avoid the N+1 Query Problem

The N+1 issue happens when lazy loading causes multiple queries unintentionally.

โŒ Bad

var orders = context.Orders.ToList();

foreach (var order in orders)
{
    Console.WriteLine(order.Customer.Name); // Triggers a query each time
}

โœ… Good (Eager Loading)

var orders = context.Orders
    .Include(o => o.Customer)
    .ToList();

๐Ÿ“ฆ 4. Use Batching with AddRange / UpdateRange

Instead of calling SaveChanges() repeatedly, batch your inserts or updates:

context.Users.AddRange(user1, user2, user3);
await context.SaveChangesAsync();

๐Ÿ”ฅ For large batches

Use libraries like EFCore.BulkExtensions to dramatically reduce insert/update time.

๐Ÿ“Œ 5. Prefer FirstOrDefault() or Any() Instead of Count()

Count() checks all matching rows. Use Any() if you just need to check existence.

// Slower
bool hasUsers = context.Users.Count() > 0;

// Faster
bool hasUsers = context.Users.Any();

๐Ÿ› ๏ธ 6. Use Indexes on Frequently Queried Columns

EF Core doesn’t manage database indexes, you must define them manually or in your migrations:

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

โœ… When to Use

  • On columns used in WHERE, JOIN, or ORDER BY
  • For foreign keys or commonly filtered fields

๐Ÿ” 7. Use ToList() or ToArray() at the Right Time

Don’t call ToList() too early if you can keep the query in-memory longer to add more filters or projections.

โœ… Example:

var recentUsers = context.Users
    .Where(u => u.CreatedAt > DateTime.UtcNow.AddDays(-7))
    .Select(u => u.Name)
    .ToList(); // Only at the end!

๐Ÿงฎ 8. Use Compiled Queries for High-Throughput Scenarios

Compiled queries cache the query translation step, reducing overhead on repeated execution.

private static readonly Func<AppDbContext, int, User?> _getUserById =
    EF.CompileQuery((AppDbContext context, int id) =>
        context.Users.FirstOrDefault(u => u.Id == id));

// Usage
var user = _getUserById(context, 5);

๐Ÿ“ 9. Cache Frequently Used Data

Don’t hit the database for static data. Use in-memory caching:

var roles = memoryCache.GetOrCreate("roles", entry =>
{
    entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
    return context.Roles.AsNoTracking().ToList();
});

Or use a distributed cache for multiple server environments.

๐Ÿงต 10. Use async Queries for Scalability

Always prefer async versions of EF Core methods:

var user = await context.Users
    .FirstOrDefaultAsync(u => u.Email == "[email protected]");

โœ… Benefits

  • Prevents thread blocking
  • Improves scalability in web apps

๐Ÿงน 11. Avoid Unnecessary Change Tracking and Detach When Needed

If you need to load and discard data without tracking:

context.Entry(entity).State = EntityState.Detached;

Useful in long-running contexts or background jobs to avoid memory bloat.

๐Ÿ“Š 12. Profile Queries and Use Logging

Use tools like:

  • EF Core logging (ILogger)
  • MiniProfiler
  • SQL Server Profiler
  • Visual Studio Diagnostic Tools

To inspect:

  • Query duration
  • Redundant database calls
  • Unoptimized SQL generated by LINQ

๐Ÿงฑ 13. Use Raw SQL for Complex Queries (When Necessary)

When LINQ becomes inefficient or unreadable:

var users = context.Users
    .FromSqlRaw("SELECT * FROM Users WHERE IsActive = 1")
    .AsNoTracking()
    .ToList();

โš ๏ธ Be cautious with SQL injection, parameterize your queries!

๐Ÿงฐ 14. Use Route-Level Filtering in APIs

Instead of filtering in-memory or in the controller, do it in the query itself:

// Good
var result = await context.Users
    .Where(u => u.Role == "Admin")
    .ToListAsync();

๐Ÿ“ Conclusion

EF Core is powerful, but performance can suffer without thoughtful design. Apply these best practices to:

  • Reduce latency
  • Minimize memory usage
  • Avoid costly database operations
  • Scale efficiently

โšก A fast app is a happy app, optimize early, monitor often!