Options are just DTOs (data transfer objects). It is that simple! (in modern programming, it is good pratice to make DTOs immutable, though this is currently not supported)
// Parameterless constructor required!public sealed class MyOptions{public required Text { get; set; }}
The options are registered to the dependency injection (DI) container on startup.
services.AddOptions<MyOptions>();.Configure(myOptions =>{myOptions.Text = "Hello World!";});
Under the hood, this will be registered as following:
services.AddOptions();services.AddTransient<IConfigureOptions<MyOptions>>(new ConfigureOptions<MyOptions>(myOptions =>{myOptions.Text = "Hello World!";}));
'services.AddOptions()' will register the follwing CORE services:
services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(UnnamedOptionsManager<>)));services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
- IOptions<TOptions> - singleton options instance that does NOT react to changes
- IOptionsSnapshot<TOptions> - scoped options instance that is created once per HTTP request (in ASP.NET Core)
- IOptionsMonitor<TOptions> - singleton options instance that DOES reacts to changes by a registered IOptionsChangeTokenSource<TOptions>
- IOptionsFactory<TOptions> - creates the options instance by 'Activator.CreateInstance<TOptions>()' and calling 'IConfigureOptions<TOptions>.Configure(optionsInstance)' registered by DI
- IOptionsMonitorCache<TOptions> - caching mechanism to improve performance
To retrieve and consume the options, we do that through one of the following interface:
- IOptions<TOptions> by accessing the Value property
- IOptionsSnapshot<TOptions> by accessing the Value property (inherited from IOptions<Options>)
- IOptionsMonitor<TOptions> by accessing the CurrentValue property
var builder = WebApplication.CreateBuilder(args);// When omitting the following options registration,// the IOptionsFactory<TOptions> will just create an (empty) instance of 'TOptions'// with NULL/default property values.services.AddOptions<MyOptions>();.Configure(myOptions =>{myOptions.Text = "Hello World!";});var app = builder.Build();app.MapGet("/", (IOptions<MyOptions> myOptions, IOptionsSnapshot<MyOptions> myOptionsSnapshot, IOptionsMonitor<MyOptions> myOptionsMonitor) =>{return $"{myOptions.Value.Text},{myOptionsSnapshot.Value.Text},{myOptionsMonitor.CurrentValue.Text};}await app.RunAsync();
Important: the Value/CurrentValue property lazily calls 'IOptionsFactory<TOptions>.Create()' on first access and caches the result. This means that any error inside the '.Configure(myOptions => { /*some exception*/ })' delegate will throw 'later' at runtime. To avoid that we can call 'ValidateOnStart()':
services.AddOptions<MyOptions>();.Configure(myOptions =>{myOptions.Text = "Hello World!";}).ValidateOnStart();
Options validation
Validation of options especially makes sense for library authors or when the option values come from e.g. JSON file.
services.AddOptions<MyOptions>();.Configure(myOptions =>{myOptions.Text = "Hello World!";}).Validate(myOptions =>{return !string.IsNullOrWhiteSpace(myOptions.Text);}).ValidateOnStart();
Configuration vs Options
Configurations (IConfiguration) and Options are two separate concepts.
Configurations are basically just key/value pairs (Dictionary<string, string?>) coming from a configuration source like a JSON file, XML file, INI file or even from InMemory and much more.
The Options API provides an extension to bind an IConfiguration to options.
services.AddOptions<MyOptions>();.Configure(app.Configuration.GetSection("MyOptions"));
appsettings.json
{"MyOptions": {"Text": "Hello World!"}}