Advanced options configuration in ASP.NET Core

ASP.NET Core comes with a completely new Options framework for accessing and configuration POCO settings. There are a few ways to configure the options and I’d like to elaborate on some more advanced features.

Component settings

When building a component-based system, these components probably have some settings. We define a component called ConsoleWriter implementing the IConsoleWriter interface:

public interface IConsoleWriter
{
    void Write();
}

public class ConsoleWriter : IConsoleWriter
{
    private readonly IOptions<ConsoleWriterOptions> _options;

    public ConsoleWriter(IOptions<ConsoleWriterOptions> options)
    {
        _options = options;
    }

    public void Write()
    {
        Console.WriteLine(_options.Value.Message);
    }
}

ConsoleWriterOptions is a POCO options class which looks like this:

public class ConsoleWriterOptions
{
    public string Message { get; set; }
}

The ConsoleWriter class can be added to the services using a convenient extension method:

public static void AddConsoleWriter(this IServiceCollection services)
{
    services.AddSingleton<IConsoleWriter, ConsoleWriter>();
}

Using that we can add the service:

public void ConfigureServices(IServiceCollection services)
{
    // Add the Options framework.
    services.AddOptions();
    
    // Add our ConsoleWriter service using manual configuration.
    services.AddConsoleWriter();
}

Now we want to configure those ConsoleWriterOptions to change the message being printed. It can either be configured manually or via a configuration file.

Manual configuration

To enable manual configuration of the options, we add an Action<ConsoleWriterOptions> parameter to the AddConsoleWriter method:

public static void AddConsoleWriter(this IServiceCollection services, Action<ConsoleWriterOptions> setupAction)
{
    // Add the service.
    services.AddSingleton<IConsoleWriter, ConsoleWriter>();

    // Configure the options manually.
    services.Configure(setupAction);
}

The Configure method is part of the Options API and accepts a Action<TOptions> parameter which in turn creates a new ConfigureOptions<TOptions> instance and calls ConfigureOptions. When the application is build, it will create an instance of the ConsoleWriterOptions class and apply the action we pass in the lambda expression. We can use it like this:

services.AddConsoleWriter(x => x.Message = "Hello world!");

If we activate an instance of our ConsoleWriter, the runtime will inject an instance of IOptions<ConsoleWriterOptions> into our constructor.

Configuration using the Configuration API

While this is nice, we usually want dynamic settings that can be changed at runtime. The Configuration API allows us to populate our POCO options from a JSON file.

Let’s add JSON file called config.json:

{
  "ConsoleWriter": {
    "Message": "Hello World from config.json!"
  }
}

The ConsoleWriter element in the JSON file is the section we’re going to use to populate our ConsoleWriterOptions object. To accomplish this, add another overload of AddConsoleWriter:

public static void AddConsoleWriter(this IServiceCollection services, IConfigurationSection config)
{
    // Add the service.
    services.AddSingleton<IConsoleWriter, ConsoleWriter>();

    // Configure the options using the given configuration.
    services.Configure<ConsoleWriterOptions>(config);
}

To populate our configuration with the config.json file, add a constructor in the startup class:

public Startup()
{
    Configuration = new ConfigurationBuilder()
        .AddJsonFile("config.json")
        .Build();
}

private IConfiguration Configuration { get; }

And call our new overload:

services.AddConsoleWriter(Configuration.GetSection("ConsoleWriter"));

We retrieve the section in the configuration properties by its name: ConsoleWriter. If we’d run the app, it will print:

Hello World from config.json!

We can make our life a bit easier by using a default section name in the component registration:

public static void AddConsoleWriter(this IServiceCollection services, IConfiguration config)
{
    services.AddSingleton<IConsoleWriter, ConsoleWriter>();
    services.Configure<ConsoleWriterOptions>(config.OrSection("ConsoleWriter"));
}

The OrSection method is a nifty extension method to help us:

public static IConfigurationSection OrSection(this IConfiguration config, string key)
{
    return config as IConfigurationSection ?? config.GetSection(key);
}

It tries to cast the given IConfiguration object to an IConfigurationSection object (IConfigurationSection derives from IConfiguration). That allows us to pass in either a section or just the complete configuration object. If the given object happens to be a section, we use that, otherwise we get the section using the given key as fallback. Now we can call AddConsoleWriter with just the IConfiguration object:

services.AddConsoleWriter(Configuration);

And it will use the default ConsoleWriter section. However, if the settings happens to be in another section, we can still pass that section:

services.AddConsoleWriter(Configuration.GetSection("App:CustomConsoleWriter"));

Global options configuration

Beside component settings, we could also have application-wide settings. These settings might be populated by components being added to the application.

For example, we have a class AppSettings:

public class AppSettings
{
    public string Option { get; set; }
}

Now we want to configure some settings if the ConsoleWriter component is added to the application. To accomplish, we can implement the IConfigureOptions<T> interface which defines a Configure(T) method:

public class ConfigureAppSettings : IConfigureOptions<AppSettings>
{
    public void Configure(AppSettings options)
    {
        options.Option = $"Option from: {nameof(ConfigureAppSettings)}";
    }
}

We can configure this class in the service collection by calling ConfigureOptions<T>:

services.ConfigureOptions<ConfigureAppSettings>();

This adds the IConfigureOptions<AppSettings> implementation to the service collection causing the OptionsManager to call the Configure method when a service requests an IOptions<T>.

Conclusion

We’re now able to write modular and reusable components and pass in configuration at application startup either manually or from a configuration source. We also learned how components can populate application wide settings using the IConfigureOptions<T> interface.

The source code for this post is available here.

Written on January 6, 2016