Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Latest commit

 

History

History
450 lines (312 loc) · 26.2 KB

File metadata and controls

450 lines (312 loc) · 26.2 KB
title Options pattern
description Learn the options pattern to represent groups of related settings in .NET apps. The options pattern uses classes to provide strongly-typed access to settings.
ms.date 10/22/2025
ai-usage ai-assisted

Options pattern in .NET

The options pattern uses classes to provide strongly typed access to groups of related settings. When configuration settings are isolated by scenario into separate classes, the app adheres to two important software engineering principles:

Options also provide a mechanism to validate configuration data. For more information, see the Options validation section.

Bind hierarchical configuration

The preferred way to read related configuration values is using the options pattern. The options pattern is possible through the xref:Microsoft.Extensions.Options.IOptions`1 interface, where the generic type parameter TOptions is constrained to a class. The IOptions<TOptions> can later be provided through dependency injection. For more information, see Dependency injection in .NET.

For example, to read the highlighted configuration values from an appsettings.json file:

:::code language="json" source="snippets/configuration/console-json/appsettings.json" highlight="3-6":::

Create the following TransientFaultHandlingOptions class:

:::code language="csharp" source="snippets/configuration/console-json/TransientFaultHandlingOptions.cs" range="3-7":::

When using the options pattern, an options class:

  • Must be non-abstract with a public parameterless constructor
  • Contain public read-write properties to bind (fields are not bound)

The following code is part of the Program.cs C# file and:

  • Calls ConfigurationBinder.Bind to bind the TransientFaultHandlingOptions class to the "TransientFaultHandlingOptions" section.
  • Displays the configuration data.

:::code language="csharp" source="snippets/configuration/console-json/Program.cs" highlight="15-20" range="1-29":::

In the preceding code, the JSON configuration file has its "TransientFaultHandlingOptions" section bound to the TransientFaultHandlingOptions instance. This hydrates the C# objects properties with those corresponding values from the configuration.

ConfigurationBinder.Get<T> binds and returns the specified type. ConfigurationBinder.Get<T> may be more convenient than using ConfigurationBinder.Bind. The following code shows how to use ConfigurationBinder.Get<T> with the TransientFaultHandlingOptions class:

var options =
    builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
        .Get<TransientFaultHandlingOptions>();

Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");

In the preceding code, the ConfigurationBinder.Get<T> is used to acquire an instance of the TransientFaultHandlingOptions object with its property values populated from the underlying configuration.

Important

The xref:Microsoft.Extensions.Configuration.ConfigurationBinder class exposes several APIs, such as .Bind(object instance) and .Get<T>() that are not constrained to class. When using any of the Options interfaces, you must adhere to aforementioned options class constraints.

An alternative approach when using the options pattern is to bind the "TransientFaultHandlingOptions" section and add it to the dependency injection service container. In the following code, TransientFaultHandlingOptions is added to the service container with xref:Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions.Configure* and bound to configuration:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.Configure<TransientFaultHandlingOptions>(
    builder.Configuration.GetSection(
        key: nameof(TransientFaultHandlingOptions)));

The builder in the preceding example is an instance of xref:Microsoft.Extensions.Hosting.HostApplicationBuilder.

Tip

The key parameter is the name of the configuration section to search for. It does not have to match the name of the type that represents it. For example, you could have a section named "FaultHandling" and it could be represented by the TransientFaultHandlingOptions class. In this instance, you'd pass "FaultHandling" to the xref:Microsoft.Extensions.Configuration.IConfiguration.GetSection* function instead. The nameof operator is used as a convenience when the named section matches the type it corresponds to.

Using the preceding code, the following code reads the position options:

:::code language="csharp" source="snippets/configuration/console-json/ExampleService.cs":::

In the preceding code, changes to the JSON configuration file after the app has started are not read. To read changes after the app has started, use IOptionsSnapshot or IOptionsMonitor to monitor changes as they occur, and react accordingly.

Options interfaces

xref:Microsoft.Extensions.Options.IOptions`1:

xref:Microsoft.Extensions.Options.IOptionsSnapshot`1:

xref:Microsoft.Extensions.Options.IOptionsMonitor`1:

  • Is used to retrieve options and manage options notifications for TOptions instances.
  • Is registered as a Singleton and can be injected into any service lifetime.
  • Supports:

xref:Microsoft.Extensions.Options.IOptionsFactory`1 is responsible for creating new options instances. It has a single xref:Microsoft.Extensions.Options.IOptionsFactory`1.Create* method. The default implementation takes all registered xref:Microsoft.Extensions.Options.IConfigureOptions`1 and xref:Microsoft.Extensions.Options.IPostConfigureOptions`1 and runs all the configurations first, followed by the post-configuration. It distinguishes between xref:Microsoft.Extensions.Options.IConfigureNamedOptions`1 and xref:Microsoft.Extensions.Options.IConfigureOptions`1 and only calls the appropriate interface.

