Extensions: Question: AddOptions<T>() vs. Multiple Configure<T>(…)

Created on 16 Nov 2018  Â·  5Comments  Â·  Source: dotnet/extensions

I've been writing code like the following in my projects in Startup.cs:

// this is from ConfigureServices, so services is an instance of IServiceCollection
// assume Configuration is an instance of IConfiguration and was injected via the constructor
services.AddOptions()
  .Configure<MyOptions>(Configuration.GetSection("mySection"))
  .Configure<MyOptions>(x => x.SomeProperty = "foo");

and i'm wondering if I should expect this to continue working, or if it is just coincidence and I should switch to the following:

services.AddOptions()
  .AddOptions<MyOptions>()
  .Bind(Configuration.GetSection("mySection"))
  .Configure(x => x.SomeProperty = "foo");

Some more context: the reason i'm asking this is because i see that in the first example both of those Configure<TOptions>(…) methods use services.AddSingleton<IConfigureOptions<TOptions>>(…) under the hood, and -- although each uses a different type for the implementation instance -- i was under the impression that the number of registrations for a service type is restricted to one, either by ServiceCollection or ServiceProvider

area-options

Most helpful comment

Configure<TOptions>(Action<TOptions> configureOptions) and OptionsBuilder<TOptions>.Configure(Action<TOptions> configureOptions) will both end up doing the same thing:

services.AddSingleton<IConfigureOptions<TOptions>>(
    new ConfigureNamedOptions<TOptions>(name, configureOptions));

And OptionsBuilder<TOptions>.Bind(IConfiguration config) will actually call Configure<TOptions>(IConfiguration config) directly, so they are also equivalent.

So both APIs are interchangeable. And you can expect the services.Configure calls to continue to work. The options builder API came later to allow for a bit more control with various utility methods. But it’s not a replacement for the direct services.Configure API.

I was under the impression that the number of registrations for a service type is restricted to one

No, you can register any number of implementations for the same service type. While it is true that the normal service resolution will only give you a single instance, which will then be the latest registered dependency for that type, the options framework will resolve an IEnumerable<IConfigureOptions<TOptions>> which will make the DI container provide _all_ registered implementations for that type. So the framework can then work through them in order and apply all registered configuration sources on the options object.

All 5 comments

Configure<TOptions>(Action<TOptions> configureOptions) and OptionsBuilder<TOptions>.Configure(Action<TOptions> configureOptions) will both end up doing the same thing:

services.AddSingleton<IConfigureOptions<TOptions>>(
    new ConfigureNamedOptions<TOptions>(name, configureOptions));

And OptionsBuilder<TOptions>.Bind(IConfiguration config) will actually call Configure<TOptions>(IConfiguration config) directly, so they are also equivalent.

So both APIs are interchangeable. And you can expect the services.Configure calls to continue to work. The options builder API came later to allow for a bit more control with various utility methods. But it’s not a replacement for the direct services.Configure API.

I was under the impression that the number of registrations for a service type is restricted to one

No, you can register any number of implementations for the same service type. While it is true that the normal service resolution will only give you a single instance, which will then be the latest registered dependency for that type, the options framework will resolve an IEnumerable<IConfigureOptions<TOptions>> which will make the DI container provide _all_ registered implementations for that type. So the framework can then work through them in order and apply all registered configuration sources on the options object.

Thanks for the reply @poke . Doesn't resolving IEnumerable<T> require using [Try]AddEnumerable<T>(…) when registering a service? (And this doesn't, because it uses AddSingleton<T>(…))

The only special thing TryAddEnumerable does is that it ensures that you are not accidentally adding the same implementation type multiple times. So when adding something, it checks the collection first.

But no, resolving with IEnumerable<T> will give you all implementations that were registered as T regardless of how that happened. So if you have multiple AdsSingleton<T> calls, you will be able to get all those instances by injecting the enumerable.

If you just inject T, then you will only get the implementation that was registered _last_.

Thanks! That explanation was very helpful. One last question: I assume the "you will only get the implementation that was registered _last_" behavior is dependent on the IServiceProvider implementation, yes?

Hmm, I’m not too sure about other implementations and how much of that behavior is required by IServiceProvider. I think what is true for all DI containers is that the last registration wins. So when you inject T, then the last registration for T will be what you get. This is to allow overwriting registrations.

Some containers may then choose not to make multiple registrations for T available as IEnumerable<T>. I think SimpleInjector does not do this; there, you have to register the collection individually.

But since ASP.NET Core requires this behavior for some of the things to work, I would assume that you can expect this to work for all DI containers that integrate with ASP.NET Core.

Was this page helpful?
0 / 5 - 0 ratings