Strongly typed configuration in ASP.NET Core without IOptions<T>

· 781 words · 4 minutes to read

There are several great resources on the Internet about using the new Configuration and Options framework of ASP.NET Core - like this comprehensive post by Rick Strahl.

Using strongly typed configuration is without a question a great convenience and productivity boost for the developers; but what I wanted to show you today is how to bind IConfiguration directly to your POCO object - so that you can inject it directly into the dependent classes without wrapping into IOptions.

POCO configuration with IOptions 🔗

The typical IOptions driven configuration setup would look like on the snippet below. To use this code, you also need to reference the Microsoft.Extensions.Options.ConfigurationExtensions package, which will expose the extension methods and will also bring in the options framework package as a dependency.

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

This will allow you to load up MySettings from appsettings.json into MySettings POCO.

However, using the options framework also means that your configuration is registered in the DI container as IOptions, and that’s how you will need to inject it.

This typically wouldn’t matter but it also means that you will need to reference the Microsoft.Extensions.Options package everywhere where you want to consume this configuration (that’s where IOptions is defined).

In other words, your POCO configuration is not exactly POCO anymore, as it drags the extra dependency alongside it, as you always consume it like this:

public class DummyService
{
    public DummyService(IOptions<MySettings> settings)
    {
        //do stuff
    }
}

It also doesn’t give you access to the POCO configuration instance straight away - it just buries it directly into the DI, so to access it immediately in the Startup class, you’d need to resolve it from DI.

EDIT: On top of that - as mentioned by Steven - the problem with IOptions is that it will defer the evaluation of the configuration. This means that if your configuration file is incorrect, your app can crash later on, the first time IOptions.Value is accessed, rather than at startup.

POCO configuration without IOptions 🔗

You could, however, register the POCO configuration manually, and avoid the extra dependency on the options framework. This is shown below.

public static class ServiceCollectionExtensions
{
    public static TConfig ConfigurePOCO<TConfig>(this IServiceCollection services, IConfiguration configuration) where TConfig : class, new()
    {
        if (services == null) throw new ArgumentNullException(nameof(services));
        if (configuration == null) throw new ArgumentNullException(nameof(configuration));

        var config = new TConfig();
        configuration.Bind(config);
        services.AddSingleton(config);
        return config;
    }
}

We are manually binding the configuration to the POCO, rather than having the options framework do it for us. This functionality is available in the Microsoft.Extensions.Configuration.Binder package, which needs to be referenced (it is not an extra dependency for us though, it is already referenced by the Microsoft.Extensions.Options.ConfigurationExtensions package). It also needs to be referenced only in the entry project (or your composition root).

We can now consume this extension method as follows:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.ConfigurePOCO<MySettings>(Configuration.GetSection("MySettings"));
}

And you can now use your POCO configuration from the DI simply by injecting it, without any external dependencies.

public class DummyService
{
    public DummyService(MySettings settings)
    {
        //do stuff
    }
}

Another small limitation of the options framework is that the POCO configuration class must have a parameterless constructor, as the framework will try to instantiate it for us.

However, with our custom approach, we can control how our class is instantiated. We could write extra extension methods that either take in an already existing instance, or a delegate responsible for its creation.

public static class ServiceCollectionExtensions
{
    public static TConfig ConfigurePOCO<TConfig>(this IServiceCollection services, IConfiguration configuration, Func<TConfig> pocoProvider) where TConfig : class
    {
        if (services == null) throw new ArgumentNullException(nameof(services));
        if (configuration == null) throw new ArgumentNullException(nameof(configuration));
        if (pocoProvider == null) throw new ArgumentNullException(nameof(pocoProvider));

        var config = pocoProvider();
        configuration.Bind(config);
        services.AddSingleton(config);
        return config;
    }

    public static TConfig ConfigurePOCO<TConfig>(this IServiceCollection services, IConfiguration configuration, TConfig config) where TConfig : class
    {
        if (services == null) throw new ArgumentNullException(nameof(services));
        if (configuration == null) throw new ArgumentNullException(nameof(configuration));
        if (config == null) throw new ArgumentNullException(nameof(config));

        configuration.Bind(config);
        services.AddSingleton(config);
        return config;
    }
}

We can now consume these extension methods as its shown on the next snippet, without having to worry about exposing a default constructor. The instance could be created upfront or on demand through the delegate.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    // non default constructor
    var mySettings = new MySettings("foo"); 

    // note, the generic type can now by inferred
I   services.ConfigurePOCO(Configuration.GetSection("MySettings"), mySettings);
}

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    // note, the generic type can now by inferred, non default constructor
I   services.ConfigurePOCO(Configuration.GetSection("MySettings"), () => new MySettings("foo"));
}

This type of approach allows you to combine your POCO configuration classes with runtime data stretching beyond the configuration file - for example information you’d read from IHostingEnvironment.

About


Hi! I'm Filip W., a cloud architect from Zürich 🇨🇭. I like Toronto Maple Leafs 🇨🇦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁢󠁳󠁣󠁴󠁿.

You can find me on Github and on Mastodon.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP