Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Build .NET backend services with clean architecture, dependency injection, Entity Framework, and ASP.NET Core patterns.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/ef-core-best-practices.md
1# Entity Framework Core Best Practices23Performance optimization and best practices for EF Core in production applications.45## Query Optimization67### 1. Use AsNoTracking for Read-Only Queries89```csharp10// ✅ Good - No change tracking overhead11var products = await _context.Products12.AsNoTracking()13.Where(p => p.CategoryId == categoryId)14.ToListAsync(ct);1516// ❌ Bad - Unnecessary tracking for read-only data17var products = await _context.Products18.Where(p => p.CategoryId == categoryId)19.ToListAsync(ct);20```2122### 2. Select Only Needed Columns2324```csharp25// ✅ Good - Project to DTO26var products = await _context.Products27.AsNoTracking()28.Where(p => p.CategoryId == categoryId)29.Select(p => new ProductDto30{31Id = p.Id,32Name = p.Name,33Price = p.Price34})35.ToListAsync(ct);3637// ❌ Bad - Fetching all columns38var products = await _context.Products39.Where(p => p.CategoryId == categoryId)40.ToListAsync(ct);41```4243### 3. Avoid N+1 Queries with Eager Loading4445```csharp46// ✅ Good - Single query with Include47var orders = await _context.Orders48.AsNoTracking()49.Include(o => o.Items)50.ThenInclude(i => i.Product)51.Where(o => o.CustomerId == customerId)52.ToListAsync(ct);5354// ❌ Bad - N+1 queries (lazy loading)55var orders = await _context.Orders56.Where(o => o.CustomerId == customerId)57.ToListAsync(ct);5859foreach (var order in orders)60{61// Each iteration triggers a separate query!62var items = order.Items.ToList();63}64```6566### 4. Use Split Queries for Large Includes6768```csharp69// ✅ Good - Prevents cartesian explosion70var orders = await _context.Orders71.AsNoTracking()72.Include(o => o.Items)73.Include(o => o.Payments)74.Include(o => o.ShippingHistory)75.AsSplitQuery() // Executes as multiple queries76.Where(o => o.CustomerId == customerId)77.ToListAsync(ct);78```7980### 5. Use Compiled Queries for Hot Paths8182```csharp83public class ProductRepository84{85// Compile once, reuse many times86private static readonly Func<AppDbContext, string, Task<Product?>> GetByIdQuery =87EF.CompileAsyncQuery((AppDbContext ctx, string id) =>88ctx.Products.AsNoTracking().FirstOrDefault(p => p.Id == id));8990private static readonly Func<AppDbContext, int, IAsyncEnumerable<Product>> GetByCategoryQuery =91EF.CompileAsyncQuery((AppDbContext ctx, int categoryId) =>92ctx.Products.AsNoTracking().Where(p => p.CategoryId == categoryId));9394public Task<Product?> GetByIdAsync(string id, CancellationToken ct)95=> GetByIdQuery(_context, id);9697public IAsyncEnumerable<Product> GetByCategoryAsync(int categoryId)98=> GetByCategoryQuery(_context, categoryId);99}100```101102## Batch Operations103104### 6. Use ExecuteUpdate/ExecuteDelete (.NET 7+)105106```csharp107// ✅ Good - Single SQL UPDATE108await _context.Products109.Where(p => p.CategoryId == oldCategoryId)110.ExecuteUpdateAsync(s => s111.SetProperty(p => p.CategoryId, newCategoryId)112.SetProperty(p => p.UpdatedAt, DateTime.UtcNow),113ct);114115// ✅ Good - Single SQL DELETE116await _context.Products117.Where(p => p.IsDeleted && p.UpdatedAt < cutoffDate)118.ExecuteDeleteAsync(ct);119120// ❌ Bad - Loads all entities into memory121var products = await _context.Products122.Where(p => p.CategoryId == oldCategoryId)123.ToListAsync(ct);124125foreach (var product in products)126{127product.CategoryId = newCategoryId;128}129await _context.SaveChangesAsync(ct);130```131132### 7. Bulk Insert with EFCore.BulkExtensions133134```csharp135// Using EFCore.BulkExtensions package136var products = GenerateLargeProductList();137138// ✅ Good - Bulk insert (much faster for large datasets)139await _context.BulkInsertAsync(products, ct);140141// ❌ Bad - Individual inserts142foreach (var product in products)143{144_context.Products.Add(product);145}146await _context.SaveChangesAsync(ct);147```148149## Connection Management150151### 8. Configure Connection Pooling152153```csharp154services.AddDbContext<AppDbContext>(options =>155{156options.UseSqlServer(connectionString, sqlOptions =>157{158sqlOptions.EnableRetryOnFailure(159maxRetryCount: 3,160maxRetryDelay: TimeSpan.FromSeconds(10),161errorNumbersToAdd: null);162163sqlOptions.CommandTimeout(30);164});165166// Performance settings167options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);168169// Development only170if (env.IsDevelopment())171{172options.EnableSensitiveDataLogging();173options.EnableDetailedErrors();174}175});176```177178### 9. Use DbContext Pooling179180```csharp181// ✅ Good - Context pooling (reduces allocation overhead)182services.AddDbContextPool<AppDbContext>(options =>183{184options.UseSqlServer(connectionString);185}, poolSize: 128);186187// Instead of AddDbContext188```189190## Concurrency and Transactions191192### 10. Handle Concurrency with Row Versioning193194```csharp195public class Product196{197public string Id { get; set; }198public string Name { get; set; }199200[Timestamp]201public byte[] RowVersion { get; set; } // SQL Server rowversion202}203204// Or with Fluent API205builder.Property(p => p.RowVersion)206.IsRowVersion();207208// Handle concurrency conflicts209try210{211await _context.SaveChangesAsync(ct);212}213catch (DbUpdateConcurrencyException ex)214{215var entry = ex.Entries.Single();216var databaseValues = await entry.GetDatabaseValuesAsync(ct);217218if (databaseValues == null)219{220// Entity was deleted221throw new NotFoundException("Product was deleted by another user");222}223224// Client wins - overwrite database values225entry.OriginalValues.SetValues(databaseValues);226await _context.SaveChangesAsync(ct);227}228```229230### 11. Use Explicit Transactions When Needed231232```csharp233await using var transaction = await _context.Database.BeginTransactionAsync(ct);234235try236{237// Multiple operations238_context.Orders.Add(order);239await _context.SaveChangesAsync(ct);240241await _context.OrderItems.AddRangeAsync(items, ct);242await _context.SaveChangesAsync(ct);243244await _paymentService.ProcessAsync(order.Id, ct);245246await transaction.CommitAsync(ct);247}248catch249{250await transaction.RollbackAsync(ct);251throw;252}253```254255## Indexing Strategy256257### 12. Create Indexes for Query Patterns258259```csharp260public class ProductConfiguration : IEntityTypeConfiguration<Product>261{262public void Configure(EntityTypeBuilder<Product> builder)263{264// Unique index265builder.HasIndex(p => p.Sku)266.IsUnique();267268// Composite index for common query patterns269builder.HasIndex(p => new { p.CategoryId, p.Name });270271// Filtered index (SQL Server)272builder.HasIndex(p => p.Price)273.HasFilter("[IsDeleted] = 0");274275// Include columns for covering index276builder.HasIndex(p => p.CategoryId)277.IncludeProperties(p => new { p.Name, p.Price });278}279}280```281282## Common Anti-Patterns to Avoid283284### ❌ Calling ToList() Too Early285286```csharp287// ❌ Bad - Materializes all products then filters in memory288var products = _context.Products.ToList()289.Where(p => p.Price > 100);290291// ✅ Good - Filter in SQL292var products = await _context.Products293.Where(p => p.Price > 100)294.ToListAsync(ct);295```296297### ❌ Using Contains with Large Collections298299```csharp300// ❌ Bad - Generates massive IN clause301var ids = GetThousandsOfIds();302var products = await _context.Products303.Where(p => ids.Contains(p.Id))304.ToListAsync(ct);305306// ✅ Good - Use temp table or batch queries307var products = new List<Product>();308foreach (var batch in ids.Chunk(100))309{310var batchResults = await _context.Products311.Where(p => batch.Contains(p.Id))312.ToListAsync(ct);313products.AddRange(batchResults);314}315```316317### ❌ String Concatenation in Queries318319```csharp320// ❌ Bad - Can't use index321var products = await _context.Products322.Where(p => (p.FirstName + " " + p.LastName).Contains(searchTerm))323.ToListAsync(ct);324325// ✅ Good - Use computed column with index326builder.Property(p => p.FullName)327.HasComputedColumnSql("[FirstName] + ' ' + [LastName]");328builder.HasIndex(p => p.FullName);329```330331## Monitoring and Diagnostics332333```csharp334// Log slow queries335services.AddDbContext<AppDbContext>(options =>336{337options.UseSqlServer(connectionString);338339options.LogTo(340filter: (eventId, level) => eventId.Id == CoreEventId.QueryExecutionPlanned.Id,341logger: (eventData) =>342{343if (eventData is QueryExpressionEventData queryData)344{345var duration = queryData.Duration;346if (duration > TimeSpan.FromSeconds(1))347{348_logger.LogWarning("Slow query detected: {Duration}ms - {Query}",349duration.TotalMilliseconds,350queryData.Expression);351}352}353});354});355```356