如何访问ConfigurationProvider初始化的容器与HttpClient用于依赖注入?
问题:如何在WebApplicationFactory中访问自定义ConfigurationProvider内的TestContainer/HttpClient
背景
基于testcontainers-dotnet实现了自定义ConfigurationProvider,用于配置并启动测试容器。目前卡在测试的依赖注入配置阶段——需要获取Provider初始化的HttpClient,但不清楚如何让它对外可见。
流程必须按以下顺序执行,若能让Provider中的容器或HttpClient被WebApplicationFactory访问,步骤2、3可移至Provider外完成:
- Provider配置并启动TestContainer
- 从已启动的容器创建HttpClient
- 将HttpClient注入为依赖
WireMock工作示例
以下是该方案在WireMock模块中正常运行的精简代码,能启动容器、向测试用WireMock客户端发送映射,还可通过ConfigurationProvider的Set()方法覆盖appsettings默认值:
WireMockConfigurationSource
public class WireMockConfigurationSource : IConfigurationSource { public IList<MappingModel> Mappings { get; } public WireMockConfigurationSource(IList<MappingModel> mappings) { Mappings = mappings; } public IConfigurationProvider Build(IConfigurationBuilder builder) => new WireMockConfigurationProvider(this); }
WireMockConfigurationProvider(核心逻辑,省略IDisposable相关代码)
public class WireMockConfigurationProvider : ConfigurationProvider { private static readonly TaskFactory TaskFactory = new(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default); private readonly WireMockContainer _container; private readonly WireMockConfigurationSource _source; private CancellationTokenSource? _cts; public WireMockConfigurationProvider(WireMockConfigurationSource source) { _source = source; _container = new WireMockContainerBuilder() .WithAutoRemove(true) .WithCleanUp(true) .Build(); } public override void Load() { if (_cts is not null) { return; } _cts = new CancellationTokenSource(); TaskFactory.StartNew(() => LoadAsync(_cts.Token), _cts.Token).Unwrap().ConfigureAwait(false) .GetAwaiter() .GetResult(); } public async Task LoadAsync(CancellationToken ct) { // 启动容器 await _container.StartAsync(ct); // 将容器URL写入配置 SetAppSettingsForWireMockedComponent(); // 给容器初始化测试用映射数据 await InitializeMappings(ct); } private async Task InitializeMappings(CancellationToken ct) { var wireMockAdminClient = _container.CreateWireMockAdminClient(); await wireMockAdminClient.PostMappingsAsync(_source.Mappings, ct); } private void SetAppSettingsForWireMockedComponent() { var containerUrl = _container.GetPublicUrl().TrimEnd('/'); Set("MyAppSetting:ApiUrl", containerUrl); } }
WebApplicationFactory中使用Provider
public class ApiFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureAppConfiguration(configure => configure.Add(new WireMockConfigurationSource())); builder.ConfigureServices( (context, services) => { // 测试DI配置 // 此处需要注入HttpClient,但无法获取Provider内的容器实例 }); } }
CosmosDB场景需求
希望复用这套方案到CosmosDB模块,但需要在Provider外部访问其中启动的容器。原因是容器会生成自带信任证书的HttpClient,使用它向CosmosDB模拟器发送请求才能通过授权:
var cosmosClient = new CosmosClient( // 模拟器连接字符串可通过Set()写入配置,外部能正常获取 _container.GetConnectionString(), new CosmosClientOptions { // 容器创建的HttpClient自动信任模拟器证书,无此实例请求会返回401 // 需要在Provider外部拿到该HttpClient来创建CosmosClient并注入DI HttpClientFactory = () => _container.HttpClient, ConnectionMode = ConnectionMode.Gateway, });
解决方案
方案1:给ConfigurationProvider添加公开属性
修改CosmosDB的ConfigurationProvider,将容器实例公开,直接在WebApplicationFactory中获取:
- 定义CosmosDB的ConfigurationSource和Provider:
public class CosmosDbConfigurationSource : IConfigurationSource { public IConfigurationProvider Build(IConfigurationBuilder builder) => new CosmosDbConfigurationProvider(this); } public class CosmosDbConfigurationProvider : ConfigurationProvider { private static readonly TaskFactory TaskFactory = new(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default); // 公开容器实例供外部访问 public CosmosDbContainer Container { get; private set; } private readonly CosmosDbConfigurationSource _source; private CancellationTokenSource? _cts; public CosmosDbConfigurationProvider(CosmosDbConfigurationSource source) { _source = source; Container = new CosmosDbBuilder() .WithAutoRemove(true) .WithCleanUp(true) .Build(); } public override void Load() { if (_cts is not null) return; _cts = new CancellationTokenSource(); TaskFactory.StartNew(() => LoadAsync(_cts.Token), _cts.Token).Unwrap().ConfigureAwait(false) .GetAwaiter().GetResult(); } public async Task LoadAsync(CancellationToken ct) { await Container.StartAsync(ct); // 将连接字符串写入配置 Set("CosmosDb:ConnectionString", Container.GetConnectionString()); } }
- 在WebApplicationFactory中使用:
public class ApiFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { private CosmosDbConfigurationProvider _cosmosDbProvider; protected override void ConfigureWebHost(IWebHostBuilder builder) { var cosmosSource = new CosmosDbConfigurationSource(); _cosmosDbProvider = (CosmosDbConfigurationProvider)cosmosSource.Build(null); builder.ConfigureAppConfiguration(configure => configure.Add(cosmosSource)); builder.ConfigureServices( (context, services) => { // 从Provider获取容器,创建CosmosClient并注入DI var cosmosClient = new CosmosClient( context.Configuration["CosmosDb:ConnectionString"], new CosmosClientOptions { HttpClientFactory = () => _cosmosDbProvider.Container.HttpClient, ConnectionMode = ConnectionMode.Gateway, }); // 替换原有CosmosClient注册 services.RemoveAll<CosmosClient>(); services.AddSingleton(cosmosClient); }); } }
方案2:使用共享存储传递实例
若不想直接持有Provider实例,可在ConfigurationProvider加载完成后,将容器实例存入共享存储(如静态字典),再在WebApplicationFactory中获取:
- 修改CosmosDbConfigurationProvider:
public class CosmosDbConfigurationProvider : ConfigurationProvider { // 静态字典存储容器实例,用唯一ID区分不同测试实例 public static Dictionary<string, CosmosDbContainer> Containers { get; } = new(); private readonly string _instanceId; private CosmosDbContainer _container; private readonly CosmosDbConfigurationSource _source; private CancellationTokenSource? _cts; public CosmosDbConfigurationProvider(CosmosDbConfigurationSource source, string instanceId) { _source = source; _instanceId = instanceId; _container = new CosmosDbBuilder() .WithAutoRemove(true) .WithCleanUp(true) .Build(); } public async Task LoadAsync(CancellationToken ct) { await _container.StartAsync(ct); Set("CosmosDb:ConnectionString", _container.GetConnectionString()); // 将容器存入静态字典 Containers[_instanceId] = _container; } }
- 在WebApplicationFactory中使用:
public class ApiFactory<TProgram> : WebApplicationFactory<TProgram> where TProgram : class { private readonly string _instanceId = Guid.NewGuid().ToString(); protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureAppConfiguration(configure => configure.Add(new CosmosDbConfigurationSource(_instanceId))); builder.ConfigureServices( (context, services) => { // 从静态字典获取容器实例 if (CosmosDbConfigurationProvider.Containers.TryGetValue(_instanceId, out var container)) { var cosmosClient = new CosmosClient( context.Configuration["CosmosDb:ConnectionString"], new CosmosClientOptions { HttpClientFactory = () => container.HttpClient, ConnectionMode = ConnectionMode.Gateway, }); services.RemoveAll<CosmosClient>(); services.AddSingleton(cosmosClient); } }); } }
注意事项
- 方案1更直接,适合单测试实例场景;方案2适合多测试并行场景,避免实例冲突。
- 务必实现IDisposable接口,在测试结束后销毁容器、释放资源。
- Load()方法中同步调用异步代码的写法需注意死锁问题,当前TaskFactory的实现是安全的,也可使用成熟的异步同步转换工具类。
内容的提问来源于stack exchange,提问作者Amicable
相关产品推荐
相关产品推荐

