Creating singleton named options with IOptionsMonitor

In recent posts I've been discussing some of the lesser known features of the Options system in ASP.NET Core 2.x. In the first of these posts, I described how to use named options when you want to have multiple instances of a strongly-typed setting, each with a different name. If you're new to them, I recommend reading that post for an introduction to named options, when they make sense, and how to use them.

In this post I'm going to address a limitation with the named options approach seen previously. Namely,
the documented way to access named options is with the IOptionsSnaphshot<T> interface. Accessing named options in this way means they always have a Scoped lifecycle, and are re-bound to the underlying configuration with every request. In this post I introduce the IOptionsMonitor<T> interface, and show how you can use it to create Singleton named options.

Named options are always scoped with IOptionsSnapshot<>

So far, we've mainly looked at two different interfaces for accessing your strongly-typed settings: IOptions<T> and IOptionsSnapshot<T>.

Creates and populates the T instance the first time an IOptions<T> instance is requested and the Value property is accessed.

Whereas for IOptionsSnapshot<T>:

Value property contains the default, strongly-typed settings object T

Get(name) method is used to fetch named options for T.

Is Scoped - caches T instances for the lifetime of the request.

Creates and populates the default and named T instances the first time they're accessed each request.

To me, the IOptionsSnapshot<T> feels a little bit messy, as it differs from the basic IOptions<T> interface in two orthogonal ways:

It allows you to use named options.

It is Scoped, and responds to changes in the underlying IConfiguration object.

If you wish to have your strongly-typed settings objects automatically change when someone updates the appsettings.json file (for example) then IOptionsSnapshot<T> is definitely the interface for you.

However, if you just want to use named options and don't care about the "reloading" behaviour, then the fact that IOptionsSnapshot<T> is Scoped is actually detrimental. You can't inject Scoped dependencies into Singleton services, which means you can't easily use named options in Singleton services. Also, if you know that the underlying configuration files aren't going to change (or you don't want to respect those changes), then re-binding the configuration to a new T settings object every request is very wasteful. That's a lot of pointless reflection and garbage to be collected for no benefit.

So what if you want to use named options, but you want them to be Singletons, not Scoped? There's a few options available to you, some cleaner than others. I'll discuss three of those possibilities in this post.

1. Casting IOptions<T> to IOptionsSnapshot<T>

The IOptionsSnapshot<T> interface extends the IOptions<T> interface, adding support for named options with the Get(name) method:

With that in mind, the suggestion "cast IOptions<T> to IOptionsSnapshot<T>" doesn't seem to makes sense; we could safely cast an IOptionsSnapshot<T> instance to IOptions<T>, but not the other way around, surely?

publicstaticclassOptionsServiceCollectionExtensions{publicstaticIServiceCollectionAddOptions(thisIServiceCollection services){// Both IOptions<T> and IOptionsSnapshot<T> are implemented by OptionsManager<T>
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>),typeof(OptionsManager<>)));
services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>),typeof(OptionsManager<>)));// I'll get to this one later
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>),typeof(OptionsMonitor<>)));// Other depedent services elidedreturn services;}}

This extension method is called by the framework in ASP.NET Core, so you rarely have to call it directly yourself.

Assuming you don't override this registration somehow (you really shouldn't!) then you can be pretty confident that any IOptions<T> instance is actually an OptionsManager<T> instance, and hence implements IOptionsSnapshot<T>. That means the following code is generally going to be safe:

With this approach, we get Singleton named option with very little drama. We know the OptionsManager<T> instance injected into the service is a Singleton, so it's still a singleton after casting to IOptionsSnapshot<T>. The "MyName" named options are bound only once, the first time they're requested, and they're cached for the lifetime of the app. Another benefit is that we didn't have to mess with the DI registrations at all.

It doesn't feel very nice though, does it? It requires explicit knowledge of the underlying DI configuration, and is definitely not obvious. Instead of casting interfaces around, we could just use the OptionsManager<T> directly.

2. Using OptionsManager<T> directly

As you've already seen, OptionsManager<T> is the class that implements both IOptions<T> and IOptionsSnapshot<T>. We could just directly inject this class into our services, and access the implementation methods directly:

It's pretty unlikely that this will actually cause any issues in practice. Strongly-typed settings are typically (and arguably should be) dumb POCO objects that are treated as immutable once created. So even though they may not actually be singletons (the IOptions<T> instance has one copy, and the OptionsManager<T> instance has another), they will have the same values. Just don't go storing state in them!

IOptionsMonitor<T> is a bit like IOptions<T> in some ways and IOptionsSnapshot<T> in others:

It's registered as a Singleton (like IOptions<T>)

It contains a CurrentValue property that gets the default strongly-typed settings object as a Singleton (like IOptions<T>.Value)

It has a Get(name) method for returning named options (like IOptionsSnapshot<T>). Unlike IOptionsSnapshot<T>, these named options are Singletons.

Responds to changes in the underlying IConfiguration object by re-binding options. Note this only happens when the configuration changes (not every request like IOptionsSnapshot<T> does).

IOptionsMonitor<T> is itself a Singleton, and it caches both the default and named options for the lifetime of the app. However, if the underlying IConfiguration that the options are bound to changes, IOptionsMonitor<T> will throw away the old values, and rebuild the strongly-typed settings. You can register to be informed about those changes with the OnChange(listener) method, but I won't go into that in this post.

Using named options in singleton services is now easy with IOptionsMonitor<T>:

Your services depend on an interface instead of a concrete implementation

No safe/unsafe casts required

The only thing to remember is that CurentValue (and the values from Get()) are just that, the current values. While they're the only instance at any one time, they will change if the underlying IConfiguration changes. If that's not a concern for you, then IOptionsMonitor<T> is probably the way to go.

Summary

Named options solve a specific use case - where you want to have multiple instance of a strongly-typed configuration object. You can access named options from the IOptionsSnapshot<T> interface. However, the strongly-typed settings object you get T will be recreated with every request, and can only be used in a Scoped context. Sometimes, for performance or convenience reasons, you might want to access named options from a Singleton service.

There are several ways to achieve this. You can:

Inject an IOptions<T> instance and cast it to IOptionsSnapshot<T>.

Register OptionsManager<T> as a Singleton in the DI container, and inject it directly.

Use IOptionsMonitor<T>. Be aware that if the underlying configuration changes, the singleton objects will change too.

Of these three options, I think IOptionsMonitor<T> provides the best solution, though it's important to be aware of the behaviour when the underlying IConfiguration object changes.