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!"
}
}