Databases

EF Core Performance Optimization: Advanced Techniques for High-Performance Applications

Master Entity Framework Core performance optimization with advanced techniques including query optimization, change tracking, compiled queries, and connection pooling.

I
Isaiah Clifford Opoku
Dec 12, 20245 min read
#ef-core#performance#dotnet
5 min
reading time
EF Core Performance Optimization: Advanced Techniques for High-Performance Applications

Entity Framework Core performance can make or break your application. Let's explore advanced optimization techniques that can dramatically improve your application's speed and scalability.

Query Optimization Strategies

Projection and Select Specific Fields

Avoid loading unnecessary data by projecting only what you need:

csharp
1// ❌ Bad: Loads entire entity 2var users = await context.Users 3 .Where(u => u.IsActive) 4 .ToListAsync(); 5 6// ✅ Good: Project only needed fields 7var userSummaries = await context.Users 8 .Where(u => u.IsActive) 9 .Select(u => new UserSummaryDto 10 { 11 Id = u.Id, 12 Name = u.Name, 13 Email = u.Email 14 }) 15 .ToListAsync();

Split Queries for Multiple Includes

Prevent Cartesian explosion with split queries:

csharp
1// ❌ Bad: Creates Cartesian product 2var orders = await context.Orders 3 .Include(o => o.OrderItems) 4 .Include(o => o.Customer) 5 .ToListAsync(); 6 7// ✅ Good: Split into multiple queries 8var orders = await context.Orders 9 .AsSplitQuery() 10 .Include(o => o.OrderItems) 11 .Include(o => o.Customer) 12 .ToListAsync();

Raw SQL for Complex Queries

Use raw SQL for complex scenarios where LINQ is inefficient:

csharp
1public class ProductRepository 2{ 3 private readonly ApplicationDbContext _context; 4 5 public async Task<IEnumerable<ProductSalesReport>> GetTopSellingProductsAsync( 6 DateTime startDate, DateTime endDate, int count) 7 { 8 return await _context.Database 9 .SqlQueryRaw<ProductSalesReport>(@" 10 SELECT TOP (@count) 11 p.Id, 12 p.Name, 13 SUM(oi.Quantity) as TotalSold, 14 SUM(oi.Quantity * oi.UnitPrice) as TotalRevenue 15 FROM Products p 16 INNER JOIN OrderItems oi ON p.Id = oi.ProductId 17 INNER JOIN Orders o ON oi.OrderId = o.Id 18 WHERE o.OrderDate BETWEEN @startDate AND @endDate 19 GROUP BY p.Id, p.Name 20 ORDER BY TotalRevenue DESC", 21 new SqlParameter("count", count), 22 new SqlParameter("startDate", startDate), 23 new SqlParameter("endDate", endDate)) 24 .ToListAsync(); 25 } 26}

Change Tracking Optimization

No-Tracking Queries for Read-Only Operations

Disable change tracking for read-only scenarios:

csharp
1// ❌ With change tracking (slower) 2var products = await context.Products 3 .Where(p => p.CategoryId == categoryId) 4 .ToListAsync(); 5 6// ✅ Without change tracking (faster) 7var products = await context.Products 8 .AsNoTracking() 9 .Where(p => p.CategoryId == categoryId) 10 .ToListAsync(); 11 12// Global no-tracking for read-only contexts 13public class ReadOnlyContext : ApplicationDbContext 14{ 15 public ReadOnlyContext(DbContextOptions<ReadOnlyContext> options) 16 : base(options) 17 { 18 ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; 19 } 20}

Identity Resolution Optimization

Control identity resolution for better performance:

csharp
1// ❌ Default: Identity resolution enabled 2var orders = await context.Orders 3 .AsNoTracking() 4 .Include(o => o.Customer) 5 .ToListAsync(); 6 7// ✅ Disable identity resolution for better performance 8var orders = await context.Orders 9 .AsNoTracking() 10 .AsNoTrackingWithIdentityResolution() 11 .Include(o => o.Customer) 12 .ToListAsync();

Compiled Queries

Pre-compile frequently used queries for better performance:

csharp
1public static class CompiledQueries 2{ 3 public static readonly Func<ApplicationDbContext, int, IAsyncEnumerable<Product>> 4 GetProductsByCategory = EF.CompileAsyncQuery( 5 (ApplicationDbContext context, int categoryId) => 6 context.Products 7 .AsNoTracking() 8 .Where(p => p.CategoryId == categoryId) 9 .OrderBy(p => p.Name)); 10 11 public static readonly Func<ApplicationDbContext, string, Task<User?>> 12 GetUserByEmail = EF.CompileAsyncQuery( 13 (ApplicationDbContext context, string email) => 14 context.Users 15 .AsNoTracking() 16 .FirstOrDefault(u => u.Email == email)); 17} 18 19// Usage 20public class ProductService 21{ 22 private readonly ApplicationDbContext _context; 23 24 public async Task<IEnumerable<Product>> GetProductsByCategoryAsync(int categoryId) 25 { 26 var products = new List<Product>(); 27 await foreach (var product in CompiledQueries.GetProductsByCategory(_context, categoryId)) 28 { 29 products.Add(product); 30 } 31 return products; 32 } 33}

Connection and DbContext Management

Connection Pooling

Configure connection pooling for better resource utilization:

csharp
1// Startup.cs or Program.cs 2services.AddDbContextPool<ApplicationDbContext>(options => 3{ 4 options.UseSqlServer(connectionString, sqlOptions => 5 { 6 sqlOptions.CommandTimeout(30); 7 sqlOptions.EnableRetryOnFailure( 8 maxRetryCount: 3, 9 maxRetryDelay: TimeSpan.FromSeconds(30), 10 errorNumbersToAdd: null); 11 }); 12}, poolSize: 128); // Configure pool size based on your needs

