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:
Separation of Concerns: Settings for different parts of the app aren't dependent or coupled with one another.
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 IOptions<TOptions> 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:
Create the following TransientFaultHandlingOptions class:
public sealed class TransientFaultHandlingOptions
{
public bool Enabled { get; set; }
public TimeSpan AutoRetryDelay { get; set; }
}
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.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ConsoleJson.Example;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Configuration.Sources.Clear();
IHostEnvironment env = builder.Environment;
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true);
TransientFaultHandlingOptions options = new();
builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
.Bind(options);
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
using IHost host = builder.Build();
// Application code should start here.
await host.RunAsync();
// <Output>
// Sample output:
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.
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 Configure and bound to configuration:
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 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:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ExampleService(IOptions<TransientFaultHandlingOptions> options)
{
private readonly TransientFaultHandlingOptions _options = options.Value;
public void DisplayValues()
{
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
}
}
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.
Using a generic wrapper type gives you the ability to decouple the lifetime of the option from the dependency injection (DI) container. The IOptions<TOptions>.Value 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 IOptions<TOptions>.Value, 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 do not 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 IOptionsSnapshot<TOptions>, 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.
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:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class MonitorService(IOptionsMonitor<TransientFaultHandlingOptions> monitor)
{
public void DisplayValues()
{
TransientFaultHandlingOptions options = monitor.CurrentValue;
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
}
}
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 IOptionsMonitor<TOptions> 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 is not configurable.
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; }
}
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);
}
}
OptionsBuilder<TOptions> 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.
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:
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 IConfigureNamedOptions<TOptions>, which has a constructor that accepts the generic service types specified.
Options validation
Options validation enables option values to be validated.
The following class binds to the "MyCustomSettingsSection" configuration section and applies a couple of DataAnnotations rules:
using System.ComponentModel.DataAnnotations;
namespace ConsoleJson.Example;
public sealed class SettingsOptions
{
public const string ConfigurationSectionName = "MyCustomSettingsSection";
[Required]
[RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
public required string SiteTitle { get; set; }
[Required]
[Range(0, 1_000,
ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public required int Scale { get; set; }
[Required]
public required int VerbosityLevel { get; set; }
}
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 source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, see our contributor guide.
.NET feedback
.NET is an open source project. Select a link to provide feedback:
Understand and implement dependency injection in an ASP.NET Core app. Use ASP.NET Core's built-in service container to manage dependencies. Register services with the service container.