Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Reloading strongly typed options in ASP.NET Core 1.1.0

$
0
0
Reloading strongly typed options in ASP.NET Core 1.1.0

Back in June, when ASP.NET Core was still in RC2, I wrote a post about reloading strongly typed Options when the underlying configuration sources (e.g. a JSON) file changes. As I noted in that post, this functionality was removed prior to the release of ASP.NET Core 1.0.0, as the experience was a little confusing. With ASP.NET Core 1.1.0, it's back, and much simpler to use.

In this post, I'll show how you can use the new IOptionsSnapshot<> interface to simplify reloading strongly typed options. I'll provide a very brief summary of using strongly typed configuration in ASP.NET Core, and touch on the approach that used to be required with RC2 to show how much simpler it is now!

tl;dr; To have your options reload when the underlying file / IConfigurationRoot changes, just replace any usages of IOptions<> with IOptionsSnapshot<>

The ASP.NET Core configuration system

The configuration system in ASP.NET Core is rather different to the approach taken in ASP.NET 4.X. Previously, you would typically store your configuration in the AppSettings section of the XML web.config file, and you would load these settings using a static helper class. Any changes to web.config would cause the app pool to recycle, so changing settings on the fly this way wasn't really feasible.

In ASP.NET Core, configuration of app settings is a more dynamic affair. App settings are still essentially key-value pairs, but they can be obtained from a wide array of sources. You can still load settings from XML files, but also JSON files, from the command line, from environment variables, and many others. Writing your own custom configuration provider is also possible if you have another source you wish to use to configure your application.

Configuration is typically performed in the constructor of Startup, loading from multiple sources:

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 IConfigurationRoot Configuration { get; }  

This constructor creates a configuration object, loading the configuration found in each of the file sources (two JSON files and Environment Variables in this case). Each source supplies a set of key-value pairs, and each subsequent source overwrites values found in earlier sources. The final IConfigurationRoot is essentially a dictionary of all the final key-value pairs from all of your configuration sources.

It is perfectly possible to use this IConfigurationRoot directly in your application, but the suggested approach is to use strongly typed settings instead. Rather than injecting the whole dictionary of settings whenever you need to access a single value, you take a dependency on a strongly typed POCO C# class. This can be bound to your configuration values and used directly.

For example, imagine I have the following values in appsettings.json:

{
  "MyValues": {
    "DefaultValue" : "first"
  }
}

This could be bound to the following class:

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

The binding is setup when you are configuring your application for dependency injection in the ConfigureServices method:

public void ConfigureServices(IServiceCollection services)  
{
    services.Configure<MyValues>(Configuration.GetSection("MyValues"));
}

With this approach, you can inject an instance of IOptions<MyValues> into your controllers and access the settings values using the strongly typed object. For example, a simple web API controller that just displays the setting value:

[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;
    }
}

would give the following output when the url /api/Values is hit:

Reloading strongly typed options in ASP.NET Core 1.1.0

Reloading strongly typed options in ASP.NET Core RC2

Now that you know how to read settings in ASP.NET Core, we get to the interesting bit - reloading options. You may have noticed that there is a reloadOnChange parameter on the AddJsonFile method when building your configuration object in Startup. Based on this parameter it would seem like any changes to the underlying file should propagate into your project.

Unfortunately, as I explored in a previous post, you can't just expect that functionality to happen magically. While it is possible to achieve, it takes a bit of work.

The problem lies in the fact that although the IConfigurationRoot is automatically updated whenever the underlying appsettings.json file changes, the strongly typed configuration IOptions<> is not. Instead, the IOptions<> is created as a singleton when first requested and is never updated again.

To get around this, RC2 provided the IOptionsMonitor<> interface. In principle, this could be used almost identically to the IOptions<> interface, but it would be updated when the underlying IConfigurationRoot changed. So, for example, you should be able to modify your constructor to take an instance of IOptionsMonitor<MyValues> instead, and to use the CurrentValue property:

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

