Appearance
DbContext 详解
深入理解 EF Core 的核心类 - DbContext 的生命周期、配置和最佳实践
📖 目录
DbContext 的职责
DbContext 是 EF Core 的**工作单元(Unit of Work)和仓储(Repository)**模式的实现。
核心职责
┌─────────────────────────────────────┐
│ DbContext 职责 │
├─────────────────────────────────────┤
│ 1. 管理数据库连接 │
│ 2. 跟踪实体状态 │
│ 3. 生成并执行 SQL │
│ 4. 处理事务 │
│ 5. 缓存查询结果 │
│ 6. 协调变更保存 │
└─────────────────────────────────────┘代码示例
csharp
public class AppDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 配置实体映射
}
}生命周期管理
Web 应用(Scoped)
推荐配置:
csharp
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
// 默认生命周期:Scoped(每个请求一个实例)工作原理:
HTTP Request 1 → 创建 DbContext A → 使用 → 请求结束 → 释放 A
HTTP Request 2 → 创建 DbContext B → 使用 → 请求结束 → 释放 B
HTTP Request 3 → 创建 DbContext C → 使用 → 请求结束 → 释放 CController 中使用:
csharp
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;
// 通过构造函数注入(每个请求一个新的 DbContext)
public ProductsController(AppDbContext context)
{
_context = context;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
return await _context.Products.ToListAsync();
}
}Blazor Server / 后台服务(Factory 模式)
问题:Blazor Server 使用 SignalR 长连接,Scoped 生命周期不适用。
解决方案:
csharp
// Program.cs
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseSqlServer(connectionString));
// 或者使用池化
builder.Services.AddPooledDbContextFactory<AppDbContext>(options =>
options.UseSqlServer(connectionString),
poolSize: 128);使用方式:
csharp
public class ProductService
{
private readonly IDbContextFactory<AppDbContext> _factory;
public ProductService(IDbContextFactory<AppDbContext> factory)
{
_factory = factory;
}
public async Task<List<Product>> GetProductsAsync()
{
// 手动创建和释放 DbContext
await using var context = _factory.CreateDbContext();
return await context.Products.ToListAsync();
} // context 自动释放
}控制台应用(手动管理)
csharp
class Program
{
static async Task Main(string[] args)
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer(connectionString)
.Options;
// 使用 using 语句确保正确释放
await using var context = new AppDbContext(options);
var products = await context.Products.ToListAsync();
foreach (var product in products)
{
Console.WriteLine(product.Name);
}
} // context 自动释放
}配置选项详解
1. 日志记录
csharp
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
// 开发环境:详细日志
if (environment.IsDevelopment())
{
options.LogTo(
Console.WriteLine,
new[]
{
DbLoggerCategory.Database.Command.Name, // SQL 命令
DbLoggerCategory.Query.Name, // 查询翻译
DbLoggerCategory.ChangeTracking.Name // 变更跟踪
},
LogLevel.Information);
options.EnableSensitiveDataLogging(); // 显示参数值
options.EnableDetailedErrors(); // 详细错误信息
}
});输出示例:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (12ms) [Parameters=[@__id_0='1' (DbType = Int32)],
CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Name], [p].[Price]
FROM [Products] AS [p]
WHERE [p].[Id] = @__id_02. 查询跟踪行为
csharp
// 全局设置:所有查询默认无追踪
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});
// 单个查询覆盖
var trackedProducts = await context.Products
.AsTracking() // 覆盖全局设置
.ToListAsync();
var untrackedProducts = await context.Products
.AsNoTracking() // 显式无追踪
.ToListAsync();性能对比:
| 跟踪行为 | 适用场景 | 性能 |
|---|---|---|
Tracking | 需要修改实体 | 基准 |
NoTracking | 只读查询 | +20-50% |
3. 重试策略(弹性连接)
csharp
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString, sqlOptions =>
{
// 启用重试策略
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 3, // 最多重试 3 次
maxRetryDelay: TimeSpan.FromSeconds(10), // 最大延迟 10 秒
errorNumbersToAdd: null); // 额外重试的错误代码
});
});适用场景:
- ✅ Azure SQL Database(临时故障)
- ✅ 网络不稳定
- ❌ 本地开发环境(不需要)
自定义重试逻辑:
csharp
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: new[] { 4060, 40197 }); // 特定错误代码4. 命令超时
csharp
options.UseSqlServer(connectionString, sqlOptions =>
{
// 设置命令超时(秒)
sqlOptions.CommandTimeout(60); // 默认 30 秒
});建议值:
- 简单查询:30 秒
- 复杂报表:60-120 秒
- 批量操作:300 秒
5. 敏感数据日志
csharp
// ⚠️ 仅开发环境启用
if (environment.IsDevelopment())
{
options.EnableSensitiveDataLogging();
}
// 生产环境禁用(防止泄露密码等敏感信息)输出对比:
// 启用前
Executed DbCommand (12ms) [Parameters=[@__id_0='?' (DbType = Int32)]]
// 启用后
Executed DbCommand (12ms) [Parameters=[@__id_0='1' (DbType = Int32)]]线程安全
⚠️ DbContext 不是线程安全的
csharp
// ❌ 错误:多线程共享同一个 DbContext
Parallel.For(0, 100, async i =>
{
await context.Products.AddAsync(new Product { Name = $"Product {i}" });
await context.SaveChangesAsync(); // 可能抛出异常
});
// 异常信息:
// A second operation was started on this context instance before a previous operation completed.✅ 正确的并发处理方式
方案 1:每个操作创建新的 DbContext
csharp
private readonly IServiceScopeFactory _scopeFactory;
public async Task ProcessProductsAsync()
{
var tasks = Enumerable.Range(0, 100).Select(async i =>
{
// 每个任务创建独立的 scope 和 DbContext
await using var scope = _scopeFactory.CreateAsyncScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
context.Products.Add(new Product { Name = $"Product {i}" });
await context.SaveChangesAsync();
});
await Task.WhenAll(tasks);
}方案 2:使用 IDbContextFactory
csharp
private readonly IDbContextFactory<AppDbContext> _factory;
public async Task ProcessProductsAsync()
{
var tasks = Enumerable.Range(0, 100).Select(async i =>
{
// 每个任务创建独立的 DbContext
await using var context = _factory.CreateDbContext();
context.Products.Add(new Product { Name = $"Product {i}" });
await context.SaveChangesAsync();
});
await Task.WhenAll(tasks);
}方案 3:串行执行
csharp
// 如果必须共享 DbContext,则串行执行
for (int i = 0; i < 100; i++)
{
context.Products.Add(new Product { Name = $"Product {i}" });
}
await context.SaveChangesAsync(); // 一次性保存连接池化
DbContext 池化
csharp
// 启用 DbContext 池化
builder.Services.AddDbContextPool<AppDbContext>(
options => options.UseSqlServer(connectionString),
poolSize: 128 // 池大小,默认 128
);工作原理:
请求 1 → 从池中获取 DbContext → 使用 → 重置状态 → 归还池中
请求 2 → 从池中获取同一个 DbContext → 使用 → 重置状态 → 归还池中性能提升:10-30%(高并发场景)
注意事项:
- ⚠️ 不能在
OnConfiguring中使用外部依赖 - ⚠️ 不适合使用实例状态的上下文
- ⚠️ 池大小根据并发量调整
数据库连接池
csharp
// 连接字符串中配置
"Server=localhost;Database=MyDb;Pooling=true;Min Pool Size=0;Max Pool Size=100;"EF Core 自动管理:
- ✅ 打开连接时从池中获取
- ✅ 关闭连接时归还池中
- ✅ 无需手动管理
高级用法
1. 重写 SaveChangesAsync
csharp
public class AuditableDbContext : DbContext
{
public override async Task<int> SaveChangesAsync(
CancellationToken cancellationToken = default)
{
// 自动填充审计字段
var entries = ChangeTracker.Entries<IAuditable>();
foreach (var entry in entries)
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = DateTime.UtcNow;
entry.Entity.CreatedBy = GetCurrentUserId();
break;
case EntityState.Modified:
entry.Entity.ModifiedAt = DateTime.UtcNow;
entry.Entity.ModifiedBy = GetCurrentUserId();
break;
}
}
return await base.SaveChangesAsync(cancellationToken);
}
private string GetCurrentUserId()
{
// 从 HttpContext 或 ClaimsPrincipal 获取
return "system";
}
}2. 事务管理
csharp
public async Task TransferMoneyAsync(int fromAccountId, int toAccountId, decimal amount)
{
await using var transaction = await Database.BeginTransactionAsync();
try
{
// 扣款
var fromAccount = await Accounts.FindAsync(fromAccountId);
fromAccount.Balance -= amount;
// 收款
var toAccount = await Accounts.FindAsync(toAccountId);
toAccount.Balance += amount;
await SaveChangesAsync();
// 提交事务
await transaction.CommitAsync();
}
catch (Exception)
{
// 回滚事务
await transaction.RollbackAsync();
throw;
}
}3. 批量操作优化
csharp
public async Task BulkInsertAsync(IEnumerable<Product> products)
{
// 禁用自动检测变化
ChangeTracker.AutoDetectChangesEnabled = false;
try
{
await Products.AddRangeAsync(products);
await SaveChangesAsync();
}
finally
{
// 恢复自动检测
ChangeTracker.AutoDetectChangesEnabled = true;
}
}性能提升:20-50%(大批量插入)
4. 查询标签
csharp
// 为查询添加标签,便于调试
var products = await context.Products
.TagWith("Get active products")
.Where(p => p.IsActive)
.ToListAsync();
// 生成的 SQL 注释:
-- Get active products
SELECT [p].[Id], [p].[Name] FROM [Products] AS [p] WHERE [p].[IsActive] = 15. 拦截器(Interceptors)
csharp
public class QueryPerformanceInterceptor : DbCommandInterceptor
{
private readonly ILogger<QueryPerformanceInterceptor> _logger;
public QueryPerformanceInterceptor(ILogger<QueryPerformanceInterceptor> logger)
{
_logger = logger;
}
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result)
{
// 记录慢查询
var stopwatch = Stopwatch.StartNew();
var reader = base.ReaderExecuting(command, eventData, result);
stopwatch.Stop();
if (stopwatch.ElapsedMilliseconds > 1000)
{
_logger.LogWarning("Slow query detected: {Sql} ({Elapsed}ms)",
command.CommandText, stopwatch.ElapsedMilliseconds);
}
return reader;
}
}
// 注册拦截器
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString);
options.AddInterceptors(new QueryPerformanceInterceptor(logger));
});💡 最佳实践
1. 及时释放 DbContext
csharp
// ✅ 正确:使用 using 语句
await using var context = new AppDbContext(options);
var products = await context.Products.ToListAsync();
// ❌ 错误:忘记释放
var context = new AppDbContext(options);
var products = await context.Products.ToListAsync();
// 内存泄漏!2. 避免长时间持有 DbContext
csharp
// ❌ 错误:在循环中保持 DbContext
foreach (var order in orders)
{
await ProcessOrderAsync(context, order); // context 长时间存活
}
// ✅ 正确:短生命周期
foreach (var order in orders)
{
await using var scope = scopeFactory.CreateAsyncScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await ProcessOrderAsync(context, order);
}3. 合理使用 AsNoTracking
csharp
// 只读查询
var products = await context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
// 需要修改的查询
var product = await context.Products.FindAsync(id);
product.Price = 99.99m;
await context.SaveChangesAsync();4. 批量操作用 AddRange
csharp
// ✅ 推荐
context.Products.AddRange(products);
await context.SaveChangesAsync();
// ❌ 避免
foreach (var product in products)
{
context.Products.Add(product);
}
await context.SaveChangesAsync();⚠️ 常见陷阱
1. DbContext 已释放
错误:
Cannot access a disposed context instance.原因:
- 异步操作中 DbContext 已被释放
- 跨线程访问 DbContext
解决:
csharp
// 确保 DbContext 在 using 范围内
await using var context = new AppDbContext(options);
var products = await context.Products.ToListAsync();
// 不要在这里继续使用 context2. 跟踪冲突
错误:
The instance of entity type 'Product' cannot be tracked because another instance with the same key value is already being tracked.原因:
- 多次加载同一实体
解决:
csharp
// 方法 1:使用 AsNoTracking
var product1 = await context.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == 1);
var product2 = await context.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == 1);
// 方法 2:先检查是否已跟踪
var existing = context.ChangeTracker.Entries<Product>()
.FirstOrDefault(e => e.Entity.Id == id);
if (existing != null)
{
existing.Entity.Price = 99.99m;
}
else
{
var product = await context.Products.FindAsync(id);
product.Price = 99.99m;
}3. 内存泄漏
原因:
- DbContext 长时间存活
- 跟踪大量实体
解决:
csharp
// 定期清理变更跟踪器
context.ChangeTracker.Clear();
// 或使用短生命周期的 DbContext
await using var context = factory.CreateDbContext();📊 性能监控
1. 统计查询次数
csharp
public class QueryCounterInterceptor : DbCommandInterceptor
{
public int QueryCount { get; private set; }
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result)
{
QueryCount++;
return base.ReaderExecuting(command, eventData, result);
}
}
// 使用
var counter = new QueryCounterInterceptor();
options.AddInterceptors(counter);
// 执行查询...
Console.WriteLine($"Total queries: {counter.QueryCount}");2. 监控内存使用
csharp
var memoryBefore = GC.GetTotalMemory(false);
// 执行操作
var products = await context.Products.ToListAsync();
var memoryAfter = GC.GetTotalMemory(false);
var memoryUsed = (memoryAfter - memoryBefore) / 1024.0 / 1024.0;
Console.WriteLine($"Memory used: {memoryUsed:F2} MB");📚 延伸阅读
💡 小结
关键点:
- ✅ Web 应用使用 Scoped 生命周期
- ✅ Blazor/后台服务使用 Factory 模式
- ✅ DbContext 不是线程安全的
- ✅ 及时释放 DbContext 避免内存泄漏
- ✅ 只读查询使用 AsNoTracking
- ✅ 批量操作用 AddRange
下一步: