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

ASP.NET Core 7中IConfiguration与DI的循环依赖问题求解

问题

我有一个用C#编写的ASP.NET Core 7 MVC应用,希望构建自定义IConfiguration并添加到已注册的配置提供者中。
我希望使用DI服务集合中已注册的服务实现该配置提供者,而这些服务通常依赖现有IConfiguration的配置值。
我需要借助由现有IConfiguration配置的服务初始化新的IConfiguration,再用新配置提供者配置更多要添加到服务集合的服务。

例如,我可能需要添加基于仓储的IConfiguration,它需要仓储连接字符串,其他服务配置也存储在该仓储中。该仓储依赖EF等访问服务及其他组件。不使用DI时我会编写如下代码:

int attempts = Configuration["attempts"];
string connectionString = Configuration["repository"];
IRetry retry = new Retry(attempts);
IConnect connect = new Connect(connectionString, retry);
IRead read = new Read();
IStore store = new Store(connect, read); // 包含敏感配置
IConfiguration moreConfiguration = new MoreConfiguration(store);
builder.Configuration.Add(moreConfiguration);
string secret = Configuration["secret"];
IService finalService = new Service(secret);
finalService.DoSomething(...);

我曾尝试实例化“临时”服务提供者,用其服务构建配置提供者并添加到已注册配置中,再添加更多服务,但这并非理想方案,尤其是存在单例和作用域服务时:

int attempts = Configuration["attempts"];
string connectionString = Configuration["repository"];
builder.Services
    .AddSingleton<IRetry>(_ => new Retry(attempts))
    .AddSingleton<IConnect>(p =>
        {
               IRetry r = p.GetRequiredService<IRetry>();
               return new Connect(connectionString, r);
        })
    .AddSingleton<IRead, Read>()
    .AddSingleton<IStore>(p =>
        {
            IConnect c = p.GetRequiredService<IConnect>();
            IRead r = p.GetRequiredService<IRead>();
            return new Store(c, r);
        });
ServiceProvider intermediate = builder.Services.BuildServiceProvider();
IStore store = intermediate.GetRequiredService<IStore>();
IConfiguration moreConfiguration = new MoreConfiguration(store);
builder.Configuration.Add(moreConfiguration);
string secret = Configuration["secret"];
builder.Services.AddTransient<IService>(_ => new Service(secret));
ServiceProvider final = builder.Services.BuildServiceProvider();
IService finalService = final.GetRequiredService<IService>();
finalService.DoSomething(...);

是否存在合理方式,既能使用服务提供者,又能添加更多服务后再次使用它?还是我的思路有误?

解决方案

你的核心问题是在配置构建流程中依赖DI服务,同时还要让后续服务能使用新添加的配置,构建临时ServiceProvider确实会带来单例重复注册、作用域服务失效等问题,推荐以下两种更规范的方式:

1. 自定义IConfigurationSource,延迟依赖解析

ASP.NET Core的配置系统支持自定义IConfigurationSource和IConfigurationProvider,你可以在提供者中延迟获取DI服务,避免提前构建临时容器。

实现步骤:

  • 定义自定义配置源,持有服务集合的引用:
public class RepositoryConfigurationSource : IConfigurationSource
{
    private readonly IServiceCollection _services;
    public RepositoryConfigurationSource(IServiceCollection services)
    {
        _services = services;
    }

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new RepositoryConfigurationProvider(_services);
    }
}
  • 实现配置提供者,在首次读取配置时获取最终构建的根服务提供者:
public class RepositoryConfigurationProvider : ConfigurationProvider
{
    private readonly IServiceCollection _services;
    private IServiceProvider _rootProvider;

    public RepositoryConfigurationProvider(IServiceCollection services)
    {
        _services = services;
    }

    // 空实现Load方法,延迟到首次获取配置时初始化
    public override void Load() { }