Unfortunately, as written, this does not have quite the desired effect - there is an additional step required. As well as injecting an instance of IOptionsMonitor you must also configure an event handler for when the underlying configuration changes. This doesn't have to actually do anything, it just has to be set. So for example, you could set the monitor to just create a log whenever the underlying file changes:

public void Configure(IApplicationBuilder app, 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();
}

With this in place, changes to the underlying appsettings.json file will be reflected each time you request an instance of IOptionsMonitor<MyValues> from the dependency injection container.

The new way in ASP.NET Core 1.1.0

The approach required for RC2 felt a bit convoluted and was very easy to miss. Microsoft clearly thought the same, as they removed IOptionsMonitor<> from the public package when they went RTM with 1.0.0. Luckily, a new improved approach is back with version 1.1.0 of ASP.NET Core.

No additional setup is required to have your strongly typed options reload when the IConfigurationRoot changes. All you need to do is inject IOptionsSnapshot<> instead of IOptions<>:

public class ValuesController : Controller  
{
    private readonly MyValues _myValues;
    public ValuesController(IOptionsSnapshot<MyValues> values)
    {
        _myValues = values.Value;
    }
}

No additional faffing in the ConfigureMethod, no need to setup additional services to make use of IOptionsSnapshot - it is all setup and works out of the box once you configure your strongly typed class using

public void ConfigureServices(IServiceCollection services)  
{
    services.Configure<MyValues>(Configuration.GetSection("MyValues"));
}

Trying it out

To make sure it really did work as expected, I created a simple project using the values described in this post, and injected both an IOptions<MyValues> object and an IOptionsSnapshot<MyValues> object into a web API controller:

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

    // GET api/values
    [HttpGet]
    public string Get()
    {
        return $@"
IOptions<>:         {_myValues.DefaultValue}  
IOptionsSnapshot<>: {_snapshot.DefaultValue},  
Are same:           {_myValues == _snapshot}";  
    }
}

When you hit /api/Values this simply writes out the values stored in the current IOptions and IOptionsSnapshot<> values as plaintext:

Reloading strongly typed options in ASP.NET Core 1.1.0

With the application still running, I edited the appsettings.json file:

{
  "MyValues": {
    "DefaultValue" : "The second value"
  }
}

I then reloaded the web page (without restarting the app), and voila, the value contained in IOptionsSnapshot<> has updated while the IOptions value remains the same:

Reloading strongly typed options in ASP.NET Core 1.1.0

One point of note here - although the initial values are the same for both IOptions<> and IOptionsSnapshot<>, they are not actually the same object. If I had injected two IOptions<> objects, they would have been the same object, but that is not the case when one is an IOptionsSnapshot<>. (This makes sense if you think about it - you couldn't have them both be the same object and have one change while the other stayed the same).

If you don't like to use IOptions

Some people don't like polluting their controllers by using the IOptions<> interface everywhere they want to inject settings. There are a number of ways around this, such as those described by Khalid here and Filip from StrathWeb here. You can easily extend those techniques to use the IOptionsSnapshot<> approach, so that all of your strongly typed options classes are reloaded when an underlying file changes.

A simple solution is to just delegate the request for the MyValues object to the IOptionsSnapshot<MyValues>.Value value, by setting up a delegate in ConfigureServices:

public void ConfigureServices(IServiceCollection services)  
{
    services.Configure<MyValues>(Configuration.GetSection("MyValues"));
    services.AddScoped(cfg => cfg.GetService<IOptionsSnapshot<MyValues>>().Value);
}

With this approach, you can have reloading of the MyValues object in the ValuesController, without needing to explicitly specify the IOptionsSnapshot<> interface - just use MyValues directly:

public class ValuesController : Controller  
{
    private readonly MyValues _myValues;
    public ValuesController(MyValues values)
    {
        _myValues = values;
    }
}

Summary

Reloading strongly typed options in ASP.NET Core when the underlying configuration file changes is easy when you are using ASP.NET Core 1.1.0. Simply replace your usages of IOptions<> with IOptionsSnapshot<>.


Viewing all articles
Browse latest Browse all 743

Trending Articles