blog post image
Andrew Lock avatar

Andrew Lock

~8 min read

Reloading strongly typed Options on file changes in ASP.NET Core RC2

Note: ASP.NET Core 1.1.0 introduces a new way to reload Options using IOptionsSnapshot. Check out my post on it here.

In the previous version of ASP.NET, configuration was typically stored in the <AppSettings> section of web.config. Touching the web.config file would cause the application to restart with the new settings. Generally speaking this worked well enough, but triggering a full application reload every time you want to tweak a setting can sometimes create a lot of friction during development.

ASP.NET Core has a new configuration system that is designed to aggregate settings from multiple sources, and expose them via strongly typed classes using the Options pattern. You can load your configuration from environment variables, user secrets, in memory collections json file types, or even your own custom providers.

When loading from files, you may have noticed the reloadOnChange parameter in some of the file provider extension method overloads. You'd be right in thinking that does exactly what it sounds - it reloads the configuration file if it changes. However, it probably won't work as you expect without some additional effort.

In this article I'll describe the process I went through trying to reload Options when appsettings.json changes. Note that the final solution is currently only applicable for RC2 - it has been removed from the RTM release, but will be back post-1.0.0.

Trying to reload settings

To demonstrate the default behaviour, I've created a simple ASP.NET Core WebApi project using Visual Studio. To this I have added a MyValues class:

public class MyValues
{
    public string DefaultValue { get; set; }
}

This is a simple class that will be bound to the configuration data, and injected using the options pattern into consuming classes. I bind the DefaultValues property by adding a Configure call in Startup.ConfigureServices :

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

    public void ConfigureServices(IServiceCollection services)
    {
        // Configure our options values
        services.Configure<MyValues>(Configuration.GetSection("MyValues"));
        services.AddMvc();
    }
}

I have included the configuration building step so you can see that appsettings.json is configured with reloadOnChange: true. Our MyValues class needs a default value, so I added the required configuration to appsettings.json:

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "MyValues": {
    "DefaultValue" : "first"
  }
}

Finally, the default ValuesController is updated to have an IOptions<MyValues> instance injected in to the constructor, and the Get action just prints out the DefaultValue.

[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly MyValues _myValues;
    public ValuesController(IOptions<MyValues> values)
    {
        _myValues = values.Value;
    }

    // GET api/values
    [HttpGet]
    public string Get()
    {
        return _myValues.DefaultValue;
    }
}

Debugging our application using F5, and navigating to http://localhost:5123/api/values, gives us the following output:

Chrome showing expected output

Perfect, so we know our values are being loaded and bound correctly. So what happens if we change appsettings.json? While still debugging, I updated the appsettings.json as below, and hit refresh in the browser…

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "MyValues": {
    "DefaultValue": "I'm new!"
  }
}

Chrome showing same output

Hmmm… That's the same as before… I guess it doesn't work.

Overview of configuration providers

Before we dig in to why this didn't work, and how to update it to give our expected behaviour, I'd like to take a step back to cover the basics of how the configuration providers work.

After creating a ConfigurationBuilder in our Startup class constructor, we can add a number of sources to it. These can be file-based providers, user secrets, environment variables or a wide variety of other sources. Once all your sources are added, a call to Build will cause each source's provider to load their configuration settings internally, and returns a new ConfigurationRoot.

This ConfigurationRoot contains a list of providers with the values loaded, and functions for retrieving particular settings. The settings themselves are stored internally by each provider in an IDictionary<string, string>. Considering the first appsettings.json in this post, once loaded the JsonConfigurationProvider would contain a dictionary similar to the following:

new Dictionary<string, string> {
  {"Logging:IncludeScopes": "Debug"},
  {"Logging:LogLevel:Default": "Debug"},
  {"Logging:LogLevel:System": "Information"}
  {"Logging:LogLevel:Microsoft": "Information"},
  {"MyValues:DefaultValue": first}
}

When retrieving a setting from the ConfigurationRoot, the list of sources is inspected in reverse to see if it has a value for the string key provided; if it does, it returns the value, otherwise the search continues up the stack of providers until it is found, or all providers have been searched.

Overview of model binding

Now we understand how the configuration values are built, let's take a quick look at how our IOptions<> instances get created. There are a number of gotchas to be aware of when model binding (I discuss some in a previous post/), but essentially it allows you to bind the flat string dictionary that IConfigurationRoot receives to simple POCO classes that can be injected.

When you setup one of your classes (e.g. MyValues above) to be used as an IOptions<> class, and you bind it to a configuration section, a number of things happen.

First of all, the binding occurs. This takes the ConfigurationRoot we were supplied previously, and interrogates it for settings which map to properties on the model. So, again considering the MyValues class, the binder first creates an instance of the class. It then uses reflection to loop over each of the properties in the class (in this case it only finds DefaultValue) and tries to populate it. Once all the properties that can be bound are set, the instantiated MyValues object is cached and returned.

Secondly, it configures the IoC dependency injection container to inject the IOptions<MyValues> class whenever it is required.

Exploring the reload problem

