如何为多租户场景动态应用EF Core迁移至多个数据库
多租户.NET Core API运行时自动应用数据库迁移方案
核心结论:不要在ApiDbContext构造函数中执行迁移
构造函数无法处理异步操作,且每次请求都会创建DbContext实例,会导致迁移重复执行,甚至引发服务初始化、作用域依赖的问题。下面是两种可行的编程实现方案:
1. 封装租户迁移服务
先创建专门的迁移执行服务,统一处理迁移逻辑:
public interface ITenantMigrationService { Task ApplyMigrationsAsync(string connectionString); } public class TenantMigrationService : ITenantMigrationService { public async Task ApplyMigrationsAsync(string connectionString) { // 针对当前租户连接字符串构建DbContext选项 var dbOptions = new DbContextOptionsBuilder<ApiDbContext>() .UseSqlServer(connectionString) .Options; using var tenantContext = new ApiDbContext(dbOptions); // 自动应用所有未执行的迁移 await tenantContext.Database.MigrateAsync(); } }
在服务注册中添加这个作用域服务:
// Program.cs/Startup.cs services.AddScoped<ITenantMigrationService, TenantMigrationService>();
2. 选择合适的执行时机
方案一:创建租户时立即执行(推荐)
如果有创建租户的API接口,在生成租户连接字符串、创建数据库后直接调用迁移服务,避免后续请求重复检查:
[ApiController] [Route("api/tenants")] public class TenantController : ControllerBase { private readonly ITenantMetaDataService _tenantMetaService; private readonly ITenantMigrationService _migrationService; public TenantController(ITenantMetaDataService tenantMetaService, ITenantMigrationService migrationService) { _tenantMetaService = tenantMetaService; _migrationService = migrationService; } [HttpPost] public async Task<IActionResult> CreateTenant([FromBody] TenantCreateDto dto) { // 1. 生成该租户的专属连接字符串 var tenantConnStr = _tenantMetaService.GenerateConnectionString(dto.TenantId); // 2. 可选:先创建空数据库(如果数据库不存在) await CreateTenantDatabaseIfNotExists(tenantConnStr); // 3. 立即应用迁移 await _migrationService.ApplyMigrationsAsync(tenantConnStr); return Ok(new { TenantId = dto.TenantId }); } // 辅助方法:创建数据库(基于master库执行创建命令) private async Task CreateTenantDatabaseIfNotExists(string connectionString) { var connBuilder = new SqlConnectionStringBuilder(connectionString); var dbName = connBuilder.InitialCatalog; connBuilder.InitialCatalog = "master"; using var masterConn = new SqlConnection(connBuilder.ToString()); await masterConn.OpenAsync(); var createCmd = masterConn.CreateCommand(); createCmd.CommandText = $"IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = N'{dbName}') CREATE DATABASE [{dbName}]"; await createCmd.ExecuteNonQueryAsync(); } }
方案二:首次请求租户时自动执行
如果租户数据库可能由外部系统创建,无法在租户创建时触发迁移,可以通过中间件在每个请求前检查并执行:
public class TenantMigrationMiddleware { private readonly RequestDelegate _next; // 可选:用内存缓存记录已迁移的租户ID,避免重复检查 private readonly IMemoryCache _cache; private const string MigratedTenantsKey = "MigratedTenants"; public TenantMigrationMiddleware(RequestDelegate next, IMemoryCache cache) { _next = next; _cache = cache; } public async Task InvokeAsync(HttpContext context, ITenantMetaDataService tenantMetaService, ITenantMigrationService migrationService) { var tenantId = tenantMetaService.GetTenantId(); var connStr = tenantMetaService.GetConnectionString(); // 检查缓存:如果该租户已迁移,直接跳过 var migratedTenants = _cache.GetHashSet<string>(MigratedTenantsKey) ?? new HashSet<string>(); if (!migratedTenants.Contains(tenantId)) { var dbOptions = new DbContextOptionsBuilder<ApiDbContext>() .UseSqlServer(connStr) .Options; using var tenantContext = new ApiDbContext(dbOptions); // 仅当有未执行的迁移时才执行 if (tenantContext.Database.GetPendingMigrations().Any()) { await migrationService.ApplyMigrationsAsync(connStr); migratedTenants.Add(tenantId); _cache.Set(MigratedTenantsKey, migratedTenants); } } await _next(context); } } // 注册中间件(放在租户识别中间件之后) // Program.cs app.UseMiddleware<TenantMigrationMiddleware>();
内容的提问来源于stack exchange,提问作者RedRose
相关产品推荐
相关产品推荐

