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.