Shamshiel
Shamshiel

Reputation: 2211

Reloading Options with reloadOnChange in ASP.NET Core

In my ASP.NET Core application I bind the appsettings.json to a strongly typed class AppSettings.

public Startup(IHostingEnvironment environment)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(environment.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
        .AddJsonFile($"appsettings.{environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
        .AddEnvironmentVariables();

    Configuration = builder.Build();
}

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<AppSettings>(Configuration);
    //...
}

In a singleton class I wrap this AppSettings class like this:

public class AppSettingsWrapper : IAppSettingsWrapper
{
    private readonly IOptions<AppSettings> _options;

    public AppSettingsAdapter(IOptions<AppSettings> options)
    {
        _options = options ?? throw new ArgumentNullException("Options cannot be null");
    }

    public SomeObject SomeConvenienceGetter()
    {
        //...
    }
}

Now I'm struggling with reloading the AppSettings if the json file changes. I read somewhere that the class IOptionsMonitor can detect changes but it doesn't work in my case.

I tried calling the OnChange event like this for testing purposes:

public void Configure(IApplicationBuilder applicationBuilder, IOptionsMonitor<AppSettings> optionsMonitor)
{
    applicationBuilder.UseStaticFiles();
    applicationBuilder.UseMvc();

    optionsMonitor.OnChange<AppSettings>(vals => 
    {
        System.Diagnostics.Debug.WriteLine(vals);
    });
}

The event is never triggered when I change the json file. Has someone an idea what I can change to get the reloading mechanic to work in my scenario?

Upvotes: 15

Views: 33030

Answers (3)

D.R.
D.R.

Reputation: 21224

If you want to have a singleton IHostedService which is, e.g., registered as singleton and the Execute method is triggered repeatedly by a timer + you want a new IOptionsSnapshot on each trigger, you can use the following code:

public MyHostedService (IServiceProvider serviceProvider)
{
    _serviceProvider = serviceProvider;
}

private void ExecutedByTimer ()
{
    using var scope = _serviceProvider.CreateScope();
    var options = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<MyHostedServiceOptions>>().Value;
    // Voila, options is an up-to-date snapshot
}

Upvotes: 1

Bjorn Bailleul
Bjorn Bailleul

Reputation: 3265

What you can do is create your wrapper class around the config class like you did in AppSettingsWrapper and inject IOptionsMonitor. Then keep a private property of your settings class. That wrapper can be injected as a singleton and the IOptionsMonitor will keep track of your changes.

public class AppSettingsWrapper
{
    private AppSettings _settings;

    public AppSettingsWrapper(IOptionsMonitor<AppSettings> settings)
    {
        _settings = settings.CurrentValue;

        // Hook in on the OnChange event of the monitor
        settings.OnChange(Listener);
    }

    private void Listener(AppSettings settings)
    {
        _settings = settings;
    }

    // Example getter
    public string ExampleOtherApiUrl => _settings.ExampleOtherApiUrl;
}

Then register your wrapper class as a singleton

services.AddSingleton(sp => new AppSettingsWrapper(sp.GetService<IOptionsMonitor<AppSettings>>()));

Upvotes: 19

Simply Ged
Simply Ged

Reputation: 8672

You need to inject IOptionsSnapshot<AppSettings> to get the reload working.

Unfortunately you cannot load the IOptionsSnapshot into a Singleton service. IOptionsSnapshot is a Scoped service so you can only reference it in a Scoped or Transient registered class.

But, if think about it, that makes sense. The settings need to be reloaded when they change so if you inject them into a Singleton then the class will never get the updated settings because the constructor will not be called again for a Singleton.

Upvotes: 8

Related Questions