Reputation: 12685
It seems to me that it's a bad idea to have a domain service require an instance of IOptions<T>
to pass it configuration. Now I've got to pull additional (unnecessary?) dependencies into the library. I've seen lots of examples of injecting IOptions
all over the web, but I fail to see the added benefit of it.
Why not just inject that actual POCO into the service?
services.AddTransient<IConnectionResolver>(x =>
{
var appSettings = x.GetService<IOptions<AppSettings>>();
return new ConnectionResolver(appSettings.Value);
});
Or even use this mechanism:
AppSettings appSettings = new AppSettings();
Configuration.GetSection("AppSettings").Bind(appSettings);
services.AddTransient<IConnectionResolver>(x =>
{
return new ConnectionResolver(appSettings.SomeValue);
});
Usage of the settings:
public class MyConnectionResolver
{
// Why this?
public MyConnectionResolver(IOptions<AppSettings> appSettings)
{
...
}
// Why not this?
public MyConnectionResolver(AppSettings appSettings)
{
...
}
// Or this
public MyConnectionResolver(IAppSettings appSettings)
{
...
}
}
Why the additional dependencies? What does IOptions
buy me instead of the old school way of injecting stuff?
Upvotes: 65
Views: 50762
Reputation: 2096
Let's dissect the question into two parts:
AppSettings
directly as a service?AddOptions
, when to inject with IOptions*
interfaces?Microsoft now recommendations to use <nullable>true</nullable>
in new projects.
If your AppSettings
contain nullable fields, the compiler will warn about it with CS8618 to add default values or the required
keyword.
So your mechanism with an empty dummy instance new AppSettings();
won't work with required
fields.
You can still do it without a dummy thanks to ConfigurationBinder.Get
.AddTransient(sp => sp
.GetService<IConfiguration()!
.GetSection(nameof(AppSettings))
.Get<AppSettings>()!)
But that leaves you without any form of null reference checks and validation.
The benefit of using AddOptions
is, that it returns an OptionsBuilder with ease of use extension methods and validation.
BindConfiguration is pretty neat, which reads a config section and binds it in one go:
.AddOptions<AppSettings>()
.BindConfiguration(nameof(AppSettings))
But the most compelling reason for the OptionsBuilder
is the validations.
Validations can be done with DataAnnotations by calling ValidateDataAnnotations or with a custom callback via Validate.
ValidateDataAnnotations
is not compatible with AoT/Trimming, but can be replaced by IValidateOptions with a compile-time generated [OptionsValidator] class.If you want to use the same options structure for multiple components it's much better to use named options.
It's possible to do it without named options by using named generics instead.
You would have to create a generic dummy class on top of your options.
Here's an example based on Language Components:
internal class LanguageSettings<Language> : LanguageSettings { }
.AddOptions<LanguageSettings<English>>()
.BindConfiguration(nameof(English));
.AddOptions<LanguageSettings<Dutch>>()
.BindConfiguration(nameof(Dutch));
// then inject it as
internal class English(LanguageSettings<English> settings)
Since this is quite repetetive you'd like to use some generic method to add your components. But dealing with LanguageSettings<TLanguage>
will break AoT functionality.
If you need validations, you would also have to add them separately to each settings version.
If you use named options, its enough to add validations once per options structure as IValidateOptions
.
Add a compile-time generated Validator.
[OptionsValidator] internal partial class ValidateLanguageSettings() : IValidateOptions<LanguageSettings> {}
Add validations once:
.AddSingleton<IValidateOptions<LanguageSettings>, ValidateLanguageSettings>()
Use a generic extension method to add multiple components, which wouldn't have been possible with AoT and without named options:
internal static IServiceCollection AddLanguage<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TLanguage>(this IServiceCollection services)
where TLanguage : Language
=> services
.AddOptions<LanguageSettings>(typeof(TLanguage).Name)
.BindConfiguration(typeof(TLanguage).Name)
.Services
.AddSingleton<TLanguage>();
.AddLanguage<English>()
.AddLanguage<Dutch>()
Use a base class to automatically integrate the matching settings:
internal class Language
{
private readonly LanguageSettings _settings;
protected LanguageSettings Settings => _settings;
internal Language(IOptionsMonitor<LanguageSettings> settings)
{
_settings = settings.Get(this.GetType().Name);
}
}
Now inject it like that:
internal class English(IOptionsMonitor<LanguageSettings> settings) : Language(settings)
This also eliminates copy paste mistakes, because the base class selects the right settings for you!
If you need validations or named options I would also recommend to use AddOptions/Configure for other settings where you don't need it for consistency along projects.
Keith Jackson mentioned conflicts with hybrid projects, but you can see more and more devs adapting the DI pattern in non ASP.NET projects. I converted all my Console Apps to use DI so I can stick to the options pattern.
For static options I use the same approach as Shahar Shokrani but with singletons to read and validate at startup (or you use ValidateOnStart) and having just one instance if it's static anyway:
services.AddSingleton(cfg => cfg.GetService<IOptions<AppSettings>>()!.Value);
You shouldn't worry about additional dependencies here. I only do this for design reasons, because with primary constructors you don't have to add an extra field for the options and you eliminate accessing the .Value
field. Services configuration is ugly anyway, but I like my classes clean and pretty!
Use IOptionsMonitor
or IOptionsSnapshot
if you need config changes in real time OR multiple named options of the same type
Upvotes: 0
Reputation: 3259
I would recommend avoiding it wherever possible. I used to really like IOptions
back when I was working primarily with core but as soon as you're in a hybrid framework scenario it's enough to drive you spare.
I found a similar issue with ILogger
- Code that should work across frameworks won't because I just can't get it to bind properly as the code is too dependent on the DI framework.
Upvotes: 2
Reputation: 8762
In order to avoid constructors pollution of IOptions<>
:
With this two simple lines in startup.cs
inside ConfigureServices
you can inject the IOptions
value like:
public void ConfigureServices(IServiceCollection services)
{
//...
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
services.AddScoped(cfg => cfg.GetService<IOptions<AppSettings>>().Value);
}
And then use with:
public MyService(AppSettings appSettings)
{
...
}
Upvotes: 22
Reputation: 64259
Technically nothing prevents you from registering your POCO classes with ASP.NET Core's Dependency Injection or create a wrapper class and return the IOption<T>.Value
from it.
But you will lose the advanced features of the Options package, namely to get them updated automatically when the source changes as you can see in the source here.
As you can see in that code example, if you register your options via services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
it will read and bind the settings from appsettings.json into the model and additionally track it for changes. When appsettings.json is edited, and will rebind the model with the new values as seen here.
Of course you need to decide for yourself, if you want to leak a bit of infrastructure into your domain or pass on the extra features offered by the Microsoft.Extensions.Options
package. It's a pretty small package which is not tied to ASP.NET Core, so it can be used independent of it.
The Microsoft.Extensions.Options
package is small enough that it only contains abstractions and the concrete services.Configure
overload which for IConfiguration
(which is closer tied to how the configuration is obtained, command line, json, environment, azure key vault, etc.) is a separate package.
So all in all, its dependencies on "infrastructure" is pretty limited.
Upvotes: 49
Reputation: 11
You can do something like this:
services.AddTransient(
o => ConfigurationBinder.Get<AppSettings>(Configuration.GetSection("AppSettings")
);
Using Net.Core v.2.2, it's worked for me.
Or then, use IOption<T>.Value
It would look something like this
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
Upvotes: 1
Reputation: 19864
I can't stand the IOptions recommendation either. It's a crappy design to force this on developers. IOptions should be clearly documented as optional, oh the irony.
This is what I do for my configuraition values
var mySettings = new MySettings();
Configuration.GetSection("Key").Bind(mySettings);
services.AddTransient(p => new MyService(mySettings));
You retain strong typing and don't need need to use IOptions in your services/libraries.
Upvotes: 3
Reputation: 2316
While using IOption
is the official way of doing things, I just can't seem to move past the fact that our external libraries shouldn't need to know anything about the DI container or the way it is implemented. IOption
seems to violate this concept since we are now telling our class library something about the way the DI container will be injecting settings - we should just be injecting a POCO or interface defined by that class.
This annoyed me badly enough that I've written a utility to inject a POCO into my class library populated with values from an appSettings.json section. Add the following class to your application project:
public static class ConfigurationHelper
{
public static T GetObjectFromConfigSection<T>(
this IConfigurationRoot configurationRoot,
string configSection) where T : new()
{
var result = new T();
foreach (var propInfo in typeof(T).GetProperties())
{
var propertyType = propInfo.PropertyType;
if (propInfo?.CanWrite ?? false)
{
var value = Convert.ChangeType(configurationRoot.GetValue<string>($"{configSection}:{propInfo.Name}"), propInfo.PropertyType);
propInfo.SetValue(result, value, null);
}
}
return result;
}
}
There's probably some enhancements that could be made, but it worked well when I tested it with simple string and integer values. Here's an example of where I used this in the application project's Startup.cs -> ConfigureServices method for a settings class named DataStoreConfiguration
and an appSettings.json section by the same name:
services.AddSingleton<DataStoreConfiguration>((_) =>
Configuration.GetObjectFromConfigSection<DataStoreConfiguration>("DataStoreConfiguration"));
The appSettings.json config looked something like the following:
{
"DataStoreConfiguration": {
"ConnectionString": "Server=Server-goes-here;Database=My-database-name;Trusted_Connection=True;MultipleActiveResultSets=true",
"MeaningOfLifeInt" : "42"
},
"AnotherSection" : {
"Prop1" : "etc."
}
}
The DataStoreConfiguration
class was defined in my library project and looked like the following:
namespace MyLibrary.DataAccessors
{
public class DataStoreConfiguration
{
public string ConnectionString { get; set; }
public int MeaningOfLifeInt { get; set; }
}
}
With this application and libraries configuration, I was able to inject a concrete instance of DataStoreConfiguration directly into my library using constructor injection without the IOption
wrapper:
using System.Data.SqlClient;
namespace MyLibrary.DataAccessors
{
public class DatabaseConnectionFactory : IDatabaseConnectionFactory
{
private readonly DataStoreConfiguration dataStoreConfiguration;
public DatabaseConnectionFactory(
DataStoreConfiguration dataStoreConfiguration)
{
// Here we inject a concrete instance of DataStoreConfiguration
// without the `IOption` wrapper.
this.dataStoreConfiguration = dataStoreConfiguration;
}
public SqlConnection NewConnection()
{
return new SqlConnection(dataStoreConfiguration.ConnectionString);
}
}
}
Decoupling is an important consideration for DI, so I'm not sure why Microsoft have funnelled users into coupling their class libraries to an external dependency like IOptions
, no matter how trivial it seems or what benefits it supposedly provides. I would also suggest that some of the benefits of IOptions
seem like over-engineering. For example, it allows me to dynamically change configuration and have the changes tracked - I've used three other DI containers which included this feature and I've never used it once... Meanwhile, I can virtually guarantee you that teams will want to inject POCO classes or interfaces into libraries for their settings to replace ConfigurationManager
, and seasoned developers will not be happy about an extraneous wrapper interface. I hope a utility similar to what I have described here is included in future versions of ASP.NET Core OR that someone provides me with a convincing argument for why I'm wrong.
Upvotes: 9