Testcontainers启动SQL Server容器但无法创建数据库与执行迁移
SQL Server Testcontainers 无法创建数据库及执行EF迁移的解决方法
问题现象
使用Testcontainers启动SQL Server容器做集成测试时,容器能正常启动,但无法自动创建数据库及执行EF Core迁移。手动创建数据库后,迁移操作仍然失败。
代码问题分析
- 未等待SQL Server完全就绪:启动容器后直接执行数据库创建和迁移,此时SQL Server可能还未完成初始化
- 迁移时未指定目标数据库:原
ApplyMigrations方法使用连接master库的字符串,导致迁移不会作用到新建的NewDb - 数据库创建后未等待初始化完成:SQL Server创建数据库后需要短暂时间完成启动,直接迁移可能失败
- 不必要的端口绑定:硬绑定1433端口可能导致本地端口冲突,Testcontainers会自动处理端口映射
修复后的代码
public class CustomApiFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime { private const string Database = "NewDb"; private readonly MsSqlContainer _dbContainer; public CustomApiFactory() { _dbContainer = new MsSqlBuilder() .WithImage("mcr.microsoft.com/mssql/server:2022-CU10-ubuntu-22.04") .WithPassword("Password123") .WithCleanUp(true) .Build(); } public async Task InitializeAsync() { await _dbContainer.StartAsync(); // 先等待SQL Server服务完全就绪 var masterConnectionString = _dbContainer.GetConnectionString(); await WaitForSqlServerToBeReady(masterConnectionString); // 创建目标数据库 await CreateDatabaseIfNotExists(masterConnectionString); // 使用目标数据库的连接字符串执行迁移 var targetConnectionString = $"{masterConnectionString};Database={Database}"; // 等待新数据库初始化完成 await WaitForSqlServerToBeReady(targetConnectionString); await ApplyMigrations(targetConnectionString); } public new async Task DisposeAsync() { await _dbContainer.StopAsync(); } protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment("Test"); builder.ConfigureServices(services => { services.AddTestAuthentication(); }); builder.ConfigureLogging(logging => { logging.ClearProviders(); }); builder.ConfigureTestServices(services => { services.RemoveAll(typeof(DbContextOptions<ApplicationContext>)); services.AddDbContext<ApplicationContext>(opts => { opts.UseSqlServer($"{_dbContainer.GetConnectionString()};Database={Database}"); }); }); } private async Task CreateDatabaseIfNotExists(string masterConnectionString) { using (var conn = new SqlConnection(masterConnectionString)) { await conn.OpenAsync(); var cmd = conn.CreateCommand(); // 使用参数化查询避免SQL注入风险 cmd.CommandText = "IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = @dbName) CREATE DATABASE [@dbName]"; cmd.Parameters.AddWithValue("@dbName", Database); await cmd.ExecuteNonQueryAsync(); } } private async Task WaitForSqlServerToBeReady(string connectionString) { var retryCount = 10; for (int i = 0; i < retryCount; i++) { try { using (var conn = new SqlConnection(connectionString)) { await conn.OpenAsync(); // 执行简单查询验证数据库是否可用 using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT 1"; await cmd.ExecuteScalarAsync(); Console.WriteLine($"数据库连接就绪: {connectionString}"); return; } } catch (SqlException) { Console.WriteLine("数据库未就绪,5秒后重试..."); await Task.Delay(5000); } } throw new Exception("数据库超时未就绪"); } private async Task ApplyMigrations(string targetConnectionString) { var optionsBuilder = new DbContextOptionsBuilder<ApplicationContext>(); optionsBuilder.UseSqlServer(targetConnectionString); using (var context = new ApplicationContext(optionsBuilder.Options)) { try { await context.Database.MigrateAsync(); Console.WriteLine("迁移执行成功"); } catch (Exception ex) { Console.WriteLine($"迁移失败: {ex.Message}"); throw; } } } }
关键修改说明
- 添加服务就绪等待:启动容器后先等待SQL Server服务可用,再执行后续操作
- 迁移指定目标数据库:迁移时使用包含
NewDb的连接字符串,确保迁移作用到目标库 - 数据库创建后等待:新建数据库后再次调用等待方法,确保数据库完成初始化
- 移除硬绑定端口:删除
WithPortBinding(1433),避免端口冲突 - SQL注入防护:创建数据库时使用参数化查询,替代字符串拼接
Docker日志说明
从日志可以看到NewDb已被成功创建并启动,但之前的迁移未作用到该库,修复后迁移会正确在NewDb中创建Schema和表结构。
内容的提问来源于stack exchange,提问作者Leviathan
相关产品推荐
相关产品推荐

