Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Note
This isn't the latest version of this article. For the current release, see the .NET 10 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 10 version of this article.
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:
- Encapsulation: Classes that depend on configuration settings depend only on the configuration settings that they use.
- Separation of Concerns: Settings for different parts of the app aren't dependent or coupled to one another.
Options also provide a mechanism to validate configuration data, which is described in the Options validation section.
This article provides information on the options pattern in ASP.NET Core. For information on using the options pattern in console apps, see Options pattern in .NET.
The examples in this article rely on a general understanding of injecting services into classes. For more information, see Dependency injection in ASP.NET Core. Examples are based on Blazor's Razor components. To see Razor Pages examples, see the 7.0 version of this article. The examples in the .NET 8 or later versions of this article use primary constructors (Primary constructors (C# Guide)) and nullable reference types (NRTs) with .NET compiler null-state static analysis.
How to use the options pattern
Consider the following JSON configuration data from an app settings file (for example, appsettings.json), which includes related data for an employee's name and title in an organization's position:
"Position": {
"Name": "Joe Smith",
"Title": "Editor"
}
The following PositionOptions options class:
- Is a POCO, a simple .NET class with properties. An options class must not be an abstract class.
- Has public read-write properties that match corresponding entries in the configuration data.
- Does not have its field (
Position) bound. ThePositionfield is used to avoid hardcoding the string"Position"in the app when binding the class to a configuration provider.
PositionOptions.cs:
public class PositionOptions
{
public const string Position = "Position";
public string? Name { get; set; }
public string? Title { get; set; }
}
The following example:
- Calls ConfigurationBinder.Bind to bind the
PositionOptionsclass to thePositionsection. - Displays the
Positionconfiguration data.
BasicOptions.razor:
@page "/basic-options"
@inject IConfiguration Config
Name: @positionOptions?.Name<br>
Title: @positionOptions?.Title
@code {
private PositionOptions? positionOptions;
protected override void OnInitialized()
{
positionOptions = new PositionOptions();
Config.GetSection(PositionOptions.Position).Bind(positionOptions);
}
}
BasicOptions.cshtml:
@page
@model RazorPagesSample.Pages.BasicOptionsModel
@{
}
BasicOptions.cshtml.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorPagesSample.Pages;
public class BasicOptionsModel : PageModel
{
private readonly IConfiguration _config;
public BasicOptionsModel(IConfiguration config)
{
_config = config;
}
public ContentResult OnGet()
{
var positionOptions = new PositionOptions();
_config.GetSection(PositionOptions.Position).Bind(positionOptions);
return Content(
$"Name: {positionOptions.Name}\n" +
$"Title: {positionOptions.Title}");
}
}
Output:
Name: Joe Smith
Title: Editor
After the app has started, changes to the JSON configuration in the app settings file are read. To demonstrate the behavior, change one or both configuration values in the app settings file and reload the page without restarting the app.
Bind allows an abstract class to be instantiated. Consider the following example that uses the abstract class AbstractClassWithName.
NameTitleOptions.cs:
public abstract class AbstractClassWithName
{
public abstract string? Name { get; set; }
}
public class NameTitleOptions(int age) : AbstractClassWithName
{
public const string NameTitle = "NameTitle";
public override string? Name { get; set; }
public string? Title { get; set; }
public int Age { get; set; } = age;
}
JSON configuration:
"NameTitle": {
"Name": "Sally Jones",
"Title": "Writer"
}
The following example displays the NameTitleOptions configuration values.
AbstractClassOptions.razor:
@page "/abstract-class-options"
@inject IConfiguration Config
Name: @nameTitleOptions?.Name<br>
Title: @nameTitleOptions?.Title<br>
Age: @nameTitleOptions?.Age
@code {
private NameTitleOptions? nameTitleOptions;
protected override void OnInitialized()
{
nameTitleOptions = new NameTitleOptions(22);
Config.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);
}
}
AbstractClassOptions.cshtml:
@page
@model RazorPagesSample.Pages.AbstractClassOptionsModel
@{
}
AbstractClassOptions.cshtml.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorPagesSample.Pages;
public class AbstractClassOptionsModel : PageModel
{
private readonly IConfiguration _config;
public AbstractClassOptionsModel(IConfiguration config)
{
_config = config;
}
public ContentResult OnGet()
{
var nameTitleOptions = new NameTitleOptions(22);
_config.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);
return Content(
$"Name: {nameTitleOptions.Name}\n" +
$"Title: {nameTitleOptions.Title}\n" +
$"Age: {nameTitleOptions.Age}");
}
}
Output:
Name: Sally Jones
Title: Writer
Age: 22
ConfigurationBinder.Get binds and returns the specified type. The following example shows how to use Get with the PositionOptions class.
GetOptions.razor:
@page "/get-options"
@inject IConfiguration Config
Name: @positionOptions?.Name<br>
Title: @positionOptions?.Title
@code {
private PositionOptions? positionOptions;
protected override void OnInitialized() =>
positionOptions = Config.GetSection(PositionOptions.Position)
.Get<PositionOptions>();
}
GetOptions.cshtml:
@page
@model RazorPagesSample.Pages.GetOptionsModel
@{
}
GetOptions.cshtml.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorPagesSample.Pages;
public class GetOptionsModel : PageModel
{
private readonly IConfiguration _config;
public GetOptionsModel(IConfiguration config)
{
_config = config;
}
public ContentResult OnGet()
{
var positionOptions = _config.GetSection(PositionOptions.Position)
.Get<PositionOptions>();
return Content(
$"Name: {positionOptions?.Name}\n" +
$"Title: {positionOptions?.Title}");
}
}
Output:
Name: Joe Smith
Title: Editor
After the app has started, changes to the JSON configuration in the app settings file are read. To demonstrate the behavior, change one or both configuration values in the app settings file and reload the page without restarting the app.
Summary of the differences between ConfigurationBinder.Bind and ConfigurationBinder.Get:
- Get is usually more convenient than using Bind because Get creates and returns a new instance of the object, while Bind populates properties of an existing object instance that's typically established by another line of code.
- Bind allows the concretion of an abstract class, while Get is only able to create a non-abstract instance of the options type.
Bind options to the dependency injection service container
In the following example, PositionOptions is added to the service container with Configure and bound to configuration.
JSON configuration:
"Position": {
"Name": "Joe Smith",
"Title": "Editor"
}
PositionOptions.cs:
public class PositionOptions
{
public const string Position = "Position";
public string? Name { get; set; }
public string? Title { get; set; }
}
Where services are registered for dependency injection in the app's Program file:
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
The following example reads the position options.
DIOptions.razor:
@page "/di-options"
@using Microsoft.Extensions.Options
@inject IOptions<PositionOptions> Options
Name: @Options.Value.Name<br>
Title: @Options.Value.Title
DIOptions.cshtml:
@page
@model RazorPagesSample.Pages.DIOptionsModel
@{
}
DIOptions.cshtml.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
namespace RazorPagesSample.Pages;
public class DIOptionsModel : PageModel
{
private readonly IOptions<PositionOptions> _options;
public DIOptionsModel(IOptions<PositionOptions> options)
{
_options = options;
}
public ContentResult OnGet()
{
return Content(
$"Name: {_options.Value.Name}\n" +
$"Title: {_options.Value.Title}");
}
}
Output:
Name: Joe Smith
Title: Editor
For the preceding code, changes to the JSON configuration in the app settings file after the app has started are not read. To read changes after the app has started, use IOptionsSnapshot.
Options interfaces
- Does not support:
- Reading of configuration data after the app has started.
- Named options.
- Is registered as a singleton service and can be injected into any service lifetime.
- Covered later in this article in the Use
IOptionsSnapshotto read updated data section. - Is useful in scenarios where options should be recomputed on every request.
- Is registered as a scoped service, so it can't be injected into a singleton service.
- Supports named options.
- Covered later in this article in the Use
IOptionsMonitorto read updated data section. - Is used to retrieve options and manage options notifications for
TOptionsinstances. - Is registered as a singleton service and can be injected into any service lifetime.
- Supports:
- Change notifications.
- Named options.
- Reloadable configuration.
- Selective options invalidation (IOptionsMonitorCache<TOptions>).
Post-configuration scenarios enable setting or changing options after all IConfigureOptions<TOptions> configuration occurs.
IOptionsFactory<TOptions> is responsible for creating new options instances. It has a single Create method. The default implementation takes all registered IConfigureOptions<TOptions> and IPostConfigureOptions<TOptions> and runs all the configurations first, followed by the post-configuration. It distinguishes between IConfigureNamedOptions<TOptions> and IConfigureOptions<TOptions> and only calls the appropriate interface.
Use IOptionsSnapshot to read updated data
Using IOptionsSnapshot<TOptions>:
- Options are computed once per request when accessed and cached for the lifetime of the request.
- May incur a significant performance penalty because it's a scoped service and is recomputed per request. For more information, see
IOptionsSnapshotis very slow (dotnet/runtime#53793) and Improve the performance of configuration binding (dotnet/runtime#36130). - 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<TOptions> is that:
- IOptionsMonitor<TOptions> is a singleton service that retrieves current option values at any time, which is especially useful in singleton dependencies.
- IOptionsSnapshot<TOptions> 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 ASP.NET Core runtime uses OptionsCache<TOptions> to cache the options instance after it's created.
JSON configuration:
"Position": {
"Name": "Joe Smith",
"Title": "Editor"
}
PositionOptions.cs:
public class PositionOptions
{
public const string Position = "Position";
public string? Name { get; set; }
public string? Title { get; set; }
}
The following example uses IOptionsSnapshot<TOptions>.
SnapshotOptions.razor:
@page "/snapshot-options"
@using Microsoft.Extensions.Options
@inject IOptionsSnapshot<PositionOptions> Options
Name: @Options.Value.Name<br>
Title: @Options.Value.Title
SnapshotOptions.cshtml:
@page
@model RazorPagesSample.Pages.SnapshotOptionsModel
@{
}
SnapshotOptions.cshtml.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
namespace RazorPagesSample.Pages;
public class SnapshotOptionsModel : PageModel
{
private readonly IOptionsSnapshot<PositionOptions> _options;
public SnapshotOptionsModel(IOptionsSnapshot<PositionOptions> options)
{
_options = options;
}
public ContentResult OnGet()
{
return Content(
$"Name: {_options.Value.Name}\n" +
$"Title: {_options.Value.Title}");
}
}
Where services are registered for dependency injection, the following example registers a configuration instance which PositionOptions binds against:
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
Output:
Name: Joe Smith
Title: Editor
After the app has started, changes to the JSON configuration in the app settings file are read. To demonstrate the behavior, change one or both configuration values in the app settings file and reload the page without restarting the app.
Use IOptionsMonitor to read updated data
IOptionsMonitor<TOptions> is used to retrieve options and manage options notifications for TOptions instances.
The difference between IOptionsMonitor<TOptions> and IOptionsSnapshot is that:
- IOptionsMonitor<TOptions> is a singleton service that retrieves current option values at any time, which is especially useful in singleton dependencies.
- IOptionsSnapshot<TOptions> 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.
IOptionsMonitorCache<TOptions> is used by IOptionsMonitor<TOptions> to cache TOptions instances. IOptionsMonitorCache<TOptions>.TryRemove invalidates options instances in the monitor so that the value is recomputed. Values can be manually introduced with IOptionsMonitorCache<TOptions>.TryAdd. The Clear method is used when all named instances should be recreated on demand.
JSON configuration:
"Position": {
"Name": "Joe Smith",
"Title": "Editor"
}
PositionOptions.cs:
public class PositionOptions
{
public const string Position = "Position";
public string? Name { get; set; }
public string? Title { get; set; }
}
Where services are registered for dependency injection, the following example registers a configuration instance which PositionOptions binds against:
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
The following example uses IOptionsMonitor<TOptions>.
MonitorOptions.razor:
@page "/monitor-options"
@using Microsoft.Extensions.Options
@inject IOptionsMonitor<PositionOptions> Options
Name: @Options.CurrentValue.Name<br>
Title: @Options.CurrentValue.Title
MonitorOptions.cshtml:
@page
@model RazorPagesSample.Pages.MonitorOptionsModel
@{
}
MonitorOptions.cshtml.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
namespace RazorPagesSample.Pages;
public class MonitorOptionsModel : PageModel
{
private readonly IOptionsMonitor<PositionOptions> _options;
public MonitorOptionsModel(IOptionsMonitor<PositionOptions> options)
{
_options = options;
}
public ContentResult OnGet()
{
return Content(
$"Name: {_options.CurrentValue.Name}\n" +
$"Title: {_options.CurrentValue.Title}");
}
}
Output:
Name: Joe Smith
Title: Editor
After the app has started, changes to the JSON configuration in the app settings file are read. To demonstrate the behavior, change one or both configuration values in the app settings file and reload the page without restarting the app.
Specify a custom key name for a configuration property using ConfigurationKeyName
By default, the property names of the options class are used as the key name in the configuration source. If the property name is Title, the key name in the configuration is Title as well.
When the names differentiate, you can use the [ConfigurationKeyName] attribute to specify the key name in the configuration source. Using this technique, you can map a property in the configuration to one in your code with a different name. This is useful when the property name in the configuration source isn't a valid C# identifier or when you want to use a different name in your code.
For example, consider the following options class.
PositionKeyName.cs:
public class PositionKeyName
{
public const string Position = "PositionKeyName";
[ConfigurationKeyName("PositionName")]
public string? Name { get; set; }
[ConfigurationKeyName("PositionTitle")]
public string? Title { get; set; }
}
The Name and Title class properties are bound to the PositionName and PositionTitle from the following JSON configuration:
"PositionKeyName": {
"PositionName": "Carlos Diego",
"PositionTitle": "Director"
}
PositionKeyNameOptions.razor:
@page "/position-key-name-options"
@inject IConfiguration Config
Name: @positionOptions?.Name<br>
Title: @positionOptions?.Title
@code {
private PositionKeyName? positionOptions;
protected override void OnInitialized() =>
positionOptions = Config.GetSection(PositionKeyName.Position)
.Get<PositionKeyName>();
}
PositionKeyName.cshtml:
@page
@model RazorPagesSample.Pages.PositionKeyNameModel
@{
}
PositionKeyName.cshtml.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorPagesSample.Pages;
public class PositionKeyNameModel : PageModel
{
private readonly IConfiguration _config;
public PositionKeyNameModel(IConfiguration config)
{
_config = config;
}
public ContentResult OnGet()
{
var positionOptions = _config.GetSection(PositionKeyName.Position)
.Get<PositionKeyName>();
return Content(
$"Name: {positionOptions?.Name}\n" +
$"Title: {positionOptions?.Title}");
}
}
Output:
Name: Carlos Diego
Title: Director
After the app has started, changes to the JSON configuration in the app settings file are read. To demonstrate the behavior, change one or both configuration values in the app settings file and reload the page without restarting the 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 JSON configuration:
"TopItem": {
"Month": {
"Name": "Green Widget",
"Model": "GW46"
},
"Year": {
"Name": "Orange Gadget",
"Model": "OG35"
}
}
Rather than creating two classes to bind TopItem:Month and TopItem:Year, the following class is used for each section.
TopItemSettings.cs:
public class TopItemSettings
{
public const string Month = "Month";
public const string Year = "Year";
public string? Name { get; set; }
public string? Model { get; set; }
}
Where services are registered for dependency injection, the following example configures the named options:
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
The following example displays the named options:
NamedOptions.razor:
@page "/named-options"
@using Microsoft.Extensions.Options
@inject IOptionsSnapshot<TopItemSettings> Options
Month: Name: @monthTopItem?.Name Model: @monthTopItem?.Model<br>
Year: Name: @yearTopItem?.Name Model: @yearTopItem?.Model
@code {
private TopItemSettings? monthTopItem;
private TopItemSettings? yearTopItem;
protected override void OnInitialized()
{
monthTopItem = Options.Get(TopItemSettings.Month);
yearTopItem = Options.Get(TopItemSettings.Year);
}
}
NamedOptions.cshtml:
@page
@model RazorPagesSample.Pages.NamedOptionsModel
@{
}
NamedOptions.cshtml.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
namespace RazorPagesSample.Pages;
public class NamedOptionsModel : PageModel
{
private readonly IOptionsSnapshot<TopItemSettings> _options;
public NamedOptionsModel(IOptionsSnapshot<TopItemSettings> options)
{
_options = options;
}
public ContentResult OnGet()
{
var monthTopItem = _options.Get(TopItemSettings.Month);
var yearTopItem = _options.Get(TopItemSettings.Year);
return Content(
$"Month:Name {monthTopItem.Name}\n" +
$"Month:Model {monthTopItem.Model}\n" +
$"Year:Name {yearTopItem.Name}\n" +
$"Year:Model {yearTopItem.Model}");
}
}
All options are named instances. IConfigureOptions<TOptions> instances are treated as targeting the Options.DefaultName instance, which is string.Empty. IConfigureNamedOptions<TOptions> also implements IConfigureOptions<TOptions>. The default implementation of the IOptionsFactory<TOptions> 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. ConfigureAll and PostConfigureAll use this convention.
Guidance on post-configuring named options is provided in the Options post-configuration section.
OptionsBuilder API
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 IConfigureOptions<TOptions> overloads that accept service dependencies are only available via OptionsBuilder (see Use DI services to configure options).
OptionsBuilder<TOptions> is demonstrated in the Options validation section.
For information adding a custom repository, see Get started with the Data Protection APIs in ASP.NET Core.
Use DI services to configure options
Services can be accessed from dependency injection while configuring options in two ways:
Configuration delegate approach
Where services are registered for dependency injection, 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<PositionOptions>("optionalName")
.Configure<Service1, Service2, Service3, Service4, Service5>(
(o, s, s2, s3, s4, s5) =>
o.Property = DoSomethingWith(s, s2, s3, s4, s5));
services.AddOptions<PositionOptions>("optionalName")
.Configure<Service1, Service2, Service3, Service4, Service5>(
(o, s, s2, s3, s4, s5) =>
o.Property = DoSomethingWith(s, s2, s3, s4, s5));
Configuration options service approach
Create a type that implements IConfigureOptions<TOptions> or IConfigureNamedOptions<TOptions> and register the type as a service.
We recommend passing 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 the validation of option values.
Consider the following JSON configuration:
"KeyOptions": {
"Key1": "Key One",
"Key2": 10,
"Key3": 32
}
The following class is used to bind to the "KeyOptions" configuration section and applies two data annotations rules, which include a regular expression and range requirement.
KeyOptions.cs:
public class KeyOptions
{
public const string Key = "KeyOptions";
[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; }
}
Where services are registered for dependency injection, the following example:
- Calls AddOptions to get an OptionsBuilder<TOptions> that binds to the
KeyOptionsclass. - Calls ValidateDataAnnotations to enable validation.
builder.Services.AddOptions<KeyOptions>()
.Bind(builder.Configuration.GetSection(KeyOptions.Key))
.ValidateDataAnnotations();
services.AddOptions<KeyOptions>()
.Bind(builder.Configuration.GetSection(KeyOptions.Key))
.ValidateDataAnnotations();
The ValidateDataAnnotations extension method is defined in the Microsoft.Extensions.Options.DataAnnotations NuGet package. For web apps that use the Microsoft.NET.Sdk.Web SDK, this package is referenced implicitly from the shared framework.
Where services are registered for dependency injection, the following example applies a more complex validation rule using a delegate:
builder.Services.AddOptions<KeyOptions>()
.Bind(builder.Configuration.GetSection(KeyOptions.Key))
.ValidateDataAnnotations()
.Validate(options =>
{
return options.Key3 > options.Key2;
}, "Key3 must be > than Key2");
services.AddOptions<KeyOptions>()
.Bind(builder.Configuration.GetSection(KeyOptions.Key))
.ValidateDataAnnotations()
.Validate(options =>
{
return options.Key3 > options.Key2;
}, "Key3 must be > than Key2");
The following example demonstrates how to log options validation exceptions and display an OptionsValidationException.Message.
Note
For demonstration purposes, the following example uses a MarkupString to format raw HTML. Rendering raw HTML constructed from any untrusted source is a security risk and should always be avoided. For more information, see ASP.NET Core Razor components.
OptionsValidation1.razor:
@page "/options-validation-1"
@inject IOptionsSnapshot<KeyOptions> Options
@inject ILogger<OptionsValidation1> Logger
@if (message is not null)
{
@((MarkupString)message)
}
@code {
private string? message;
protected override void OnInitialized()
{
try
{
var keyOptions = Options.Value;
}
catch (OptionsValidationException ex)
{
foreach (var failure in ex.Failures)
{
Logger.LogError(failure);
}
}
try
{
message =
$"Key1: {Options.Value.Key1}<br>" +
$"Key2: {Options.Value.Key2}<br>" +
$"Key3: {Options.Value.Key3}";
}
catch (OptionsValidationException optValEx)
{
message = optValEx.Message;
}
}
}
The following example demonstrates how to log options validation exceptions in the page model's constructor and display an OptionsValidationException.Message in the page's OnGet method.
OptionsValidation1.cshtml:
@page
@model RazorPagesSample.Pages.OptionsValidation1Model
@{
}
OptionsValidation1.cshtml.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
namespace RazorPagesSample.Pages;
public class OptionsValidation1Model : PageModel
{
private readonly IOptionsSnapshot<KeyOptions>? _options;
public OptionsValidation1Model(IOptionsSnapshot<KeyOptions> options,
ILogger<OptionsValidation1Model> logger)
{
_options = options;
try
{
var keyOptions = _options?.Value;
}
catch (OptionsValidationException ex)
{
foreach (var failure in ex.Failures)
{
logger?.LogError("Validation: {Failure}", failure);
}
}
}
public ContentResult OnGet()
{
string message;
try
{
message =
$"Key1: {_options?.Value.Key1}\n" +
$"Key2: {_options?.Value.Key2}\n" +
$"Key3: {_options?.Value.Key3}";
}
catch (OptionsValidationException optValEx)
{
return Content(optValEx.Message);
}
return Content(message);
}
}
The preceding code displays the configuration values or validation errors:
- Use the code with the preceding app settings configuration to demonstrate no validation errors.
- Change the configuration in the app settings file in one or more ways that violate the data annotations rules. Reload the options validation page to see the rule violations.
In the following example, the page content indicates that the value of Key2 is out of range if the value of Key2 in the app settings file is changed to a value below zero or over 1,000:
DataAnnotation validation failed for 'KeyOptions' members: 'Key2' with the error: 'Value for Key2 must be between 0 and 1000.'.
Validate options in a dedicated class with IValidateOptions<TOptions>
Implement IValidateOptions<TOptions> to validate options without the need to maintain validation rules with data annotations or in the app's Program file.
In the following example, the data annotations rules and options delegate validation of the preceding examples is moved to a validation class. The Options model class (KeyOptions2) doesn't contain data annotations.
Consider the following JSON configuration:
"KeyOptions": {
"Key1": "Key One",
"Key2": 10,
"Key3": 32
}
KeyOptions2.cs:
public class KeyOptions2
{
public const string Key = "KeyOptions";
public string? Key1 { get; set; }
public int Key2 { get; set; }
public int Key3 { get; set; }
}
public class KeyOptionsValidation : IValidateOptions<KeyOptions2>
{
public ValidateOptionsResult Validate(string? name, KeyOptions2 options)
{
if (options == null)
{
return ValidateOptionsResult.Fail("KeyOptions not found.");
}
StringBuilder? validationResult = new();
var rx = new Regex(@"^[a-zA-Z\s]{1,40}$");
var match = rx.Match(options.Key1!);
if (string.IsNullOrEmpty(match.Value))
{
validationResult.Append($"{options.Key1} doesn't match RegEx<br>");
}
if (options.Key2 < 0 || options.Key2 > 1000)
{
validationResult.Append($"{options.Key2} doesn't match Range 0 - 1000<br>");
}
if (options.Key3 < options.Key2)
{
validationResult.Append("Key3 must be > than Key2<br>");
}
if (validationResult.Length > 0)
{
return ValidateOptionsResult.Fail(validationResult.ToString());
}
return ValidateOptionsResult.Success;
}
}
public class KeyOptionsValidation : IValidateOptions<KeyOptions2>
{
public ValidateOptionsResult Validate(string? name, KeyOptions2 options)
{
if (options == null)
{
return ValidateOptionsResult.Fail("KeyOptions not found");
}
StringBuilder? validationResult = new();
var rx = new Regex(@"^[a-zA-Z\s]{1,40}$");
var match = rx.Match(options.Key1!);
if (string.IsNullOrEmpty(match.Value))
{
validationResult.Append($"{options.Key1} doesn't match RegEx\n");
}
if (options.Key2 < 0 || options.Key2 > 1000)
{
validationResult.Append($"{options.Key2} doesn't match Range 0 - 1000\n");
}
if (options.Key3 < options.Key2)
{
validationResult.Append("Key3 must be > than Key2\n");
}
if (validationResult.Length > 0)
{
return ValidateOptionsResult.Fail(validationResult.ToString());
}
return ValidateOptionsResult.Success;
}
}
Where services are registered for dependency injection and using the preceding code, validation is enabled in Program.cs with the following example:
builder.Services.Configure<KeyOptions2>(
builder.Configuration.GetSection(KeyOptions2.Key));
builder.Services.AddSingleton<IValidateOptions<KeyOptions2>,
KeyOptionsValidation>();
services.Configure<KeyOptions>(
builder.Configuration.GetSection(KeyOptions2.Key));
services.AddSingleton<IValidateOptions<KeyOptions2>, KeyOptionsValidation>();
:::moniker-range=">= aspnetcore-8.0"
The following example demonstrates how to log options validation exceptions and display an OptionsValidationException.Message.
OptionsValidation2.razor:
@page "/options-validation-2"
@inject IOptionsSnapshot<KeyOptions2> Options
@inject ILogger<OptionsValidation2> Logger
@if (message is not null)
{
@((MarkupString)message)
}
@code {
private string? message;
protected override void OnInitialized()
{
try
{
var keyOptions = Options.Value;
}
catch (OptionsValidationException ex)
{
foreach (var failure in ex.Failures)
{
Logger.LogError(failure);
}
}
try
{
message =
$"Key1: {Options.Value.Key1}<br>" +
$"Key2: {Options.Value.Key2}<br>" +
$"Key3: {Options.Value.Key3}";
}
catch (OptionsValidationException optValEx)
{
message = optValEx.Message;
}
}
}
:::moniker-end
The following example demonstrates how to log options validation exceptions in the page model's constructor and display an OptionsValidationException.Message in the page's OnGet method.
OptionsValidation2.cshtml:
@page
@model RazorPagesSample.Pages.OptionsValidation2Model
@{
}
OptionsValidation2.cshtml.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Options;
namespace RazorPagesSample.Pages;
public class OptionsValidation2Model : PageModel
{
private readonly IOptionsSnapshot<KeyOptions2>? _options;
public OptionsValidation2Model(IOptionsSnapshot<KeyOptions2> options,
ILogger<OptionsValidation2Model> logger)
{
_options = options;
try
{
var keyOptions = _options?.Value;
}
catch (OptionsValidationException ex)
{
foreach (var failure in ex.Failures)
{
logger?.LogError("Validation: {Failure}", failure);
}
}
}
public ContentResult OnGet()
{
string message;
try
{
message =
$"Key1: {_options?.Value.Key1}\n" +
$"Key2: {_options?.Value.Key2}\n" +
$"Key3: {_options?.Value.Key3}";
}
catch (OptionsValidationException optValEx)
{
return Content(optValEx.Message);
}
return Content(message);
}
}
Class-level validation with IValidatableObject
Options validation supports IValidatableObject to perform class-level validation of a class within a class:
- Implement the IValidatableObject interface and its Validate method within the class.
- Call ValidateDataAnnotations in the
Programfile.
Run options validation when the app starts with ValidateOnStart
Options validation runs the first time a TOption instance is created, which is when the first
access to IOptionsSnapshot<TOptions>.Value occurs in a request pipeline or when
IOptionsMonitor<TOptions>.Get(string) is called. Each time options are reloaded, validation runs again.
To run options validation when the app starts, call ValidateOnStart in the Program file where services are registered for dependency injection:
builder.Services.AddOptions<KeyOptions>()
.Bind(builder.Configuration.GetSection(KeyOptions.Key))
.ValidateDataAnnotations()
.ValidateOnStart();
Options post-configuration
Where services are registered for dependency injection, PostConfigure is available to initialize a particular named option. In the following example, only TopItem:Month:Name is post-configured:
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
builder.Services.PostConfigure<TopItemSettings>(TopItemSettings.Month, options =>
{
options.Name = "Blue Gizmo";
});
services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
services.PostConfigure<TopItemSettings>(TopItemSettings.Month, options =>
{
options.Name = "Blue Gizmo";
});
Rendered output from the named options example, where only the TopItem:Month:Name is post-configured:
Month: Name: Blue Gizmo Model: GW46
Year: Name: Orange Gadget Model: OG35
Where services are registered for dependency injection, use PostConfigureAll to initialize all named instances of the specified options type. In the following example, all instances of TopItem.Name are set to Blue Gizmo:
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
builder.Services.PostConfigureAll<TopItemSettings>(options =>
{
options.Name = "Blue Gizmo";
});
services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
services.PostConfigureAll<TopItemSettings>(options =>
{
options.Name = "Blue Gizmo";
});
Rendered output from the named options example, where all TopItem:Name options are post-configured:
Month: Name: Blue Gizmo Model: GW46
Year: Name: Blue Gizmo Model: OG35
Access options in the request processing pipeline
To access IOptions<TOptions> or IOptionsMonitor<TOptions> in the request processing pipeline, call GetRequiredService on WebApplication.Services:
var name = app.Services.GetRequiredService<IOptionsMonitor<PositionOptions>>()
.CurrentValue.Name;
IOptions<TOptions> and IOptionsMonitor<TOptions> can be used in Startup.Configure, since services are built before the Configure method executes.
In the following example, IOptionsMonitor<TOptions> is injected into the Startup.Configure method to obtain PositionOptions values:
public void Configure(IApplicationBuilder app,
IOptionsMonitor<PositionOptions> options)
Access the options in the request processing pipeline of Startup.Configure:
var name = options.CurrentValue.Name;
Don't use IOptions<TOptions> or IOptionsMonitor<TOptions> in Startup.ConfigureServices. An inconsistent options state may exist due to the ordering of service registrations.
Additional resources
ASP.NET Core