Entity Framework Core Patterns
When to Use This Skill
Use this skill when:
- Setting up EF Core in a new project
- Optimizing query performance
- Managing database migrations
- Integrating EF Core with .NET Aspire
- Debugging change tracking issues
- Loading multiple navigation collections efficiently (query splitting)
Core Principles
- NoTracking by Default - Most queries are read-only; opt-in to tracking
- Never Edit Migrations Manually - Always use CLI commands
- Dedicated Migration Service - Separate migration execution from application startup
- ExecutionStrategy for Retries - Handle transient database failures
- Explicit Updates - When NoTracking, explicitly mark entities for update
Pattern 1: NoTracking by Default
Configure your DbContext to disable change tracking by default. This improves performance for read-heavy workloads.
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
public DbSet<Order> Orders => Set<Order>();
public DbSet<Customer> Customers => Set<Customer>();
}
When NoTracking is Active
Read-only queries work normally:
var orders = await dbContext.Orders
.Where(o => o.Status == OrderStatus.Pending)
.ToListAsync();
Writes require explicit handling:
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync();
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
dbContext.Orders.Update(order);
await dbContext.SaveChangesAsync();
var order = await dbContext.Orders
.AsTracking()
.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync();
When to Use Tracking
| Scenario |
Use Tracking? |
Why |
| Display data in UI |
No |
Read-only, no updates |
| API GET endpoints |
No |
Returning data, no mutations |
| Update single entity |
Yes or explicit Update() |
Need to save changes |
| Complex update with navigation |
Yes |
Tracking handles relationships |
| Batch operations |
No + ExecuteUpdate |
More efficient |
Explicit Add/Update Pattern
public class OrderService
{
private readonly ApplicationDbContext _db;
public async Task<Order> CreateOrderAsync(Order order)
{
_db.Orders.Add(order);
await _db.SaveChangesAsync();
return order;
}
public async Task UpdateOrderStatusAsync(Guid orderId, OrderStatus newStatus)
{
var order = await _db.Orders.FirstOrDefaultAsync(o => o.Id == orderId)
?? throw new NotFoundException($"Order {orderId} not found");
order.Status = newStatus;
order.UpdatedAt = DateTimeOffset.UtcNow;
_db.Orders.Update(order);
await _db.SaveChangesAsync();
}
public async Task DeleteOrderAsync(Guid orderId)
{
var order = new Order { Id = orderId };
_db.Orders.Remove(order);
await _db.SaveChangesAsync();
}
}
Pattern 2: Never Edit Migrations Manually
CRITICAL: Always use EF Core CLI commands to manage migrations. Never:
- Manually edit migration files (except for custom SQL in
Up()/Down())
- Delete migration files directly
- Rename migration files
- Copy migrations between projects
Creating Migrations
dotnet ef migrations add AddCustomerTable \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
dotnet ef migrations add AddCustomerTable \
--context ApplicationDbContext \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
Removing Migrations
dotnet ef migrations remove \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
Applying Migrations
dotnet ef database update \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
dotnet ef database update AddCustomerTable \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
dotnet ef database update PreviousMigrationName \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
Generating SQL Scripts
dotnet ef migrations script \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api \
--output migrations.sql
dotnet ef migrations script \
--idempotent \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
Pattern 3: Dedicated Migration Service with Aspire
Separate migration execution from your main application using a dedicated migration service. This ensures:
- Migrations complete before the app starts
- Clean separation of concerns
- Controlled seeding in test environments
Project Structure
src/
βββ MyApp.AppHost/ # Aspire orchestration
βββ MyApp.Api/ # Main application
βββ MyApp.Infrastructure/ # DbContext and migrations
βββ MyApp.MigrationService/ # Dedicated migration runner
MigrationService Program.cs
using MyApp.Infrastructure.Data;
using