    public override bool TryGet(string key, out string value)
    {
        // 首次获取时构建/获取根服务提供者
        _rootProvider ??= _services.BuildServiceProvider();
        
        var store = _rootProvider.GetRequiredService<IStore>();
        // 从仓储读取目标配置值
        value = store.GetConfigurationValue(key);
        return !string.IsNullOrEmpty(value);
    }
}
  • 扩展IConfigurationBuilder方便注册自定义源:
public static class ConfigurationBuilderExtensions
{
    public static IConfigurationBuilder AddRepositoryConfiguration(this IConfigurationBuilder builder, IServiceCollection services)
    {
        return builder.Add(new RepositoryConfigurationSource(services));
    }
}
  • 在Program.cs中使用:
// 1. 先注册依赖初始配置的服务
int attempts = builder.Configuration.GetValue<int>("attempts");
string connectionString = builder.Configuration["repository"];
builder.Services
    .AddSingleton<IRetry>(_ => new Retry(attempts))
    .AddSingleton<IConnect>(p => new Connect(connectionString, p.GetRequiredService<IRetry>()))
    .AddSingleton<IRead, Read>()
    .AddSingleton<IStore>(p => new Store(p.GetRequiredService<IConnect>(), p.GetRequiredService<IRead>()));

// 2. 添加自定义配置源
builder.Configuration.AddRepositoryConfiguration(builder.Services);

// 3. 注册依赖新配置的服务(配置系统会在首次需要时自动从仓储读取)
builder.Services.AddTransient<IService>(sp => 
{
    var config = sp.GetRequiredService<IConfiguration>();
    string secret = config["secret"];
    return new Service(secret);
});

// 4. 构建最终服务提供者并运行应用
var app = builder.Build();
// ... 中间件配置
app.Run();

这种方式的优势:

  • 无需构建临时容器,单例服务仅初始化一次
  • 配置读取延迟执行,确保获取配置时根服务提供者已完全构建,所有依赖均可用

2. 利用IHostedService延迟初始化(适用于非启动必需的配置)

如果仓储配置不需要在应用启动阶段立即读取(比如不需要在中间件或启动逻辑中使用),可以将配置加载逻辑放到IHostedService中,在应用启动完成后再读取仓储配置并更新到现有配置系统。

示例代码:

public class RepositoryConfigurationLoader : IHostedService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IConfigurationRoot _configuration;

    public RepositoryConfigurationLoader(IServiceProvider serviceProvider, IConfigurationRoot configuration)
    {
        _serviceProvider = serviceProvider;
        _configuration = configuration;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // 创建作用域获取仓储服务
        using var scope = _serviceProvider.CreateScope();
        var store = scope.ServiceProvider.GetRequiredService<IStore>();
        
        // 读取仓储中的所有配置项
        var repositoryConfigs = store.GetAllConfigurations();
        // 将配置项添加到现有配置系统
        foreach (var kvp in repositoryConfigs)
        {
            _configuration.GetSection(kvp.Key).Value = kvp.Value;
        }

        // 触发配置重载通知,让依赖配置的服务感知变更
        _configuration.Reload();
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

在Program.cs中注册:

// 1. 注册依赖初始配置的服务
// ... 同之前的服务注册代码

// 2. 注册HostedService负责加载仓储配置
builder.Services.AddHostedService<RepositoryConfigurationLoader>();

// 3. 注册依赖配置的服务(注意:若服务在启动时就需要读取配置,此方式不适用)
builder.Services.AddTransient<IService>(sp => 
{
    var config = sp.GetRequiredService<IConfiguration>();
    string secret = config["secret"];
    return new Service(secret);
});

原有思路的问题

多次调用BuildServiceProvider()会创建多个独立的服务容器,单例服务会被重复实例化导致状态不一致;作用域服务在临时容器中创建后无法在最终容器复用,容易引发资源泄漏和逻辑错误。上述两种方案均避免了多次构建容器的问题,符合ASP.NET Core的设计规范。


内容的提问来源于stack exchange,提问作者Skip Saillors

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 08:27:02