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<>)));

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