Search code examples
c#dependency-injectionasp.net-core-mvcinversion-of-control.net-7.0

IConfiguration DI chicken-and-egg question


I have an ASP.NET Core 7 MVC app written in C#. I want to build a custom IConfiguration to add to the providers already registered.

I would like to use some services registered in the DI service collection to implement some of the configuration provider. These services would typically take values from an IConfiguration provider.

I want to bootstrap the new IConfiguration using services that are configured using information from existing IConfiguration providers, and then I want to use the new IConfiguration provider to configure more services that I want to add to the services collection.

I might want to add a repository-based IConfiguration that needs a connection string to the repository, for example, and I might want to keep setting for other services in the repository. The repository needs some access service, like EF. but with other service dependencies. Without DI I might write:

int attempts = Confguration["attempts"];
string connectitonString = Configuration["repository"]
IRetry retry = new Retry(attempts);
IConnect connect = new Connect(connectionString, retry);
IRead read = new Read();
IStore store = new Store(connect, read); // has secret
IConfiguration moreConfiguration = new MoreConfiguration(store);
builder.Confguration.Add(moreConfiguration);
string secret = Configuration["secret"];
IService finally = new Service(secret);
finally.DoSomething(...);

Naively I could try to instantiate a "temporary" service provider, build the Configuration provider using services from the temporary, add it to the registered configs, then add more services. That doesn't seem a good idea to me, especially if I have singletons and scoped services.

int attempts = Confguration["attempts"];
string connectitonString = Configuration["repository"]
builder.Services
    .AddSingleton<IRetry>(_ => new Retry(attempts))
    .AddSingleton<IConnect>(p =>
        {
               IRetry r = p.GetRequiredService<IRetry>();
               return new Connect(connectionString, retry);
        })
    .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.Confguration.Add(moreConfiguration);
string secret = Configuration["secret"];
builder.Services.AddTransient<IService>(_ => new Service(secret));
ServiceProvider final = builder.Services.BuildServiceProvider();
IService finally = final.GetRequiredService<IService>();
finally.DoSomething(...);

Is there a way to really to use a service provider, then add more services, then use it again? Or am I crazy?


Solution

  • You can't access IServiceProvider without creating a temp instance, because actions added using ConfigureAppConfiguration are executed before the root service provider is created. But you can access information "added on Configuration by previous calls" to ConfigureAppConfiguration just by building the IConfigurationRoot from configurationBuilder.

    see the example below (also follow docs - example is taken from there):

    using CustomProvider.Example.Providers;
    
    namespace Microsoft.Extensions.Configuration;
    
    public static class ConfigurationBuilderExtensions
    {
        public static IConfigurationBuilder AddEntityConfiguration(
            this IConfigurationBuilder builder)
        {
            var tempConfig = builder.Build();
            var connectionString =
                tempConfig.GetConnectionString("WidgetConnectionString");
    
            return builder.Add(new EntityConfigurationSource(connectionString));
        }
    }
    

    Edit

    Since more information was provided in the question I will try to give you a concrete example of how you can achieve what you want without creating a temporary ServiceProvider... (example given has compilation error therefore I have modified it to work).

    First, you will need to create a ConfigurationProvider

    public class MoreConfigurationProvider : ConfigurationProvider
    {
        private readonly int _attempts;
        private readonly string _connectionString;
    
        public MoreConfigurationProvider(int attempts, string connectionString)
        {
            _attempts = attempts;
            _connectionString = connectionString;
        }
        
        public override void Load()
        {
            IRetry retry = new Retry(_attempts);
            IConnect connect = new Connect(_connectionString, retry);
            IRead read = new Read();
            IStore store = new Store(connect, read);
            
            // Simple version as per example
            Data.Add("secret", store.GetSecret());
        }
    }
    
    

    Then create a IConfigurationSource

    public class MoreConfigurationSource : IConfigurationSource
    {
        public int Attempts { get; set; }
        public string ConnectionString { get; set; }
    
        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new MoreConfigurationProvider(Attempts, ConnectionString);
        }
    }
    
    

    Change your Service class to accept IConfiguration (this is optional)

    public class Service : IService
    {
        public Service(IConfiguration configuration)
        {
            string sercert = configuration["secret"];
        }
    }
    

    Finally, change your program.cs

    int attempts = int.Parse(builder.Configuration["attempts"]);
    string connectionString = builder.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); 
        });
    
    builder.Configuration.Add<MoreConfigurationSource>(mc =>
    {
        mc.ConnectionString = connectionString;
        mc.Attempts = attempts;
    });
    
    builder.Services.AddTransient<IService>();