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?
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));
}
}
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>();