Lets recap. We have an appsettings.json file which is used to provide settings for an IOptions<MyValues> class which we are injecting into our ValuesController. The JSON file is configured with reloadOnChange: true. When we run the app, we can see the values load correctly initially, but if we edit appsettings.json then our injected IOptions<MyValues> object does not change.

Let's try and get to the bottom of this...

The reloadOnChange: true parameter

We need to establish at which point the reload is failing, so we'll start at the bottom of the stack and see if the configuration provider is noticing the file change. We can test this by updating our ConfigureServices call to inject the IConfigurationRoot directly into our ValuesController, so we can directly access the values. This is generally discouraged in favour of the strongly typed configuration available through the IOptions<> pattern, but it lets us bypass the model binding for now.

First we add the configuration to our IoC container:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // inject the configuration directly
        services.AddSingleton(Configuration);
        
        // Configure our options values
        services.Configure<MyValues>(Configuration.GetSection("MyValues"));
        services.AddMvc();
    }
}

And we update our ValuesController to receive and display the MyValues section of the IConfigurationRoot.

[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly IConfigurationRoot _config;
    public ValuesController(IConfigurationRoot config)
    {
        _config = config;
    }

    // GET api/values
    [HttpGet]
    public IEnumerable<KeyValuePair<string,string>> Get()
    {
        return _config.GetValue<string>("MyValues:DefaultValue");
    }
}

Performing the same operation as before - debugging, then changing appsettings.json to our new values - gives:

Chrome showing different output

Excellent, we can see the new value is returned! This demonstrates that the appsettings.json file is being reloaded when it changes, and that it is being propagated to the IConfigurationRoot.

Enabling trackConfigChanges

Given we know that the underlying IConfigurationRoot is reloading as required, there must be an issue with the binding configuration of IOptions<>. We bound the configuration to our MyValues class using services.Configure<MyValues>(Configuration.GetSection("MyValues"));, however there is another extension method available to us:

services.Configure<MyValues>(Configuration.GetSection("MyValues"), trackConfigChanges: true);

This extension has the property trackConfigChanges, which looks to be exactly what we're after! Unfortunately, updating our Startup.Configure() method to use this overload doesn't appear to have any effect - our injected IOptions<> still isn't updated when the underlying config file changes.

Using IOptionsMonitor

Clearly we're missing something. Diving in to the aspnet/Options library on GitHub we can see that as well as IOptions<> there is also an IOptionsMonitor<> interface.

Note, a word of warning here - the rest of this post is applicable to RC2, but has since been removed from RTM. It will be back post-1.0.0.

using System;

namespace Microsoft.Extensions.Options
{
    public interface IOptionsMonitor<out TOptions>
    {
        TOptions CurrentValue { get; }
        IDisposable OnChange(Action<TOptions> listener);
    }
}

You can inject this class in much the same way as you do IOptions<MyValues> - we can retrieve our setting value from the CurrentValue property.

We can test our appsettings.json modification routine again by injecting into our ValuesController:

private readonly MyValues _myValues;
public ValuesController(IOptionsMonitor<MyValues> values)
{
    _myValues = values.CurrentValue;
}

Unfortunately, we have the exact same behaviour as before, no reloading for us yet:

Chrome showing same output

Which, finally, brings us to…

The Solution

So again, this solution comes with the caveat that it only works in RC2, but it will most likely be back in a similar way post 1.0.0.

The key to getting reloads to propagate is to register a listener using the OnChange function of an OptionsMonitor<>. Doing so will retrieve a change token from the IConfigurationRoot and register the listener against it. You can see the exact details here. Whenever a change occurs, the OptionsMonitor<> will reload the IOptionsValue using the original configuration method, and then invoke the listener.

So to finally get reloading of our configuration-bound IOptionsMonitor<MyValues>, we can do something like this:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IOptionsMonitor<MyValues> monitor)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    monitor.OnChange(
        vals =>
        {
            loggerFactory
                .CreateLogger<IOptionsMonitor<MyValues>>()
                .LogDebug($"Config changed: {string.Join(", ", vals)}");
        });

    app.UseMvc();
}

In our configure method we inject an instance of IOptionsMonitor<MyValues> (this is automatically registered as a singleton in the services.Configure<MyValues> method). We can then add a listener using OnChange - we can do anything here, a noop function is fine. In this case we create a logger that writes out the full configuration.

We are already injecting IOptionsMonitor<MyValues> into our ValuesController so we can give one last test by running with F5, viewing the output, then modifying our appsettings.json and checking again:

Before and after AppSettings modification

Success!

Summary

In this post I discussed how to get changes to configuration files to be automatically detected and propagated to the rest of the application via the Options pattern.

It is simple to detect configuration file changes if you inject the IConfigurationRoot object into your classes. However, this is not the recommended approach to configuration - a strongly typed approach is considered better practice.

In order to use both strongly typed configuration and have the ability to respond to changes we need to use the IOptionsMonitor<> implementations in Microsoft.Extensions.Options. We must register a callback using the OnChange method and then inject IOptionsMonitor<> in our classes. With this setup, the CurrentValue property will always represent the latest configuration values.

As stated earlier, this setup works currently in the RC2 version of ASP.NET Core, but has been subsequently postponed till a post 1.0.0 release.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?