xref:Microsoft.Extensions.Options.IOptionsMonitorCache`1 is used by xref:Microsoft.Extensions.Options.IOptionsMonitor`1 to cache TOptions instances. The xref:Microsoft.Extensions.Options.IOptionsMonitorCache`1 invalidates options instances in the monitor so that the value is recomputed (xref:Microsoft.Extensions.Options.IOptionsMonitorCache`1.TryRemove*). Values can be manually introduced with xref:Microsoft.Extensions.Options.IOptionsMonitorCache`1.TryAdd*. The xref:Microsoft.Extensions.Options.IOptionsMonitorCache`1.Clear* method is used when all named instances should be recreated on demand.

xref:Microsoft.Extensions.Options.IOptionsChangeTokenSource`1 is used to fetch the xref:Microsoft.Extensions.Primitives.IChangeToken that tracks changes to the underlying TOptions instance. For more information on change-token primitives, see Change notifications.

Options interfaces benefits

Using a generic wrapper type gives you the ability to decouple the lifetime of the option from the dependency injection (DI) container. The xref:Microsoft.Extensions.Options.IOptions`1.Value?displayProperty=nameWithType interface provides a layer of abstraction, including generic constraints, on your options type. This provides the following benefits:

  • The evaluation of the T configuration instance is deferred to the accessing of xref:Microsoft.Extensions.Options.IOptions`1.Value?displayProperty=nameWithType, rather than when it is injected. This is important because you can consume the T option from various places and choose the lifetime semantics without changing anything about T.
  • When registering options of type T, you don't need to explicitly register the T type. This is a convenience when you're authoring a library with simple defaults, and you don't want to force the caller to register options into the DI container with a specific lifetime.
  • From the perspective of the API, it allows for constraints on the type T (in this case, T is constrained to a reference type).

Use IOptionsSnapshot to read updated data

When you use xref:Microsoft.Extensions.Options.IOptionsSnapshot`1, options are computed once per request when accessed and are cached for the lifetime of the request. Changes to the configuration are read after the app starts when using configuration providers that support reading updated configuration values.

The difference between IOptionsMonitor and IOptionsSnapshot is that:

  • IOptionsMonitor is a singleton service that retrieves current option values at any time, which is especially useful in singleton dependencies.
  • IOptionsSnapshot is a scoped service and provides a snapshot of the options at the time the IOptionsSnapshot<T> object is constructed. Options snapshots are designed for use with transient and scoped dependencies.

The following code uses xref:Microsoft.Extensions.Options.IOptionsSnapshot`1.

:::code language="csharp" source="snippets/configuration/console-json/ScopedService.cs":::

The following code registers a configuration instance which TransientFaultHandlingOptions binds against:

builder.Services
    .Configure<TransientFaultHandlingOptions>(
        configurationRoot.GetSection(
            nameof(TransientFaultHandlingOptions)));

In the preceding code, the Configure<TOptions> method is used to register a configuration instance that TOptions will bind against, and updates the options when the configuration changes.

IOptionsMonitor

The IOptionsMonitor type supports change notifications and enables scenarios where your app may need to respond to configuration source changes dynamically. This is useful when you need to react to changes in configuration data after the app has started. Change notifications are only supported for file-system based configuration providers, such as the following:

To use the options monitor, options objects are configured in the same way from a configuration section.

builder.Services
    .Configure<TransientFaultHandlingOptions>(
        configurationRoot.GetSection(
            nameof(TransientFaultHandlingOptions)));

The following example uses xref:Microsoft.Extensions.Options.IOptionsMonitor`1:

:::code language="csharp" source="snippets/configuration/console-json/MonitorService.cs":::

In the preceding code, changes to the JSON configuration file after the app has started are read.

Tip

