How to configure options sometime later at runtime?

Viewed 1547

I have a .Net 5 Web Api project and want to configure options with some logic. That's why I would like to move the configuration out from the Startup.ConfigureServices method and put them into their own service configurator (running during startup). So I came up with this

class OptionsConfigurator : IHostedService
{
    private readonly IServiceCollection _serviceCollection;

    public StartupOptionsConfigurator(IServiceCollection serviceCollection)
    {
        _serviceCollection = serviceCollection;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        /*

            Build the ServiceProvider from the _serviceCollection
            Get the IConfiguration service from the ServiceProvider
            Read the configurationSection from the IConfiguration
            call _serviceCollection.Configure<MyOptions>(configurationSection);

        */

        return Task.CompletedTask;
    }

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

This won't work because I can't inject IServiceCollection to the constructor. Is there a way to configure those options without the need of an instance of IServiceCollection?

I can provide another example showing the problem:

Your Api provides a single endpoint to update a specific configuration to change it during runtime (maybe change the database connection string for some reasons).

  • Create a new Web Api project
  • Create an options class

.

public class MyOptions
{
    public string Text { get; set; }
}
  • Create a controller

.

[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
    private readonly IOptionsMonitor<MyOptions> _myOptionsMonitor;
    private readonly IServiceCollection _serviceCollection;
    
    public MyController(IOptionsMonitor<MyOptions> myOptionsMonitor, IServiceCollection serviceCollection)
    {
        _myOptionsMonitor = myOptionsMonitor;
        _serviceCollection = serviceCollection;
    }

    [HttpPatch]
    public IActionResult UpdateOptions([FromBody] string text)
    {
        Console.WriteLine("Before: " + JsonSerializer.Serialize(_myOptionsMonitor.CurrentValue));

        _serviceCollection.Configure<MyOptions>(x => { x.Text = text; });

        string newConfig = JsonSerializer.Serialize(_myOptionsMonitor.CurrentValue);
        Console.WriteLine("After: " + newConfig);
        
        return Ok(newConfig);
    }
}

Call the patch endpoint with a new configuration string in the request body. As expected this doesn't work neither, the injection of IServiceCollection is not possible and throws an exception.

Are there any solutions on how to configure options later on?

2 Answers

Reconfiguring options using IServiceCollection.Configure is not possible after initialization because BuildServiceProvider has already been called. You cannot configure or add any service outside program initialiazation.

To change configuration values you just need to update appsettings.json file in UpdateOptions action and to use IOptionsMonitor everywhere.

When configuring configuration you can specify to reload the configuration file when it changes. In Program.CreateHostBuilder:

public static IHostBuilder CreateHostBuilder(string[] args)
{
    return Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .ConfigureAppConfiguration((hostingConext, configurationBuilder) =>
        {
            configurationBuilder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
            configurationBuilder.AddJsonFile($"appsettings.{hostingConext.HostingEnvironment.EnvironmentName}.json", optional: false, reloadOnChange: true);
            configurationBuilder.AddUserSecrets(Assembly.GetExecutingAssembly(), true, true);
        });
}

In this way IOptionsMonitor<TOptions>.CurrentValue will always return the value stored in the configuration file even if it changes.

It sounds like you should implement your own ConfigurationProvider. This would allow you to react to changes in your configuration and then you use IOptionsMonitor (for singleton services) or IOptionsSnapshot (for transient or scoped services) to read the values.

There is a sample implementation for a database based configuration provider over at docs.microsoft.com. That would be fairly easy to change to something where you store the values in memory (or wherever you want if you want to persist changes) and then you could expose this to your controller.

Related