Is IOptions<T> bad?

Today I stumbled about a Tweet that suggest to not use IOptions because it is a bad idea.

The tweet refers to a documentation page of SimpleInjector arguing why it is a bad idea to use IOptions. In this post I want to investigate the arguments and describe my opinions on those.

You can not verify your configuration at startup. Yes we can!

ASP.NET Core contains a new configuration model based on an IOptions abstraction. We advise against injecting IOptions dependencies into your application components. Instead let components depend directly on configuration objects and register them as Singleton. This ensures that configuration values are read during application start up and it allows verifying them at that point in time, allowing the application to fail-fast.

This argument implies that it is not possible to verify the configuration and fail-fast when using IOptions<T>. This is not correct. You are very well able to validate and verify the configuration and fail-fast on startup when using IOptions<T>.

public class MyOptions
{
    public string MyValue { get; set; }
}

...

public void ConfigureServices(IServiceCollection services)
{
    var options = Configuration.GetSection("MyOptions").Get<MyOptions>();

    if (string.IsNullOrWhiteSpace(options?.MyValue))
    {
        throw new ApplicationException("MyValue is not configured!");
    }

    services.Configure<MyOptions>(o => o.MyValue = options.MyValue);
}

This code tries to get an instance of MyOptions of the configuration, then validates the MyValue property. If it is null or whitespace it throws an exception and the application terminates (if not catched). If the configuration is verified it configures MyOptions with the values ot of the config. Of course the “reconfiguring” of MyOptions using an Action is not really nice, but it is possible to validate and verify the configuration. So this is not a valid argument against IOptions<T>.

It adds dependencies and violates DIP. Be pragmatic!

Letting application components depend on IOptions has some unfortunate downsides. First of all, it causes application code to take an unnecessary dependency on a framework abstraction. This is a violation of the Dependency Injection Principle that prescribes the use of application-tailored abstractions. Injecting an IOptions into an application component only makes this component more difficult to test, while providing no benefits. Application components should instead depend directly on the configuration values they require.

While the first part of this argument is not wrong, it sounds to me kind of religious. Yes you are adding additional dependencies, but when you are building a library for ASP.NET Core, the dependency on IOptions<T> is most likely already there. If you are targeting a WinForms/WPF/Console application you should of course NOT force users of your library to include the Options library. In this case it makes sense to NOT use IOptions<T>.

But when targeting an ASP.NET Core application it is fine, in my option, to use IOptions<T>. This reduces confusion for new developers, since this is the “standard”, recommended and documented way for configuring components. Besides the IOptions<T> I do not recommend “use application-tailored abstractions” as a general rule of thumb. Abstracting all framework abstractions in your application, just blows up your project, makes it more complex and therefor harder to maintain and to understand! Ultimately this means you should also not use the .NET Core ILogger, but abstract it with your own ILogger. While this gets the theoretic possibility to change framework/libraries, my experience is that most of the framework/libraries do not change within the life cycle of an application. Again, know your target project/audience and depending on this add additional abstraction or not! The main purpose of applications and the people that develop those is to solve problems. Real life problems. Focus on your problems and not if you strictly follow “rules”. Of course, keep your code clean and well structured, but be pragmatic!

In regards to the testability, it is not that difficult to implement a “static” IOptions<T> for testing.

public class TestOptions<T> : IOptions<T> where T : class, new()
{
    private T _instance;

    public TestOptions(T instance) => _instance = instance;

    public TestOptions(Action<T> configure)
    {
        _instance = new T();
        configure(_instance);
    }

    public T Value => _instance;
}

You can not verify your configuration at startup #2. Yes we still can!

IOptions configuration values are read lazily. Although the configuration file might be read upon application start up, the required configuration object is only created when IOptions.Value is called for the first time. When deserialization fails, because of application misconfiguration, such error will only be appear after the call to IOptions.Value. This can cause misconfigurations to keep undetected for much longer than required. By reading -and verifying- configuration values at application start up, this problem can be prevented. Configuration values can be injected as singletons into the component that requires them.

See my first example. You can read -and verify- your configuration at application start up. This has nothing to do with IOptions<T>.

You get empty options. Have sane default options!

To make things worse, in case you forget to configure a particular section (by omitting a call to services.Configure) or when you make a typo while retrieving the configuration section (by supplying the wrong name to Configuration.GetSection(name)), the configuration system will simply supply the application with a default and empty object instead of throwing an exception! This may make sense in some cases but it will easily lead to fragile applications.

Again, see my first example. If your configuration is that important that your application can not run without it, verify it at startup! If not, your option classes should really not be “empty” but should have good and sane default values! If you can not find good default values, then verify and fail at startup! Personally I am very happy that I DO NOT need to configure all options of big frameworks like ASP.NET Core Identity or IdentityServer and that they provide good defaults that I can selectively change if needed.

And if you like, you can implement your own IOptionsFactory<T> and change the way how options are instantiated (e.g. return null if not configured, add logging, etc.).

It makes no sense to use IOptions. There is more than IOptions!

Since you want to verify the configuration at start-up, it makes no sense to delay reading it, and that makes injecting IOptions into your components plain wrong. Depending on IOptions might still be useful when bootstrapping the application, but not as a dependency anywhere else.

Since you can verify the configuration at start-up, it makes no sense to call injecting IOptions<T> wrong. With IOptions<T> and the Options library comes other features, like IPostConfigureOptions that lets you or your library users hook into the configuration process of options. Or IOptionsMonitor<T> that gives you options that can be changed at runtime, without restarting the application (very useful for e.g. log levels).

Conclusion

I am very happy that in the ASP.NET Core / .NET Core world we get abstractions by the vendor. This allows us to change framework parts and makes code more testable. Is the Options API the nicest API out there? No! But more important, it introduces some “standards” and harmonizes projects, which makes it easier for developers to get into existing project. It gives your project “free” documentation, since these abstractions and their framework implementations are already documented. It lets you focus more on the problems you want to solve, because other people already thought about some of the technical problems to solve.

Contents