Some file systems, such as Docker containers and network shares, may not reliably send change notifications. When using the xref:Microsoft.Extensions.Options.IOptionsMonitor`1 interface in these environments, set the DOTNET_USE_POLLING_FILE_WATCHER environment variable to 1 or true to poll the file system for changes. The interval at which changes are polled is every four seconds and isn't configurable.

For more information on Docker containers, see Containerize a .NET app.

Named options support using IConfigureNamedOptions

Named options:

  • Are useful when multiple configuration sections bind to the same properties.
  • Are case-sensitive.

Consider the following appsettings.json file:

{
  "Features": {
    "Personalize": {
      "Enabled": true,
      "ApiKey": "aGEgaGEgeW91IHRob3VnaHQgdGhhdCB3YXMgcmVhbGx5IHNvbWV0aGluZw=="
    },
    "WeatherStation": {
      "Enabled": true,
      "ApiKey": "QXJlIHlvdSBhdHRlbXB0aW5nIHRvIGhhY2sgdXM/"
    }
  }
}

Rather than creating two classes to bind Features:Personalize and Features:WeatherStation, the following class is used for each section:

public class Features
{
    public const string Personalize = nameof(Personalize);
    public const string WeatherStation = nameof(WeatherStation);

    public bool Enabled { get; set; }
    public string ApiKey { get; set; }
}

The following code configures the named options:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Omitted for brevity...

builder.Services.Configure<Features>(
    Features.Personalize,
    builder.Configuration.GetSection("Features:Personalize"));

builder.Services.Configure<Features>(
    Features.WeatherStation,
    builder.Configuration.GetSection("Features:WeatherStation"));

The following code displays the named options:

public sealed class Service
{
    private readonly Features _personalizeFeature;
    private readonly Features _weatherStationFeature;

    public Service(IOptionsSnapshot<Features> namedOptionsAccessor)
    {
        _personalizeFeature = namedOptionsAccessor.Get(Features.Personalize);
        _weatherStationFeature = namedOptionsAccessor.Get(Features.WeatherStation);
    }
}

All options are named instances. xref:Microsoft.Extensions.Options.IConfigureOptions`1 instances are treated as targeting the Options.DefaultName instance, which is string.Empty. xref:Microsoft.Extensions.Options.IConfigureNamedOptions`1 also implements xref:Microsoft.Extensions.Options.IConfigureOptions`1. The default implementation of the xref:Microsoft.Extensions.Options.IOptionsFactory`1 has logic to use each appropriately. The null named option is used to target all of the named instances instead of a specific named instance. xref:Microsoft.Extensions.DependencyInjection.OptionsServiceCollectionExtensions.ConfigureAll* and xref:Microsoft.Extensions.DependencyInjection.OptionsServiceCollectionExtensions.PostConfigureAll* use this convention.

OptionsBuilder API

xref:Microsoft.Extensions.Options.OptionsBuilder`1 is used to configure TOptions instances. OptionsBuilder streamlines creating named options as it's only a single parameter to the initial AddOptions<TOptions>(string optionsName) call instead of appearing in all of the subsequent calls. Options validation and the ConfigureOptions overloads that accept service dependencies are only available via OptionsBuilder.

OptionsBuilder is used in the Options validation section.

Use DI services to configure options

When you're configuring options, you can use dependency injection to access registered services, and use them to configure options. This is useful when you need to access services to configure options. Services can be accessed from DI while configuring options in two ways:

  • Pass a configuration delegate to Configure on OptionsBuilder<TOptions>. OptionsBuilder<TOptions> provides overloads of Configure that allow use of up to five services to configure options:

    builder.Services
        .AddOptions<MyOptions>("optionalName")
        .Configure<ExampleService, ScopedService, MonitorService>(
            (options, es, ss, ms) =>
                options.Property = DoSomethingWith(es, ss, ms));
  • Create a type that implements xref:Microsoft.Extensions.Options.IConfigureOptions`1 or xref:Microsoft.Extensions.Options.IConfigureNamedOptions`1 and register the type as a service.

It's recommended to pass a configuration delegate to Configure, since creating a service is more complex. Creating a type is equivalent to what the framework does when calling Configure. Calling Configure registers a transient generic xref:Microsoft.Extensions.Options.IConfigureNamedOptions`1, which has a constructor that accepts the generic service types specified.

Options validation

Options validation enables option values to be validated.

Consider the following appsettings.json file:

{
  "MyCustomSettingsSection": {
    "SiteTitle": "Amazing docs from Awesome people!",
    "Scale": 10,
    "VerbosityLevel": 32
  }
}

The following class binds to the "MyCustomSettingsSection" configuration section and applies a couple of DataAnnotations rules:

:::code language="csharp" source="snippets/configuration/console-json/SettingsOptions.cs":::

In the preceding SettingsOptions class, the ConfigurationSectionName property contains the name of the configuration section to bind to. In this scenario, the options object provides the name of its configuration section.

Tip

The configuration section name is independent of the configuration object that it's binding to. In other words, a configuration section named "FooBarOptions" can be bound to an options object named ZedOptions. Although it might be common to name them the same, it's not necessary and can actually cause name conflicts.

The following code:

  • Calls xref:Microsoft.Extensions.DependencyInjection.OptionsServiceCollectionExtensions.AddOptions* to get an OptionsBuilder<TOptions> that binds to the SettingsOptions class.
  • Calls xref:Microsoft.Extensions.DependencyInjection.OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations* to enable validation using DataAnnotations.
builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(builder.Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations();

The ValidateDataAnnotations extension method is defined in the Microsoft.Extensions.Options.DataAnnotations NuGet package.

The following code displays the configuration values or reports validation errors:

:::code language="csharp" source="snippets/configuration/console-json/ValidationService.cs":::

The following code applies a more complex validation rule using a delegate:

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(builder.Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.");

The validation occurs at runtime, but you can configure it to occur at startup by instead chaining a call to ValidateOnStart:

builder.Services
    .AddOptions<SettingsOptions>()
    .Bind(builder.Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.")
    .ValidateOnStart();

To enable validation on start for a specific options type, use the xref:Microsoft.Extensions.DependencyInjection.OptionsServiceCollectionExtensions.AddOptionsWithValidateOnStart``1(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String) API:

builder.Services
    .AddOptionsWithValidateOnStart<SettingsOptions>()
    .Bind(builder.Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        if (config.Scale != 0)
        {
            return config.VerbosityLevel > config.Scale;
        }

        return true;
    }, "VerbosityLevel must be > than Scale.");

IValidateOptions for complex validation

The following class implements xref:Microsoft.Extensions.Options.IValidateOptions`1:

:::code language="csharp" source="snippets/configuration/console-json/ValidateSettingsOptions.cs":::

IValidateOptions enables moving the validation code into a class.

Note

This example code relies on the Microsoft.Extensions.Configuration.Json NuGet package.

Using the preceding code, validation is enabled when configuring services with the following code:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Omitted for brevity...

builder.Services.Configure<SettingsOptions>(
    builder.Configuration.GetSection(
        SettingsOptions.ConfigurationSectionName));

builder.Services.TryAddEnumerable(
    ServiceDescriptor.Singleton
        <IValidateOptions<SettingsOptions>, ValidateSettingsOptions>());

Recursive validation with ValidateObjectMembers and ValidateEnumeratedItems

By default, DataAnnotations validation only validates the properties of the options class itself. It doesn't recursively validate nested objects or items in collections. To enable recursive validation, use the xref:Microsoft.Extensions.Options.ValidateObjectMembersAttribute and xref:Microsoft.Extensions.Options.ValidateEnumeratedItemsAttribute attributes.

  • The xref:Microsoft.Extensions.Options.ValidateObjectMembersAttribute attribute enables recursive validation of nested objects.
  • The xref:Microsoft.Extensions.Options.ValidateEnumeratedItemsAttribute attribute enables recursive validation of enumerable objects.

Consider the following nested options classes:

:::code language="csharp" source="snippets/configuration/options-recursive-validation/DatabaseOptions.cs" id="DatabaseOptions":::

:::code language="csharp" source="snippets/configuration/options-recursive-validation/ServerOptions.cs" id="ServerOptions":::

:::code language="csharp" source="snippets/configuration/options-recursive-validation/ApplicationOptions.cs" id="ApplicationOptionsWithAttribute":::

In the preceding code, the Database property is a nested object of type DatabaseOptions.

  • Without the [ValidateObjectMembers] attribute applied to the DatabaseOptions property, the validation attributes on its properties (like [Required] on ConnectionString) would not be evaluated. With [ValidateObjectMembers] applied, the validation also recurses into the Database property and validates its members according to their DataAnnotations attributes.
  • Without the [ValidateEnumeratedItems] attribute applied to the Servers collection property, the validation attributes on individual ServerOptions items would not be evaluated. With the [ValidateEnumeratedItems] attribute applied, each ServerOptions item in the list is validated according to its DataAnnotations attributes.

Tip

Both ValidateObjectMembersAttribute and ValidateEnumeratedItemsAttribute work with the compile-time options validation source generator for improved performance. For more information, see Compile-time options validation source generation.

Options post-configuration

Set post-configuration with xref:Microsoft.Extensions.Options.IPostConfigureOptions`1. Post-configuration runs after all xref:Microsoft.Extensions.Options.IConfigureOptions`1 configuration occurs, and can be useful in scenarios when you need to override configuration:

builder.Services.PostConfigure<CustomOptions>(customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

xref:Microsoft.Extensions.Options.IPostConfigureOptions`1.PostConfigure* is available to post-configure named options:

builder.Services.PostConfigure<CustomOptions>("named_options_1", customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

Use xref:Microsoft.Extensions.DependencyInjection.OptionsServiceCollectionExtensions.PostConfigureAll* to post-configure all configuration instances:

builder.Services.PostConfigureAll<CustomOptions>(customOptions =>
{
    customOptions.Option1 = "post_configured_option1_value";
});

See also