DbContext Factory Pattern

Use DbContext factory for better lifecycle management:

csharp
1public interface IDbContextFactory<TContext> where TContext : DbContext 2{ 3 TContext CreateDbContext(); 4} 5 6public class ApplicationDbContextFactory : IDbContextFactory<ApplicationDbContext> 7{ 8 private readonly DbContextOptions<ApplicationDbContext> _options; 9 10 public ApplicationDbContextFactory(DbContextOptions<ApplicationDbContext> options) 11 { 12 _options = options; 13 } 14 15 public ApplicationDbContext CreateDbContext() 16 { 17 return new ApplicationDbContext(_options); 18 } 19} 20 21// Usage in service 22public class OrderService 23{ 24 private readonly IDbContextFactory<ApplicationDbContext> _contextFactory; 25 26 public async Task<Order> ProcessOrderAsync(CreateOrderRequest request) 27 { 28 using var context = _contextFactory.CreateDbContext(); 29 30 var order = new Order 31 { 32 CustomerId = request.CustomerId, 33 OrderDate = DateTime.UtcNow, 34 OrderItems = request.Items.Select(i => new OrderItem 35 { 36 ProductId = i.ProductId, 37 Quantity = i.Quantity, 38 UnitPrice = i.UnitPrice 39 }).ToList() 40 }; 41 42 context.Orders.Add(order); 43 await context.SaveChangesAsync(); 44 return order; 45 } 46}

Bulk Operations

Bulk Insert with EF Core Extensions

Handle large datasets efficiently:

csharp
1public class BulkDataService 2{ 3 private readonly ApplicationDbContext _context; 4 5 public async Task BulkInsertProductsAsync(IEnumerable<Product> products) 6 { 7 // Using EFCore.BulkExtensions 8 await _context.BulkInsertAsync(products); 9 } 10 11 public async Task BulkUpdatePricesAsync(IEnumerable<ProductPriceUpdate> updates) 12 { 13 var products = await _context.Products 14 .Where(p => updates.Select(u => u.ProductId).Contains(p.Id)) 15 .ToListAsync(); 16 17 foreach (var product in products) 18 { 19 var update = updates.First(u => u.ProductId == product.Id); 20 product.Price = update.NewPrice; 21 product.LastModified = DateTime.UtcNow; 22 } 23 24 await _context.BulkUpdateAsync(products); 25 } 26}

Caching Strategies

Second-Level Caching

Implement caching for frequently accessed data:

csharp
1public class CachedProductService 2{ 3 private readonly ApplicationDbContext _context; 4 private readonly IMemoryCache _cache; 5 private readonly ILogger<CachedProductService> _logger; 6 7 public async Task<Product?> GetProductByIdAsync(int id) 8 { 9 var cacheKey = $"product_{id}"; 10 11 if (_cache.TryGetValue(cacheKey, out Product? cachedProduct)) 12 { 13 _logger.LogInformation("Product {ProductId} retrieved from cache", id); 14 return cachedProduct; 15 } 16 17 var product = await _context.Products 18 .AsNoTracking() 19 .Include(p => p.Category) 20 .FirstOrDefaultAsync(p => p.Id == id); 21 22 if (product != null) 23 { 24 _cache.Set(cacheKey, product, TimeSpan.FromMinutes(30)); 25 _logger.LogInformation("Product {ProductId} cached for 30 minutes", id); 26 } 27 28 return product; 29 } 30 31 public async Task InvalidateProductCacheAsync(int productId) 32 { 33 var cacheKey = $"product_{productId}"; 34 _cache.Remove(cacheKey); 35 _logger.LogInformation("Cache invalidated for product {ProductId}", productId); 36 } 37}

Performance Monitoring

Query Logging and Analysis

Monitor and analyze query performance:

csharp
1public class QueryPerformanceInterceptor : DbCommandInterceptor 2{ 3 private readonly ILogger<QueryPerformanceInterceptor> _logger; 4 5 public QueryPerformanceInterceptor(ILogger<QueryPerformanceInterceptor> logger) 6 { 7 _logger = logger; 8 } 9 10 public override ValueTask<DbDataReader> ReaderExecutedAsync( 11 DbCommand command, 12 CommandExecutedEventData eventData, 13 DbDataReader result, 14 CancellationToken cancellationToken = default) 15 { 16 var duration = eventData.Duration.TotalMilliseconds; 17 18 if (duration > 1000) // Log queries taking more than 1 second 19 { 20 _logger.LogWarning( 21 "Slow query detected: {Duration}ms - {CommandText}", 22 duration, 23 command.CommandText); 24 } 25 26 return base.ReaderExecutedAsync(command, eventData, result, cancellationToken); 27 } 28} 29 30// Register interceptor 31services.AddDbContext<ApplicationDbContext>(options => 32{ 33 options.UseSqlServer(connectionString) 34 .AddInterceptors(serviceProvider.GetRequiredService<QueryPerformanceInterceptor>()); 35});

Conclusion

EF Core performance optimization requires a multi-faceted approach covering query optimization, change tracking management, connection pooling, and caching strategies. By implementing these advanced techniques, you can build high-performance applications that scale effectively.

Remember to always measure performance before and after optimizations, and use tools like Application Insights or custom logging to monitor your application's database performance in production.

Related Technologies

Technologies and tools featured in this article

#

ef-core

#

performance

#

dotnet

#

database

#

optimization

5
Technologies
Databases
Category
5m
Read Time

Continue Reading

Discover more insights and technical articles from our collection

Back to all posts

Found this helpful?

I'm posting .NET and software engineering content on multiple Social Media platforms.

If you enjoyed this article and want to see more content like this, consider subscribing to my newsletter or following me on social media for the latest updates.