You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何访问ConfigurationProvider初始化的容器与HttpClient用于依赖注入?

问题:如何在WebApplicationFactory中访问自定义ConfigurationProvider内的TestContainer/HttpClient

背景

基于testcontainers-dotnet实现了自定义ConfigurationProvider,用于配置并启动测试容器。目前卡在测试的依赖注入配置阶段——需要获取Provider初始化的HttpClient,但不清楚如何让它对外可见。

流程必须按以下顺序执行,若能让Provider中的容器或HttpClient被WebApplicationFactory访问,步骤2、3可移至Provider外完成:

  1. Provider配置并启动TestContainer
  2. 从已启动的容器创建HttpClient
  3. 将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中获取:

  1. 定义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());
    }
}
  1. 在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中获取:

  1. 修改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;
    }
}
  1. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 09:10:57