.NET Core中如何为每家公司注册并注入字段相同的多份AppDbContext
问题根因
你遇到的报错本质是DbContext不是线程安全类型,你之前用静态字典存储全局单例的ApplicationDbContext实例,多请求并发操作同一个实例就会触发该异常,同时绕开DI容器手动创建实例也会导致依赖注入、生命周期管理等一系列问题。
推荐实现方案
我们采用「动态构造租户DbContext + 启动时批量迁移初始化」的方案,完全适配DI规则,避免并发问题:
步骤1:实现租户DbContext提供器
定义接口用于按公司ID获取对应数据库的ApplicationDbContext实例,每次调用返回独立的新实例:
public interface ITenantDbContextProvider { ApplicationDbContext GetDbContext(string companyId); } public class TenantDbContextProvider : ITenantDbContextProvider { private readonly BaseDbContext _baseDbContext; private readonly ICurrentUserService _currentUserService; private readonly IDateTime _dateTime; // 可选:注入IMemoryCache缓存连接字符串,避免频繁查询基础库 private readonly IMemoryCache _cache; public TenantDbContextProvider(BaseDbContext baseDbContext, ICurrentUserService currentUserService, IDateTime dateTime, IMemoryCache cache) { _baseDbContext = baseDbContext; _currentUserService = currentUserService; _dateTime = dateTime; _cache = cache; } public ApplicationDbContext GetDbContext(string companyId) { // 从缓存取连接字符串,不存在则查基础库,缓存1小时 var connStr = _cache.GetOrCreate($"CompanyConnStr:{companyId}", entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); return _baseDbContext.BaseCompanies .FirstOrDefault(c => c.Id == companyId)? .ConnectionString; }); if (string.IsNullOrEmpty(connStr)) { throw new InvalidOperationException($"未找到ID为{companyId}的公司数据库配置"); } // 动态构造DbContext配置 var options = new DbContextOptionsBuilder<ApplicationDbContext>() .UseNpgsql(connStr, b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)) .Options; // 返回全新的DbContext实例,无并发冲突 return new ApplicationDbContext(options, _currentUserService, _dateTime); } }
步骤2:注册提供器到DI容器
在Startup.cs的ConfigureServices方法中添加注册:
// 租户DbContext提供器注册为Scoped,和请求生命周期绑定 services.AddScoped<ITenantDbContextProvider, TenantDbContextProvider>();
步骤3:实现启动时自动迁移/初始化租户库
写后台托管服务,服务启动时遍历所有公司执行迁移和数据播种,和请求链路隔离:
public class TenantDbInitHostedService : IHostedService { private readonly IServiceScopeFactory _scopeFactory; public TenantDbInitHostedService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public async Task StartAsync(CancellationToken cancellationToken) { // 单独创建Scope获取服务,避免和请求生命周期冲突 using var scope = _scopeFactory.CreateScope(); var provider = scope.ServiceProvider; var baseDb = provider.GetRequiredService<BaseDbContext>(); var currentUserService = provider.GetRequiredService<ICurrentUserService>(); var dateTime = provider.GetRequiredService<IDateTime>(); var allCompanies = await baseDb.BaseCompanies.ToListAsync(cancellationToken); foreach (var company in allCompanies) { var options = new DbContextOptionsBuilder<ApplicationDbContext>() .UseNpgsql(company.ConnectionString, b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)) .Options; // 每个公司的初始化用独立的DbContext实例,用完即释放 using var tenantDb = new ApplicationDbContext(options, currentUserService, dateTime); await tenantDb.Database.MigrateAsync(cancellationToken); await new ApplicationDbContextSeed(tenantDb).SeedRegistryAsync(); } } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }
同样在Startup.cs中注册托管服务:
services.AddHostedService<TenantDbInitHostedService>();
使用方式
在你的业务服务中直接注入ITenantDbContextProvider,根据当前请求的用户companyId获取对应DbContext即可:
public class YourBusinessService { private readonly ITenantDbContextProvider _tenantDbProvider; public YourBusinessService(ITenantDbContextProvider tenantDbProvider) { _tenantDbProvider = tenantDbProvider; } public async Task DoSomething(string currentUserCompanyId) { using var db = _tenantDbProvider.GetDbContext(currentUserCompanyId); // 正常操作DbContext即可 var data = await db.YourTable.FirstOrDefaultAsync(); } }
方案优势
- 每次请求获取的都是独立的ApplicationDbContext实例,完全避免多线程并发冲突
- 所有依赖均从DI容器获取,符合.NET Core依赖注入规范
- 连接字符串可选缓存,性能开销极低
- 租户库初始化和请求链路完全隔离,不影响接口响应速度
内容的提问来源于stack exchange,提问作者Mohammed Babełły
相关产品推荐
相关产品推荐

