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

