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

Simplifying dependency injection for IConfigureOptions with the ConfigureOptions() helper

$
0
0

In recent posts I've been discussing the Options pattern for strongly-typed settings configuration in some depth. One of the patterns that has come up several times is using IConfigureOptions<T> or IConfigureNamedOptions<T> when you need to use a service from DI to configure your Options. In this post I show a convenient way for registering your IConfigureOptions with the ASP.NET Core DI container using the ConfigureOptions() extension method.

tl;dr; ConfigureOptions<T> is a helper extension method that looks for all IConfigureOptions<>, and IPostConfigureOptions<> implemented by the type T, and registers them in the DI container for you, so you don't have to do it manually using AddTransient<,>.

Using services to configure strongly-typed options

Whenever you need to use a service that's registered with the DI container as part of your strongly-typed setting configuration, you need to use IConfigureOptions<T> or IConfigureNamedOptions<T>. By implementing these interfaces in a class, you can configure an options object T using any required services from the DI container.

For example, the following class implements IConfigureOptions<MySettings>. It is used to configure the default MySettings options instance, using the CalculatorService service obtained from the DI container.

public class ConfigureMySettingsOptions : IConfigureOptions<MySettings>
{
    private readonly CalculatorService _calculator;
    public ConfigureMySettingsOptions(CalculatorService calculator)
    {
        _calculator = calculator;
    }

    public void Configure(MySettings options)
    {
        options.MyValue = _calculator.DoComplexCalculation();
    }
}

To register this class with the DI container you would use something like:

services.AddTransient<IConfigureOptions<MySettings>, ConfigureMySettingsOptions>();

A similar class for configuring named options might be:

public class ConfigurePublicMySettingsOptions : IConfigureNmedOptions<MySettings>
{
    private readonly CalculatorService _calculator;
    public ConfigureMySettingsOptions(CalculatorService calculator)
    {
        _calculator = calculator;
    }

    public void Configure(string name, MySettings options)
    {
        if(name == "Public")
        {
            options.MyValue = _calculator.DoComplexCalculation();
        }
    }

    public void Configure(string name, MySettings options) => Configure(Options.DefaultName options);
}

Even though this class implements IConfigureNamedOptions<T>, you still have to register it in the DI container using the non-named interface, IConfigureOptions<MySettings>:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IConfigureOptions<MySettings>, ConfigurePublicMySettingsOptions>();
}

In my last post, I showed another interface IPostConfigureOptions<T> which can be used in a similar manner, but only runs its configuration actions after all other configure actions for an options type have been executed. This one also needs to be registered in the DI container:

services.AddTransient<IPostConfigureOptions<MySettings>, PostConfigureMySettings>();

Remember, there is no named-options-specific IPostConfigureOptions<T> - IPostConfigureOptions<T> is used to configure both default and named options.

Automatically registering the correct interfaces with ConfigureOptions()

Having to remember which version of the interface to use when registering your class in the DI container is a bit cumbersome. This is especially true if your configuration class implements multiple configuration interfaces! This class:

public class ConfigureInternalCookieOptions :
    IConfigureNamedOptions<CookieAuthenticationOptions>,
    IPostConfigureOptions<CookieAuthenticationOptions>,
    IPostConfigureOptions<OpenIdConnectOptions>,
    IConfigureOptions<CorsOptions>,
    IConfigureOptions<CachingOptions>
{}

would need all of these registrations:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IConfigureOptions<CookieAuthenticationOptions>, ConfigureInternalCookieOptions>();
    services.AddTransient<IPostConfigureOptions<CookieAuthenticationOptions>, ConfigurePublicMySettingsOptions>();
    services.AddTransient<IPostConfigureOptions<OpenIdConnectOptions>, ConfigurePublicMySettingsOptions>();
    services.AddTransient<IConfigureOptions<CorsOptions>, ConfigureInternalCookieOptions>();
    services.AddTransient<IConfigureOptions<CachingOptions>, ConfigureInternalCookieOptions>();
}

Luckily, there's a convenient extension method that can dramatically simplify the registration process, called ConfigureOptions(). With this method, your registrations slim down to the following:

public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureOptions<ConfigureInternalCookieOptions>();
}

Much better!

Behind the scenes, ConfigureOptions<> finds all of the IConfigureOptions<> (including IConfigureNamedOptions<>) and IPostConfigureOptions<> interfaces implemented by the provided type, and registers them in the DI container:

public static IServiceCollection ConfigureOptions<T>(this IServiceCollection services)
{
    var configureType = typeof(T);
    services.AddOptions(); // Adds the infrastructure classes if not already added
    var serviceTypes = FindIConfigureOptions(configureType); // Finds all the IConfigureOptions and IPostConfigure options
    foreach (var serviceType in serviceTypes)
    {
        services.AddTransient(serviceType, configureType); // Adds each registration for you
    }
    return services;
}

Even if your classes only implement one of the configuration interfaces, I suggest always using this extension method instead of manually registering them yourself. Sure, there will be the tiniest startup performance impact in doing so, as it uses reflection to do the registration. But the registration code is so much easier to read, and harder to get wrong, that I suspect it's probably worth it!

Summary

When you need to use DI services to configure your strongly-typed settings, you have to implement IConfigureOptions<>, IConfigureNamedOptions<>, or IPostConfigureOptions<>, and register your class appropriately in the DI container. The ConfigureOptions() extension method can take care of the registration for you, by reflecting over the type, finding the implemented interfaces, and registering them in the DI container with the appropriate service. If you find your registration code hard to grok it might be worth considering switching to ConfigureOptions() in your own apps.


Viewing all articles
Browse latest Browse all 743

Trending Articles