efcore-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseEntity Framework Core Patterns
Entity Framework Core 模式
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)
在以下场景中使用此技能:
- 在新项目中设置 EF Core
- 优化查询性能
- 管理数据库迁移
- 将 EF Core 与 .NET Aspire 集成
- 调试变更跟踪问题
- 高效加载多个导航集合(查询拆分)
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
- 默认启用NoTracking - 大多数查询为只读;按需启用跟踪
- 绝不手动编辑迁移 - 始终使用CLI命令
- 专用迁移服务 - 将迁移执行与应用启动分离
- 重试执行策略 - 处理数据库瞬时故障
- 显式更新 - 启用NoTracking时,显式标记实体以进行更新
Pattern 1: NoTracking by Default
模式1:默认启用NoTracking
Configure your DbContext to disable change tracking by default. This improves performance for read-heavy workloads.
csharp
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
// Disable change tracking by default for better performance on read-only queries
// Use .AsTracking() explicitly for queries that need to track changes
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
public DbSet<Order> Orders => Set<Order>();
public DbSet<Customer> Customers => Set<Customer>();
}配置DbContext以默认禁用变更跟踪。这能提升读密集型工作负载的性能。
csharp
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
// 对只读查询默认禁用变更跟踪以提升性能
// 对需要跟踪变更的查询,显式使用.AsTracking()
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
public DbSet<Order> Orders => Set<Order>();
public DbSet<Customer> Customers => Set<Customer>();
}When NoTracking is Active
启用NoTracking时的操作
Read-only queries work normally:
csharp
// ✅ Fast read - no tracking overhead
var orders = await dbContext.Orders
.Where(o => o.Status == OrderStatus.Pending)
.ToListAsync();Writes require explicit handling:
csharp
// ❌ WRONG - Entity not tracked, SaveChanges does nothing
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync(); // Nothing happens!
// ✅ CORRECT - Explicitly mark entity for update
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
dbContext.Orders.Update(order); // Marks entire entity as modified
await dbContext.SaveChangesAsync();
// ✅ ALSO CORRECT - Use AsTracking() for the query
var order = await dbContext.Orders
.AsTracking()
.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync(); // Works!只读查询可正常工作:
csharp
// ✅ 快速读取 - 无跟踪开销
var orders = await dbContext.Orders
.Where(o => o.Status == OrderStatus.Pending)
.ToListAsync();写入操作需要显式处理:
csharp
// ❌ 错误 - 实体未被跟踪,SaveChanges无任何作用
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();
// ✅ 同样正确 - 对查询使用AsTracking()
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 |
| 场景 | 是否使用跟踪? | 原因 |
|---|---|---|
| 在UI中展示数据 | 否 | 只读,无需更新 |
| API GET端点 | 否 | 返回数据,无需修改 |
| 更新单个实体 | 是或使用显式Update() | 需要保存变更 |
| 带导航属性的复杂更新 | 是 | 跟踪可处理关系 |
| 批量操作 | 否 + ExecuteUpdate | 效率更高 |
Explicit Add/Update Pattern
显式添加/更新模式
csharp
public class OrderService
{
private readonly ApplicationDbContext _db;
// CREATE - Always use Add (works regardless of tracking)
public async Task<Order> CreateOrderAsync(Order order)
{
_db.Orders.Add(order);
await _db.SaveChangesAsync();
return order;
}
// UPDATE - Explicitly mark as modified
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;
// Explicitly mark as modified since DbContext uses NoTracking by default
_db.Orders.Update(order);
await _db.SaveChangesAsync();
}
// DELETE - Attach and remove
public async Task DeleteOrderAsync(Guid orderId)
{
var order = new Order { Id = orderId };
_db.Orders.Remove(order);
await _db.SaveChangesAsync();
}
}csharp
public class OrderService
{
private readonly ApplicationDbContext _db;
// 创建 - 始终使用Add(无论是否启用跟踪都有效)
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($"未找到订单 {orderId}");
order.Status = newStatus;
order.UpdatedAt = DateTimeOffset.UtcNow;
// 由于DbContext默认使用NoTracking,需显式标记为已修改
_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
模式2:绝不手动编辑迁移
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
关键注意事项: 始终使用EF Core CLI命令管理迁移。绝不要:
- 手动编辑迁移文件(/
Up()中的自定义SQL除外)Down() - 直接删除迁移文件
- 重命名迁移文件
- 在项目间复制迁移
Creating Migrations
创建迁移
bash
undefinedbash
undefinedCreate a new migration
创建新迁移
dotnet ef migrations add AddCustomerTable
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
dotnet ef migrations add AddCustomerTable
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
With a specific DbContext (if you have multiple)
指定特定DbContext(如果有多个)
dotnet ef migrations add AddCustomerTable
--context ApplicationDbContext
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--context ApplicationDbContext
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
undefineddotnet ef migrations add AddCustomerTable
--context ApplicationDbContext
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--context ApplicationDbContext
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
undefinedRemoving Migrations
移除迁移
bash
undefinedbash
undefinedRemove the last migration (if not yet applied)
移除最后一个迁移(如果尚未应用)
dotnet ef migrations remove
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
dotnet ef migrations remove
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
NEVER do this:
绝不要这样做:
rm Migrations/20240101_AddCustomerTable.cs # ❌ BAD!
rm Migrations/20240101_AddCustomerTable.cs # ❌ 错误!
undefinedundefinedApplying Migrations
应用迁移
bash
undefinedbash
undefinedApply all pending migrations
应用所有待处理的迁移
dotnet ef database update
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
dotnet ef database update
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
Apply to a specific migration
应用到特定迁移
dotnet ef database update AddCustomerTable
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
dotnet ef database update AddCustomerTable
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
Rollback to a previous migration
回滚到之前的迁移
dotnet ef database update PreviousMigrationName
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
undefineddotnet ef database update PreviousMigrationName
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
undefinedGenerating SQL Scripts
生成SQL脚本
bash
undefinedbash
undefinedGenerate SQL script for all migrations
生成所有迁移的SQL脚本
dotnet ef migrations script
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--output migrations.sql
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--output migrations.sql
dotnet ef migrations script
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--output migrations.sql
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--output migrations.sql
Generate idempotent script (safe to run multiple times)
生成幂等脚本(可安全多次运行)
dotnet ef migrations script
--idempotent
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--idempotent
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
---dotnet ef migrations script
--idempotent
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
--idempotent
--project src/MyApp.Infrastructure
--startup-project src/MyApp.Api
---Pattern 3: Dedicated Migration Service with Aspire
模式3:结合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 runnersrc/
├── MyApp.AppHost/ # Aspire 编排
├── MyApp.Api/ # 主应用
├── MyApp.Infrastructure/ # DbContext 和迁移
└── MyApp.MigrationService/ # 专用迁移运行器MigrationService Program.cs
MigrationService Program.cs
csharp
using MyApp.Infrastructure.Data;
using MyApp.MigrationService;
using Microsoft.EntityFrameworkCore;
var builder = Host.CreateApplicationBuilder(args);
// Add Aspire service defaults
builder.AddServiceDefaults();
// Add PostgreSQL DbContext
var connectionString = builder.Configuration.GetConnectionString("appdb")
?? throw new InvalidOperationException("Connection string 'appdb' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString, npgsqlOptions =>
npgsqlOptions.MigrationsAssembly("MyApp.Infrastructure")));
// Add the migration worker
builder.Services.AddHostedService<MigrationWorker>();
var host = builder.Build();
host.Run();csharp
using MyApp.Infrastructure.Data;
using MyApp.MigrationService;
using Microsoft.EntityFrameworkCore;
var builder = Host.CreateApplicationBuilder(args);
// 添加Aspire服务默认配置
builder.AddServiceDefaults();
// 添加PostgreSQL DbContext
var connectionString = builder.Configuration.GetConnectionString("appdb")
?? throw new InvalidOperationException("未找到连接字符串'appdb'。");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString, npgsqlOptions =>
npgsqlOptions.MigrationsAssembly("MyApp.Infrastructure")));
// 添加迁移工作者
builder.Services.AddHostedService<MigrationWorker>();
var host = builder.Build();
host.Run();MigrationWorker.cs
MigrationWorker.cs
csharp
public class MigrationWorker : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly ILogger<MigrationWorker> _logger;
public MigrationWorker(
IServiceProvider serviceProvider,
IHostApplicationLifetime hostApplicationLifetime,
ILogger<MigrationWorker> logger)
{
_serviceProvider = serviceProvider;
_hostApplicationLifetime = hostApplicationLifetime;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Migration service starting...");
try
{
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await RunMigrationsAsync(dbContext, stoppingToken);
_logger.LogInformation("Migration service completed successfully.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Migration service failed: {Error}", ex.Message);
throw;
}
finally
{
// Stop the application after migrations complete
_hostApplicationLifetime.StopApplication();
}
}
private async Task RunMigrationsAsync(ApplicationDbContext dbContext, CancellationToken ct)
{
// Use execution strategy for transient failure handling
var strategy = dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync(ct);
if (pendingMigrations.Any())
{
_logger.LogInformation("Applying {Count} pending migrations...",
pendingMigrations.Count());
await dbContext.Database.MigrateAsync(ct);
_logger.LogInformation("Migrations applied successfully.");
}
else
{
_logger.LogInformation("No pending migrations. Database is up to date.");
}
});
}
}csharp
public class MigrationWorker : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly ILogger<MigrationWorker> _logger;
public MigrationWorker(
IServiceProvider serviceProvider,
IHostApplicationLifetime hostApplicationLifetime,
ILogger<MigrationWorker> logger)
{
_serviceProvider = serviceProvider;
_hostApplicationLifetime = hostApplicationLifetime;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("迁移服务启动中...");
try
{
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await RunMigrationsAsync(dbContext, stoppingToken);
_logger.LogInformation("迁移服务已成功完成。");
}
catch (Exception ex)
{
_logger.LogError(ex, "迁移服务失败:{Error}", ex.Message);
throw;
}
finally
{
// 迁移完成后停止应用
_hostApplicationLifetime.StopApplication();
}
}
private async Task RunMigrationsAsync(ApplicationDbContext dbContext, CancellationToken ct)
{
// 使用执行策略处理瞬时故障
var strategy = dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync(ct);
if (pendingMigrations.Any())
{
_logger.LogInformation("正在应用 {Count} 个待处理迁移...",
pendingMigrations.Count());
await dbContext.Database.MigrateAsync(ct);
_logger.LogInformation("迁移已成功应用。");
}
else
{
_logger.LogInformation("无待处理迁移。数据库已处于最新状态。");
}
});
}
}AppHost Configuration
AppHost 配置
csharp
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres");
var db = postgres.AddDatabase("appdb");
// Migrations run first, then exit
var migrations = builder.AddProject<Projects.MyApp_MigrationService>("migrations")
.WaitFor(db)
.WithReference(db);
// API waits for migrations to complete
var api = builder.AddProject<Projects.MyApp_Api>("api")
.WaitForCompletion(migrations) // Key: waits for migrations to finish
.WithReference(db);csharp
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres");
var db = postgres.AddDatabase("appdb");
// 迁移先运行,然后退出
var migrations = builder.AddProject<Projects.MyApp_MigrationService>("migrations")
.WaitFor(db)
.WithReference(db);
// API等待迁移完成
var api = builder.AddProject<Projects.MyApp_Api>("api")
.WaitForCompletion(migrations) // 关键:等待迁移完成
.WithReference(db);Pattern 4: ExecutionStrategy for Transient Failures
模式4:处理瞬时故障的执行策略
Always use for operations that might fail transiently:
CreateExecutionStrategy()csharp
public async Task UpdateWithRetryAsync(Guid id, Action<Order> update)
{
var strategy = _dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
var order = await _dbContext.Orders
.AsTracking()
.FirstOrDefaultAsync(o => o.Id == id);
if (order is null) return;
update(order);
await _dbContext.SaveChangesAsync();
});
}Important: You cannot use with user-initiated transactions. If you need transactions with retry:
CreateExecutionStrategy()csharp
var strategy = _dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
// Transaction must be INSIDE the strategy callback
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// ... your operations ...
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
});对于可能出现瞬时故障的操作,始终使用:
CreateExecutionStrategy()csharp
public async Task UpdateWithRetryAsync(Guid id, Action<Order> update)
{
var strategy = _dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
var order = await _dbContext.Orders
.AsTracking()
.FirstOrDefaultAsync(o => o.Id == id);
if (order is null) return;
update(order);
await _dbContext.SaveChangesAsync();
});
}重要提示: 不能将与用户发起的事务一起使用。如果需要带重试的事务:
CreateExecutionStrategy()csharp
var strategy = _dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
// 事务必须在策略回调内部
await using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// ... 你的操作 ...
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
});Pattern 5: Bulk Operations with ExecuteUpdate/ExecuteDelete
模式5:使用ExecuteUpdate/ExecuteDelete进行批量操作
For bulk operations, use EF Core 7+ and instead of loading entities:
ExecuteUpdateAsyncExecuteDeleteAsynccsharp
// ❌ SLOW - Loads all entities into memory
var expiredOrders = await _db.Orders
.Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
.ToListAsync();
foreach (var order in expiredOrders)
{
order.Status = OrderStatus.Expired;
}
await _db.SaveChangesAsync();
// ✅ FAST - Single SQL UPDATE statement
await _db.Orders
.Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
.ExecuteUpdateAsync(setters => setters
.SetProperty(o => o.Status, OrderStatus.Expired)
.SetProperty(o => o.UpdatedAt, DateTimeOffset.UtcNow));
// ✅ FAST - Single SQL DELETE statement
await _db.Orders
.Where(o => o.Status == OrderStatus.Cancelled && o.CreatedAt < cutoffDate)
.ExecuteDeleteAsync();对于批量操作,使用EF Core 7+的和,而非加载实体:
ExecuteUpdateAsyncExecuteDeleteAsynccsharp
// ❌ 缓慢 - 将所有实体加载到内存中
var expiredOrders = await _db.Orders
.Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
.ToListAsync();
foreach (var order in expiredOrders)
{
order.Status = OrderStatus.Expired;
}
await _db.SaveChangesAsync();
// ✅ 快速 - 单条SQL UPDATE语句
await _db.Orders
.Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
.ExecuteUpdateAsync(setters => setters
.SetProperty(o => o.Status, OrderStatus.Expired)
.SetProperty(o => o.UpdatedAt, DateTimeOffset.UtcNow));
// ✅ 快速 - 单条SQL DELETE语句
await _db.Orders
.Where(o => o.Status == OrderStatus.Cancelled && o.CreatedAt < cutoffDate)
.ExecuteDeleteAsync();Common Pitfalls
常见陷阱
1. Forgetting to Update When NoTracking
1. 启用NoTracking时忘记更新
csharp
// ❌ Silent failure - entity not tracked
var customer = await _db.Customers.FindAsync(id);
customer.Name = "New Name";
await _db.SaveChangesAsync(); // Does nothing!
// ✅ Explicit update
var customer = await _db.Customers.FindAsync(id);
customer.Name = "New Name";
_db.Customers.Update(customer);
await _db.SaveChangesAsync();csharp
// ❌ 静默失败 - 实体未被跟踪
var customer = await _db.Customers.FindAsync(id);
customer.Name = "新名称";
await _db.SaveChangesAsync(); // 无任何作用!
// ✅ 显式更新
var customer = await _db.Customers.FindAsync(id);
customer.Name = "新名称";
_db.Customers.Update(customer);
await _db.SaveChangesAsync();2. N+1 Query Problem
2. N+1查询问题
csharp
// ❌ N+1 queries - one query per order
var customers = await _db.Customers.ToListAsync();
foreach (var customer in customers)
{
var orders = customer.Orders; // Lazy load triggers query
}
// ✅ Eager loading - single query
var customers = await _db.Customers
.Include(c => c.Orders)
.ToListAsync();csharp
// ❌ N+1查询 - 每个订单触发一次查询
var customers = await _db.Customers.ToListAsync();
foreach (var customer in customers)
{
var orders = customer.Orders; // 延迟加载触发查询
}
// ✅ 贪婪加载 - 单条查询
var customers = await _db.Customers
.Include(c => c.Orders)
.ToListAsync();3. Tracking Conflicts with Multiple DbContext Instances
3. 多个DbContext实例的跟踪冲突
csharp
// ❌ Tracking conflict - entity tracked by different context
var order1 = await _db1.Orders.AsTracking().FindAsync(id);
var order2 = await _db2.Orders.AsTracking().FindAsync(id);
order2.Status = OrderStatus.Shipped;
await _db2.SaveChangesAsync(); // May throw or behave unexpectedly
// ✅ Use single context or detach first
_db1.Entry(order1).State = EntityState.Detached;csharp
// ❌ 跟踪冲突 - 实体被不同上下文跟踪
var order1 = await _db1.Orders.AsTracking().FindAsync(id);
var order2 = await _db2.Orders.AsTracking().FindAsync(id);
order2.Status = OrderStatus.Shipped;
await _db2.SaveChangesAsync(); // 可能抛出异常或行为异常
// ✅ 使用单个上下文或先分离
_db1.Entry(order1).State = EntityState.Detached;4. Not Using Async Consistently
4. 未一致使用异步
csharp
// ❌ Blocking call in async context
var orders = _db.Orders.ToList(); // Blocks thread
// ✅ Async all the way
var orders = await _db.Orders.ToListAsync();csharp
// ❌ 在异步上下文中使用阻塞调用
var orders = _db.Orders.ToList(); // 阻塞线程
// ✅ 全程使用异步
var orders = await _db.Orders.ToListAsync();5. Querying Inside Loops
5. 在循环内查询
csharp
// ❌ Query per iteration
foreach (var orderId in orderIds)
{
var order = await _db.Orders.FindAsync(orderId);
// process order
}
// ✅ Single query
var orders = await _db.Orders
.Where(o => orderIds.Contains(o.Id))
.ToListAsync();csharp
// ❌ 每次迭代触发一次查询
foreach (var orderId in orderIds)
{
var order = await _db.Orders.FindAsync(orderId);
// 处理订单
}
// ✅ 单条查询
var orders = await _db.Orders
.Where(o => orderIds.Contains(o.Id))
.ToListAsync();DbContext Lifetime in DI
DI中的DbContext生命周期
ASP.NET Core (Scoped by Default)
ASP.NET Core(默认作用域)
csharp
// Scoped = one instance per HTTP request
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString));csharp
// 作用域 = 每个HTTP请求一个实例
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString));Background Services (Create Scope)
后台服务(创建作用域)
csharp
public class MyBackgroundService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// ✅ Create scope for each unit of work
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// ... use dbContext ...
}
}csharp
public class MyBackgroundService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// ✅ 为每个工作单元创建作用域
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// ... 使用dbContext ...
}
}Actors / Long-Lived Objects (Factory Pattern)
角色/长生命周期对象(工厂模式)
csharp
public class OrderActor : ReceiveActor
{
private readonly IDbContextFactory<ApplicationDbContext> _dbFactory;
public OrderActor(IDbContextFactory<ApplicationDbContext> dbFactory)
{
_dbFactory = dbFactory;
ReceiveAsync<GetOrder>(async msg =>
{
// Create fresh context for each operation
await using var db = await _dbFactory.CreateDbContextAsync();
var order = await db.Orders.FindAsync(msg.OrderId);
Sender.Tell(order);
});
}
}
// Registration
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString));csharp
public class OrderActor : ReceiveActor
{
private readonly IDbContextFactory<ApplicationDbContext> _dbFactory;
public OrderActor(IDbContextFactory<ApplicationDbContext> dbFactory)
{
_dbFactory = dbFactory;
ReceiveAsync<GetOrder>(async msg =>
{
// 为每个操作创建新上下文
await using var db = await _dbFactory.CreateDbContextAsync();
var order = await db.Orders.FindAsync(msg.OrderId);
Sender.Tell(order);
});
}
}
// 注册
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString));Pattern 6: Query Splitting to Prevent Cartesian Explosion
模式6:查询拆分以避免笛卡尔积爆炸
When you load multiple navigation collections via , EF Core generates a single query that can cause cartesian explosion. If you have 10 orders with 10 items each, you get 100 rows instead of 10 + 10.
Include()当你通过加载多个导航集合时,EF Core会生成单条查询,这可能导致笛卡尔积爆炸。如果有10个订单,每个订单有10个商品,你会得到100行数据,而非10+10行。
Include()Global Configuration (Recommended for Most Cases)
全局配置(大多数场景推荐)
Enable query splitting globally in your DbContext configuration:
csharp
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString, npgsqlOptions =>
{
npgsqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
}));在DbContext配置中全局启用查询拆分:
csharp
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString, npgsqlOptions =>
{
npgsqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
}));Per-Query Override
按查询覆盖
Use single query when you know it's more efficient:
csharp
// Use single query when you know the structure is well-understood
var orders = await dbContext.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.AsSingleQuery() // Override global split behavior
.ToListAsync();当你知道单条查询更高效时,使用单条查询:
csharp
// 当你清楚数据结构时,使用单条查询
var orders = await dbContext.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.AsSingleQuery() // 覆盖全局拆分行为
.ToListAsync();Trade-offs
权衡
| Behavior | Pros | Cons |
|---|---|---|
| SplitQuery | No cartesian explosion, better for large collections | Multiple round-trips, potential consistency issues |
| SingleQuery | Single round-trip, transactional consistency | Cartesian explosion with multiple collections |
Recommendation: Default to globally, override with for specific queries where single-query is known to be better.
SplitQueryAsSingleQuery()| 行为 | 优点 | 缺点 |
|---|---|---|
| SplitQuery | 无笛卡尔积爆炸,大型集合表现更好 | 多次往返,可能存在一致性问题 |
| SingleQuery | 单次往返,事务一致性 | 多集合场景下笛卡尔积爆炸 |
推荐: 全局默认使用,在已知单条查询更优的特定查询中,使用覆盖。
SplitQueryAsSingleQuery()When to Prefer SingleQuery
何时优先选择SingleQuery
- Small, well-understood navigation graphs (2-3 levels)
- Queries where all related data is always needed
- Performance-critical paths where round-trip cost is lower than cartesian explosion
- 小型、结构清晰的导航图(2-3层)
- 始终需要所有关联数据的查询
- 往返成本低于笛卡尔积爆炸的性能关键路径
When to Prefer SplitQuery
何时优先选择SplitQuery
- Large or unpredictable navigation graphs
- Many-to-many relationships
- Queries loading collections that may not all be needed
- 大型或不可预测的导航图
- 多对多关系
- 加载的集合可能并非全部需要的查询
Testing with EF Core
EF Core测试
In-Memory Provider (Unit Tests Only)
内存提供程序(仅用于单元测试)
csharp
// Only for simple unit tests - doesn't match real database behavior
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var context = new ApplicationDbContext(options);csharp
// 仅用于简单单元测试 - 与真实数据库行为不符
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var context = new ApplicationDbContext(options);Real Database with TestContainers (Integration Tests)
使用TestContainers的真实数据库(集成测试)
See the skill for proper database testing.
testcontainers-integration-testscsharp
// Use real PostgreSQL in container
var container = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.Build();
await container.StartAsync();
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseNpgsql(container.GetConnectionString())
.Options;请查看技能以了解正确的数据库测试方法。
testcontainers-integration-testscsharp
// 使用容器中的真实PostgreSQL
var container = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.Build();
await container.StartAsync();
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseNpgsql(container.GetConnectionString())
.Options;