Overview
The options pattern uses classes to provide strongly-typed access to groups of related settings. It uses configuration files like configuration in .NET.
This pattern supports a mechanism to validate configuration data.
Configuring and Using Options
High-level Process:
- Create options classes that model the configuration data
- Add instances of
Option<TOption>
to the DI container - Inject the
Option<TOption>
instances into classes that need them
1. Create Options Classes
Options classes must:
- Be non-abstract
- Have a public, parameterless constructor
- Contain public, read-write properties to bind (fields are not bound)
See notes on Configuration for an example of how to model a class after JSON-based configuration.
2. Add Options to DI Container
appsettings.json
{
"SecretKey": "Secret key value",
"SomeOptions": {
"Enabled": true,
"AutoRetryDelay": "00:00:07"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.Configure<SomeOptions>(builder.Configuration.GetSection(key: nameof(SomeOptions)));
3. Inject Options into Classes
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ExampleService
{
private readonly SomeOptions _options;
public ExampleService(IOptions<SomeOptions> options) =>
_options = options.Value;
public void DisplayValues()
{
Console.WriteLine($"SomeOptions.Enabled={_options.Enabled}");
Console.WriteLine($"SomeOptions.AutoRetryDelay={_options.AutoRetryDelay}");
}
}
Other Ways to Configure Options
Using Configuration.GetSection
SomeOptions options = new();
builder.Configuration.GetSection(nameof(SomeOptions)) // returns the configuration section
.Bind(options); // binds the configuration section to the options object
Console.WriteLine($"SomeOptions.Enabled={options.Enabled}");
Console.WriteLine($"SomeOptions.AutoRetryDelay={options.AutoRetryDelay}");
Using Configuration.Get
var options =
builder.Configuration.GetSection(nameof(SomeOptions)) // returns the configuration section
.Get<SomeOptions>(); // binds the configuration section to the options object and
// returns the object.
Console.WriteLine($"SomeOptions.Enabled={options.Enabled}");
Console.WriteLine($"SomeOptions.AutoRetryDelay={options.AutoRetryDelay}");
Options Interfaces
Interface | Can read config data after app starts | Supports named options | Service lifetime |
---|---|---|---|
IOptions<T> | No | Yes | Singleton |
IOptionsSnapshot<T> | Yes | Yes | Scoped |
IOptionsMonitor<T> | Yes | Yes | Singleton |
IOptionsSnapshot<T>
vs IOptionsMonitor<T>
Use IOptionsSnapshot
to read updated configuration data. With this interface, options are computed once per request when accessed and are cached for the lifetime of the request.
IOptionsSnapshot
is a scoped service and provides a snapshot of the options at the time the object is constructed. It is designed for use with transient and scoped dependencies.
IOptionsMonitor
is a singleton service that retrieves the current option values at any time. It is designed for use with singleton dependencies.
Using IOptionsMonitor<T>
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class MonitorService
{
private readonly IOptionsMonitor<SomeOptions> _monitor;
public MonitorService(IOptionsMonitor<SomeOptions> monitor) =>
_monitor = monitor;
public void DisplayValues()
{
SomeOptions options = _monitor.CurrentValue;
Console.WriteLine($"SomeOptions.Enabled={options.Enabled}");
Console.WriteLine($"SomeOptions.AutoRetryDelay={options.AutoRetryDelay}");
}
}
Named Options
Use named options when multiple configuration sections bind to the same properties. Consider:
appsettings.json
{
"Features": {
"Personalize": {
"Enabled": true,
"ApiKey": "aGEgaGEgeW91IHRob3VnaHQgdGhhdCB3YXMgcmVhbGx5IHNvbWV0aGluZw=="
},
"WeatherStation": {
"Enabled": true,
"ApiKey": "QXJlIHlvdSBhdHRlbXB0aW5nIHRvIGhhY2sgdXM/"
}
}
}
Notice how both Personalize
and WeatherStation
have properties of the same name. Instead of creating two classes to bind the options, this class can be used for both sections:
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; }
}
Configuring 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"));
Using Named Options
public class sealed Service
{
private readonly Features _personalizeFeature;
private readonly Features _weatherStationFeature;
public Service(IOptionsSnapshot<Features> namedOptionsAccessor)
{
_personalizeFeature = namedOptionsAccessor.Get(Features.Personalize);
_weatherStationFeature = namedOptionsAccessor.Get(Features.WeatherStation);
}
}
Options Validation
Options can be validated with System.ComponentModel.DataAnnotations
.
By default, validation is performed the first time an options instance is created. Validation can be performed eagerly (see notes in step 3).
High-level Process:
- Annotate the options model with the appropriate data annotations
- Add support for options validation
- Enable validation
1. Annotate the Options Model
Consider this configuration file:
{
"MyConfig": {
"Key1": "My Key One",
"Key2": 10,
"Key3": 32
}
}
Here is the model:
using System.ComponentModel.DataAnnotations;
public class MyConfigOptions
{
public const string MyConfig = "MyConfig";
[RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
public string Key1 { get; set; }
[Range(0, 1000, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public int Key2 { get; set; }
public int Key3 { get; set; }
}
2. Add Support for Options Validation
Note: this package is already included in web apps:
dotnet add package Microsoft.Extensions.Options.DataAnnotations
3. Enable Validation
using Microsoft.Extensions.Options.DataAnnotations; // not required for web apps
builder.Services.AddOptions<SomeOptions>()
.Bind(Configuration.GetSection(nameof(SomeOptions)))
.ValidateDataAnnotations();
//.ValidateOnStart(); <-- Optionally, validate eagerly
Validation Sample
A sample class that displays the configuration values or the validation errors:
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ValidationService
{
private readonly ILogger<ValidationService> _logger;
private readonly IOptions<MyConfigOptions> _config;
public ValidationService(ILogger<ValidationService> logger, IOptions<MyConfigOptions> config)
{
_config = config;
_logger = logger;
try
{
MyConfigOptions options = _config.Value;
}
catch (OptionsValidationException ex)
{
foreach (string failure in ex.Failures)
{
_logger.LogError(failure);
}
}
}
}
Or, perform complex validation using a delegate:
builder.Services
.AddOptions<MyConfigOptions>()
.Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Scale != 0)
{
// Validate must return true if validation succeeds, false otherwise
return config.VerbosityLevel > config.Scale;
}
return true;
}, "VerbosityLevel must be > than Scale."); // the error message
Complex Validation with IValidateOptions<T>
For complex validation scenarios, use IValidateOptions<TOptions>
:
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
sealed partial class ValidateMyConfigOptions : IValidateOptions<MyConfigOptions>
{
public MyConfigOptions? _settings { get; private set; }
public ValidateMyConfigOptions(IConfiguration config) =>
_settings = config.GetSection(MyConfigOptions.MyConfig)
.Get<MyConfigOptions>();
public ValidateOptionsResult Validate(string? name, MyConfigOptions options)
{
StringBuilder? failure = null;
if (!ValidationRegex().IsMatch(options.SiteTitle))
(failure ??= new()).AppendLine($"{options.SiteTitle} doesn't match RegEx");
if (options.Scale is < 0 or > 1_000)
(failure ??= new()).AppendLine($"{options.Scale} isn't within Range 0 - 1000");
if (_settings is { Scale: 0 } && _settings.VerbosityLevel <= _settings.Scale)
(failure ??= new()).AppendLine("VerbosityLevel must be > than Scale.");
return failure is not null ?
? ValidateOptionsResult.Fail(failure.ToString())
: ValidateOptionsResult.Success;
}
[GeneratedRegex("^[a-zA-Z''-'\\s]{1,40}$")]
private static partial Regex ValidationRegex();
}
Enable validation when using IValidateOptions<TOptions>
like this:
builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(MyConfigOptions.MyConfig));
builder.Services.AddSingleton<IValidateOptions<MyConfigOptions>, MyConfigValidation>();
Complex Validation with IValidatableObject
Class-level validation can be performed on the options class itself by:
- Implementing
IValidatableObject
and itsValidate
method on the class - Calling
ValidateDataAnnotations
inProgram.cs
Complex Validation with CustomValidationAttribute
Mark the member to be validated with [CustomValidation]
and create a custom validation method:
When annotating with the CustomValidation
attribute, its first argument is the type of the validation method that will be used, and its second argument is the name of the validation method that will be used:
public class MyConfigOptions
{
public const string MyConfig = "MyConfig";
[RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
public string Key1 { get; set; }
[Range(0, 1000, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public int Key2 { get; set; }
[CustomValidation(typeof(Validate), nameof(IsKey3NonNegative))]
public int Key3 { get; set; }
}
The custom validation method must:
- Be public
- Be static
- Return ValidationResult
- Accept a parameter of the same type as the member being validated
public class Validate
{
public static ValidationResult IsKey3NonNegative(int key)
{
return key < 0
? new ValidationResult("key cannot be negative")
: ValidationResult.Success;
}
}
Post-configuration
Post-configuration runs after all options configuration occurs. It is useful when you need to override configuration without changing the configuration file itself:
builder.Services.PostConfigure<CustomOptions>(customOptions =>
{
customOptions.Option1 = "post_configured_option1_value";
});
Or, with named options:
builder.Services.PostConfigure<CustomOptions>("named_options_1", customOptions =>
{
customOptions.Option1 = "post_configured_option1_value";
});
Or, to post-configure all configuration instances:
builder.Services.PostConfigureAll<CustomOptions>(customOptions =>
{
customOptions.Option1 = "post_configured_option1_value";
});
Accessing Options in Program.cs
Use GetRequiredService:
var app = builder.Build();
var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>().CurrentValue.Option1;