โก 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!