Optimizing EF Core Performance
Essential tips for improving Entity Framework Core performance on Plesk Windows hosting.
1. Use Async Methods
Always use async for database operations:
// Good
var products = await _context.Products.ToListAsync();
await _context.SaveChangesAsync();
// Avoid
var products = _context.Products.ToList();
_context.SaveChanges();
2. Avoid N+1 Queries
Use eager loading with Include():
// Bad: N+1 queries
var orders = await _context.Orders.ToListAsync();
foreach (var order in orders)
{
var items = order.Items; // Additional query for each order!
}
// Good: Single query with Include
var orders = await _context.Orders
.Include(o => o.Items)
.Include(o => o.Customer)
.ToListAsync();
3. Use Projections (Select)
Only retrieve needed columns:
// Bad: Retrieves all columns
var products = await _context.Products.ToListAsync();
// Good: Only needed data
var products = await _context.Products
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
})
.ToListAsync();
4. Use No-Tracking for Read-Only Queries
// Single query
var products = await _context.Products
.AsNoTracking()
.ToListAsync();
// Or configure globally for read-heavy apps
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
5. Implement Pagination
public async Task<PagedResult<Product>> GetProductsAsync(int page, int pageSize)
{
var query = _context.Products.AsNoTracking();
var totalItems = await query.CountAsync();
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<Product>
{
Items = items,
TotalCount = totalItems,
Page = page,
PageSize = pageSize
};
}
6. Use Compiled Queries for Hot Paths
private static readonly Func<AppDbContext, int, Task<Product?>> _getProductById =
EF.CompileAsyncQuery((AppDbContext context, int id) =>
context.Products.FirstOrDefault(p => p.Id == id));
public async Task<Product?> GetByIdAsync(int id)
{
return await _getProductById(_context, id);
}
7. Batch Operations
// Bad: Multiple round trips
foreach (var product in products)
{
_context.Products.Add(product);
await _context.SaveChangesAsync(); // Save each time!
}
// Good: Single round trip
_context.Products.AddRange(products);
await _context.SaveChangesAsync();
8. Index Your Columns
Add indexes for frequently queried columns:
public class Product
{
public int Id { get; set; }
[MaxLength(100)]
public string Name { get; set; }
public string Sku { get; set; } // Frequently searched
}
// In DbContext OnModelCreating
modelBuilder.Entity<Product>()
.HasIndex(p => p.Sku)
.IsUnique();
9. Use Split Queries for Complex Includes
var orders = await _context.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.AsSplitQuery() // Separate queries to avoid cartesian explosion
.ToListAsync();
Performance Checklist
- ☐ Use async methods throughout
- ☐ Use Include() to prevent N+1
- ☐ Use Select() projections
- ☐ Use AsNoTracking() for reads
- ☐ Implement pagination
- ☐ Add database indexes
- ☐ Batch insert/update operations
- ☐ Monitor with logging (in development)