Skip to content

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 → 使用 → 请求结束 → 释放 C

Controller 中使用:

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_0

2. 查询跟踪行为 ​

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] = 1

5. 拦截器(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();
// 不要在这里继续使用 context

2. 跟踪冲突 ​

错误:

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

下一步:

  1. 学习 变更跟踪原理
  2. 掌握 实体配置方式
  3. 理解 查询管道

基于 MIT 许可发布