In one of my previous posts, Hiding Secrets in appsettings.json – Using a Bridge in your ASP.Net Core Configuration (Part 4) I had a comment from Anderson asking if I had considered IConfigureOptions<TOptions> as a way of injecting dependencies into the TOptions class that had been bound to a configuration section.
I had not come across the interface, so with an example provided by Anderson, I started to look further into it. Andrew Lock has post on his blog that describes how an implementation works which was a starting point for me.
The IConfigureOptions<TOptions> interface has a single method (Configure) that takes an instance of TOptions where TOptions is a class that you will use for binding configuration values. If unfamiliar with this, please read my previous posts starting with Creating a Bridge to your ASP.Net Core Configuration from your Controller or Razor Page (Part 1).
In your implementation of IConfigureOptions<TOptions>, you are free to manipulate the options instance by changing property values. Your implementation can have a constructor that will take any dependencies you may need into order to set the properties. These constructor parameters will be resolved by the DI container. However, be aware of different scopes that may apply and how these are resolved. Read Andrew’s post for more info.
In the example below, I have:
- a class MySettings which I will bind the configuration section to.
- three implementations of IConfigureOptions<MySettings> which change the Setting1 string in different ways.
- an interface IDateTimeResolver and class implementation to demonstrate the DI container injecting a resolved instance into the last of the IConfigureOptions<MySettings> implementations (and also serves as a reminder that using DateTime.Now directly in your code is evil and should be wrapped in another class so it can be substituted when unit testing)
The interface/implementation mappings are registered with the DI container in the Startup class. Note the Confgure<MySettings>() and AddOptions() extension methods have been registered first.
What is interesting, is that multiple classes can implement the interface and, when registered with the DI container, be applied in the order they have been registered.
When I first saw this, I was confused as my understanding of DI registration was that the last registration is the one that is used to return the resolved instance by the DI container, so how could multiple registrations be applied? Of course, the penny dropped in that whatever is using these instances is not asking for the DI container to resolve a single instance, but is somehow iterating over the relevant registrations and resolving each registration in turn then calling the .Configure method.
This pretty much confirmed the theory that the services collection is being iterated over, except you will notice that the constructor takes a parameter of IEnumerable<IConfigureOptions<TOptions>>. So what is resolving these multiple instances?
Well it turns out to be the DI Container itself! I will go into more detail in a future post (as I don’t want to deviate from the main topic of this post)*, but in short, if your constructor takes an IEnumerable<T> as a parameter, then the DI container will use all registered mappings of T to resolve and inject multiple instances as an enumeration.
*Update – Since the post was written, Steve Gordon has written a great post that goes into more detail about the DI container and resolving multiple implementations – see ASP.NET Core Dependency Injection – Registering Multiple Implementations of an Interface.
So in the OptionsFactory instance that will be created by ASP.Net using the DI container (where TOptions is MySettings, the class I have set up for configuration binding), the DI container will resolve the parameter of IEnumerable<IConfigureOptions<MySettings>> to our three registered implementations of IConfigureOptions<MySettings>. These will be injected and then the Create method in the factory will call the Configure method on each instance in order of registration in the Create method.
Therefore to demonstrate this, we will start with an appsettings.json that looks like this:
We can step through the three implementations as they are called by the factory Create method to see how the Settings1 value in MySettings is updated:
IConfigureNamedOptions<TOption> and IPostConfigureOptions<TOptions>
The eagle eyed will have spotted that in the OptionsFactory, there is a check on each instance of IConfigureOption<TOption> to see if it is an implementation of IConfigureNamedOptions<TOption>.
This is very similar to IConfigureOption<TOption>, but the signature of the Configure method has an extra string parameter called name to point to a named configuration instance.
Once each of the registered implementations of IConfigureOption<TOption> has been processed, the factory then looks for implementations of the IPostConfigureOptions<TOptions> interface.
I plan on covering these two interfaces in a future post as I don’t want to make this post too long.
At first glance, this approach is similar to the bridge pattern approach I have described in my earlier posts in that the responsibility for changing the state values is delegated to an outside class.
However it is not a bridge as the properties of the underlying configuration instance are being changed. A bridge will intercept requests and then change the return value, leaving the underlying configuration instance unchanged.
When using the IConfigureOptions<TOptions> approach, the TOptions instance that is passed in through the parameter in the Configure method is being mutated. The changes will live on in the instance.
My personal opinion is that this not as clean as the bridge approach (but I guess I am biased).
I prefer to try to avoid potentially unexpected mutation of objects and instead return either a new instance of the object with property values copied from the existing into the new one or a substitute (e.g. the bridge). In other words, a functional approach where the input object is treated as immutable.
This leaves the original instance of the class in its initialised state which may be required by other functionality in the application that does not want the updated values or can be used by a different bridge class.