| name | efcore-patterns |
| description | EF Core entity configuration with IEntityTypeConfiguration, migration workflows with dedicated migration services, query optimization using AsSplitQuery and projection, ExecuteUpdateAsync/ExecuteDeleteAsync for bulk operations, and IDbContextFactory for long-lived services. |
Entity Framework Core Patterns
When to Use This Skill
Use when setting up EF Core in a new project, managing migrations, optimizing queries, integrating with Aspire, or debugging change tracking issues.
NoTracking by Default
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
public DbSet<Order> Orders => Set<Order>();
}
When NoTracking is active, 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();
Migration Management
Always use CLI commands. Never edit, delete, or rename migration files directly.
dotnet ef migrations add AddCustomerTable \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
dotnet ef migrations remove \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
dotnet ef migrations script --idempotent \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api
Dedicated Migration Service with Aspire
Separate migration execution from the main application:
public class MigrationWorker : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly IHostApplicationLifetime _lifetime;
public MigrationWorker(IServiceProvider sp, IHostApplicationLifetime lifetime)
{
_serviceProvider = sp;
_lifetime = lifetime;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = _serviceProvider.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var strategy = db.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(() => db.Database.MigrateAsync(stoppingToken));
_lifetime.StopApplication();
}
}
AppHost wiring โ API waits for migrations to finish:
var migrations = builder.AddProject<Projects.MyApp_MigrationService>("migrations")
.WaitFor(db).WithReference(db);
var api = builder.AddProject<Projects.MyApp_Api>("api")
.WaitForCompletion(migrations)
.WithReference(db);
ExecutionStrategy for Transient Failures
Transactions must be inside the strategy callback:
var strategy = _dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
});
Bulk Operations
Use ExecuteUpdateAsync/ExecuteDeleteAsync instead of loading entities:
var expired = await _db.Orders.Where(o => o.ExpiresAt < now).ToListAsync();
foreach (var o in expired) o.Status = OrderStatus.Expired;
await _db.SaveChangesAsync();
await _db.Orders
.Where(o => o.ExpiresAt < now)
.ExecuteUpdateAsync(s => s
.SetProperty(o => o.Status, OrderStatus.Expired)
.SetProperty(o => o.UpdatedAt, DateTimeOffset.UtcNow));
Query Splitting
Enable globally to prevent cartesian explosion with multiple Includes:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString, o =>
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)));
Override per-query when single query is better:
var orders = await dbContext.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.AsSingleQuery()
.ToListAsync();
| Behavior | Pros | Cons |
|---|
| SplitQuery | No cartesian explosion | Multiple round-trips |
| SingleQuery | Single round-trip, transactional consistency | Cartesian explosion with multiple collections |
Projection Over SELECT *
var orders = await _db.Orders.Include(o => o.Items).ToListAsync();
var orders = await _db.Orders
.Where(o => o.CustomerId == customerId)
.Select(o => new OrderSummary(
o.Id, o.Total, o.Status,
o.Items.Count))
.ToListAsync();
DbContext Lifetime
builder.Services.AddDbContext<ApplicationDbContext>(o => o.UseNpgsql(cs));
using var scope = _serviceProvider.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
builder.Services.AddDbContextFactory<ApplicationDbContext>(o => o.UseNpgsql(cs));
await using var db = await _dbFactory.CreateDbContextAsync();
Common Pitfalls
foreach (var id in orderIds)
await _db.Orders.FindAsync(id);
await _db.Orders.Where(o => orderIds.Contains(o.Id)).ToListAsync();
var orders = _db.Orders.ToList();
var orders = await _db.Orders.ToListAsync();