Use IOptions<> for application configuration

Use IOptions for configuration and leverage the additional available interfaces

Home DailyDrop

Daily Knowledge Drop

Instead of trying to manually setup the dependency injection container with configuration from the, for example, appsettings.json file, use the built in .NET functionality and use the IOptions interface instead - and get IOptionsSnapshot and IOptionsMonitor for free!

This post won't go into details around the options pattern specifically, but it's the recommended approach when dealing with application settings as it enables the application to adhere to two important software architecture principles:

  • The Interface Segregation Principle (the I in SOLID)
  • Separation of concerns

Onto some code examples - on startup when configuration the DI container:

❌ Rather don't do this:

var appOptions = new ApplicationOptions();
configuration.GetSection("appConfiguration").Bind(appOptions);
services.AddSingleton(options);

With the above, ApplicationOptions can be injected into the relevant constructor and the application settings accessed.
Nothing inherently "wrong" with this, it works and follows the options pattern. However there is a better way.

✅ Rather do this:

var optionSection = configuration.GetSection("appConfiguration");
services.Configure<ApplicationOptions>(optionSection);

With the above, ApplicationOptions can NO longer be injected into the relevant constructor, instead IOptions<ApplicationOptions> (or one of the other two interfaces mentioned below) can be injected, allowing for access to the settings.


Why use Configure

So why use the IServiceCollection.Configure method instead of the Bind + AddSingleton methods as described above.

Just by using the IServiceCollection.Configure method, one automatically gets ato leverage the functionality of the three options interfaces.

For all three examples below, the following section has been added to appsettings.json:

  "appConfiguration": {
    "ApplicationName" : "OptionsDemo"
  }

And the options class, ApplicationOptions defined as follows:

public class ApplicationOptions
{
    public string ApplicationName { get; set; }
}

IOptions

✅ Added as DI container as singleton
❌ Does not allow reading of the configuration settings from source after the app has started.

var builder = WebApplication.CreateBuilder(args);

// get the "appConfiguration" section from the configuration 
//(appsettings.json in this case)
var optionSection = builder.Configuration.GetSection("appConfiguration");
// add to DI as ApplicationOptions 
builder.Services.Configure<ApplicationOptions>(optionSection);

var app = builder.Build();

// endpoint, which has the IOptions injected into it from the
// DI container
app.MapGet("/appname", (IOptions<ApplicationOptions> options) =>
{
    // .Value returns ApplicationOptions
    return options.Value.ApplicationName;
});

app.Run();

When the endpoint /appname is called, the application name from the appsettings.json is returned, via IOptions.

This injects IOptions<ApplicationOptions> as a singleton, and if the value in the appsettings.json file changes while the application is running, the change will not be reflected in IOptions<ApplicationOptions>.


IOptionsSnapshot

✅ Added as DI container as scoped
✅ Supports named options
✅ Configuration settings can be recomputed for each request (as the service is scoped)

var builder = WebApplication.CreateBuilder(args);

// get the "appConfiguration" section from the configuration 
//(appsettings.json in this case)
var optionSection = builder.Configuration.GetSection("appConfiguration");
// add to DI as ApplicationOptions 
builder.Services.Configure<ApplicationOptions>(optionSection);

var app = builder.Build();

// endpoint, which has the IOptionsSnapshot injected into it from the
// DI container
app.MapGet("/appname", (IOptionsSnapshot<ApplicationOptions> options) =>
{
    // .Value returns ApplicationOptions
    return options.Value.ApplicationName;
});

app.Run();

This injects IOptionsSnapshot<ApplicationOptions> as scoped, and if the value in the appsettings.json file changes while the application is running, this change will be reflected in IOptionsSnapshot<ApplicationOptions>.

In other words, for each scope (http request) a new snapshot of the ApplicationOptions values is calculated from source, and injected.


IOptionsMonitor

✅ Added as DI container as singleton
✅ Supports named options
✅ Supports options changed notifications

var builder = WebApplication.CreateBuilder(args);

var optionSection = builder.Configuration.GetSection("appConfiguration");
builder.Services.Configure<ApplicationOptions>(optionSection);

var app = builder.Build();

app.Services.GetService<IOptionsMonitor<ApplicationOptions>>()
    .OnChange((ApplicationOptions options) =>
{
    Console.WriteLine(options.ApplicationName);
});

app.MapGet("/appname", (IOptionsMonitor<ApplicationOptions> options) =>
{
    return options.CurrentValue.ApplicationName;
});

app.Run();

This injects IOptionsMonitor<ApplicationOptions> as a singleton, but functions very much the same as IOptionsSnapshot. If the value in the appsettings.json file changes while the application is running, this change will be reflected in IOptionsMonitor<ApplicationOptions>.

However IOptionsMonitor has the additional benefit of having an OnChange method, which accepts an Action<> which is called each time a value is changed. In other words, one can be notified of a value change.

In the above example, the lambda action method is called each time the value changes and writes the new value to the console.


Named Options

Both IOptionsSnapshot and IOptionsMonitor support named options. What this means, is that multiple of the same options (but different values) can be added to the DI container with a name, and then retrieved by name.

If there are multiple sets of the same configuration structure, for example:

"cloudKeyConfiguration": {
    "azure": {
        "name": "Microsoft",
        "key": "azurekey123"
    },
    "aws": {
        "name": "Amazon",
        "key": "awskey456"
    },
    "gcp": {
        "name": "Google",
        "key": "gcpkey789"
    }
}

And the options class, CloudKeyOptions defined as follows:

public class CloudKeyOptions
{
    public string Name { get; set; }

    public string Key { get; set; }
}

Usage is as follows:

var builder = WebApplication.CreateBuilder(args);

// add to DI container by name
var optionSectionAzure = builder.Configuration.GetSection("cloudKeyConfiguration:azure");
builder.Services.Configure<CloudKeyOptions>("azure", optionSectionAzure);

var optionSectionAws = builder.Configuration.GetSection("cloudKeyConfiguration:aws");
builder.Services.Configure<CloudKeyOptions>("aws", optionSectionAws);

var optionSectionGcp = builder.Configuration.GetSection("cloudKeyConfiguration:gcp");
builder.Services.Configure<CloudKeyOptions>("gcp", optionSectionGcp);

var app = builder.Build();

app.MapGet("/key/{provider}", (string provider, 
    IOptionsSnapshot<CloudKeyOptions> options) =>
{
    return options.Get(provider)?.Key;
});

app.Run();

IOptionsSnapshot<CloudKeyOptions> is injected, and a query string parameter is used to determine which named option to retrieve.


References

C# configuration fundamentals
Options Pattern In .NET

Daily Drop 03: 03-02-2022

At the start of 2022 I set myself the goal of learning one new coding related piece of knowledge a day.
It could be anything - some.NET / C# functionality I wasn't aware of, a design practice, a cool new coding technique, or just something I find interesting. It could be something I knew at one point but had forgotten, or something completely new, which I may or may never actually use.

The Daily Drop is a record of these pieces of knowledge - writing about and summarizing them helps re-enforce the information for myself, as well as potentially helps others learn something new as well.
c# .net dailydrop ioptions ioptionssnapshot ioptionsmonitor optionspattern configuration