Options pattern in ASP.NET Core
Note
This isn't the latest version of this article. For the current release, see the .NET 9 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 9 version of this article.
Important
This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.
For the current release, see the .NET 9 version of this article.
By Rick Anderson.
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. For more information, see 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.
Bind hierarchical configuration
The preferred way to read related configuration values is using the options pattern. For example, to read the following configuration values:
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
}
Create the following PositionOptions
class:
public class PositionOptions
{
public const string Position = "Position";
public string Title { get; set; } = String.Empty;
public string Name { get; set; } = String.Empty;
}
An options class:
- Must be non-abstract.
- Has public read-write properties of the type that have corresponding items in config are bound.
- Has its read-write properties bound to matching entries in configuration.
- Does not have its fields bound. In the preceding code,
Position
is not bound. ThePosition
field is used so the string"Position"
doesn't need to be hard coded in the app when binding the class to a configuration provider.
The following code:
- Calls ConfigurationBinder.Bind to bind the
PositionOptions
class to thePosition
section. - Displays the
Position
configuration data.
public class Test22Model : PageModel
{
private readonly IConfiguration Configuration;
public Test22Model(IConfiguration configuration)
{
Configuration = configuration;
}
public ContentResult OnGet()
{
var positionOptions = new PositionOptions();
Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);
return Content($"Title: {positionOptions.Title} \n" +
$"Name: {positionOptions.Name}");
}
}
In the preceding code, by default, changes to the JSON configuration file after the app has started are read.
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 PositionOptions
class:
public class Test21Model : PageModel
{
private readonly IConfiguration Configuration;
public PositionOptions? positionOptions { get; private set; }
public Test21Model(IConfiguration configuration)
{
Configuration = configuration;
}
public ContentResult OnGet()
{
positionOptions = Configuration.GetSection(PositionOptions.Position)
.Get<PositionOptions>();
return Content($"Title: {positionOptions.Title} \n" +
$"Name: {positionOptions.Name}");
}
}
In the preceding code, by default, changes to the JSON configuration file after the app has started are read.
Bind also allows the concretion of an abstract class. Consider the following code which uses the abstract class SomethingWithAName
:
namespace ConfigSample.Options;
public abstract class SomethingWithAName
{
public abstract string? Name { get; set; }
}
public class NameTitleOptions(int age) : SomethingWithAName
{
public const string NameTitle = "NameTitle";
public override string? Name { get; set; }
public string Title { get; set; } = string.Empty;
public int Age { get; set; } = age;
}
The following code displays the NameTitleOptions
configuration values:
public class Test33Model : PageModel
{
private readonly IConfiguration Configuration;
public Test33Model(IConfiguration configuration)
{
Configuration = configuration;
}
public ContentResult OnGet()
{
var nameTitleOptions = new NameTitleOptions(22);
Configuration.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);
return Content($"Title: {nameTitleOptions.Title} \n" +
$"Name: {nameTitleOptions.Name} \n" +
$"Age: {nameTitleOptions.Age}"
);
}
}
Calls to Bind
are less strict than calls to Get<>
:
Bind
allows the concretion of an abstract.Get<>
has to create an instance itself.
The Options Pattern
An alternative approach when using the options pattern is to bind the Position
section and add it to the dependency injection service container. In the following code, PositionOptions
is added to the service container with Configure and bound to configuration:
using ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
var app = builder.Build();
Using the preceding code, the following code reads the position options:
public class Test2Model : PageModel
{
private readonly PositionOptions _options;
public Test2Model(IOptions<PositionOptions> options)
{
_options = options.Value;
}
public ContentResult OnGet()
{
return Content($"Title: {_options.Title} \n" +
$"Name: {_options.Name}");
}
}
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.
Options interfaces
- Does not support:
- Reading of configuration data after the app has started.
- Named options
- Is registered as a Singleton and can be injected into any service lifetime.
- Is useful in scenarios where options should be recomputed on every request. For more information, see Use IOptionsSnapshot to read updated data.
- Is registered as Scoped and therefore can't be injected into a Singleton service.
- Supports named options
- 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:
- 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.
IOptionsMonitorCache<TOptions> is used by IOptionsMonitor<TOptions> to cache TOptions
instances. The IOptionsMonitorCache<TOptions> invalidates options instances in the monitor so that the value is recomputed (TryRemove). Values can be manually introduced with TryAdd. The Clear method is used when all named instances should be recreated on demand.
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 this GitHub issue and Improve the performance of configuration binding.
- 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 theIOptionsSnapshot<T>
object is constructed. Options snapshots are designed for use with transient and scoped dependencies.
The following code uses IOptionsSnapshot<TOptions>.
public class TestSnapModel : PageModel
{
private readonly MyOptions _snapshotOptions;
public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
{
_snapshotOptions = snapshotOptionsAccessor.Value;
}
public ContentResult OnGet()
{
return Content($"Option1: {_snapshotOptions.Option1} \n" +
$"Option2: {_snapshotOptions.Option2}");
}
}
The following code registers a configuration instance which MyOptions
binds against:
using SampleApp.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));
var app = builder.Build();
In the preceding code, changes to the JSON configuration file after the app has started are read.
IOptionsMonitor
The following code registers a configuration instance which MyOptions
binds against.
using SampleApp.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));
var app = builder.Build();
The following example uses IOptionsMonitor<TOptions>:
public class TestMonitorModel : PageModel
{
private readonly IOptionsMonitor<MyOptions> _optionsDelegate;
public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
{
_optionsDelegate = optionsDelegate;
}
public ContentResult OnGet()
{
return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
$"Option2: {_optionsDelegate.CurrentValue.Option2}");
}
}
In the preceding code, by default, changes to the JSON configuration file after the app has started are read.
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:
{
"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:
public class TopItemSettings
{
public const string Month = "Month";
public const string Year = "Year";
public string Name { get; set; } = string.Empty;
public string Model { get; set; } = string.Empty;
}
The following code configures the named options:
using SampleApp.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
var app = builder.Build();
The following code displays the named options:
public class TestNOModel : PageModel
{
private readonly TopItemSettings _monthTopItem;
private readonly TopItemSettings _yearTopItem;
public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
{
_monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
_yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
}
public ContentResult OnGet()
{
return Content($"Month:Name {_monthTopItem.Name} \n" +
$"Month:Model {_monthTopItem.Model} \n\n" +
$"Year:Name {_yearTopItem.Name} \n" +
$"Year:Model {_yearTopItem.Model} \n" );
}
}
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.
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 ConfigureOptions
overloads that accept service dependencies are only available via OptionsBuilder
.
OptionsBuilder
is used in the Options validation section.
See Use AddOptions to configure custom repository for information adding a custom repository.
Use DI services to configure options
Services can be accessed from dependency injection 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<Service1, Service2, Service3, Service4, Service5>( (o, s, s2, s3, s4, s5) => o.Property = DoSomethingWith(s, s2, s3, s4, s5));
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 option values to be validated.
Consider the following appsettings.json
file:
{
"MyConfig": {
"Key1": "My Key One",
"Key2": 10,
"Key3": 32
}
}
The following class is used to bind to the "MyConfig"
configuration section and applies a couple of DataAnnotations
rules:
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; }
}
The following code:
- Calls AddOptions to get an OptionsBuilder<TOptions> that binds to the
MyConfigOptions
class. - Calls ValidateDataAnnotations to enable validation using
DataAnnotations
.
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations();
var app = builder.Build();
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.
The following code displays the configuration values or the validation errors:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IOptions<MyConfigOptions> _config;
public HomeController(IOptions<MyConfigOptions> config,
ILogger<HomeController> logger)
{
_config = config;
_logger = logger;
try
{
var configValue = _config.Value;
}
catch (OptionsValidationException ex)
{
foreach (var failure in ex.Failures)
{
_logger.LogError(failure);
}
}
}
public ContentResult Index()
{
string msg;
try
{
msg = $"Key1: {_config.Value.Key1} \n" +
$"Key2: {_config.Value.Key2} \n" +
$"Key3: {_config.Value.Key3}";
}
catch (OptionsValidationException optValEx)
{
return Content(optValEx.Message);
}
return Content(msg);
}
The following code applies a more complex validation rule using a delegate:
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Key2 != 0)
{
return config.Key3 > config.Key2;
}
return true;
}, "Key3 must be > than Key2."); // Failure message.
var app = builder.Build();
IValidateOptions<TOptions>
and IValidatableObject
The following class implements IValidateOptions<TOptions>:
public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
public MyConfigOptions _config { get; private set; }
public MyConfigValidation(IConfiguration config)
{
_config = config.GetSection(MyConfigOptions.MyConfig)
.Get<MyConfigOptions>();
}
public ValidateOptionsResult Validate(string name, MyConfigOptions options)
{
string? vor = null;
var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
var match = rx.Match(options.Key1!);
if (string.IsNullOrEmpty(match.Value))
{
vor = $"{options.Key1} doesn't match RegEx \n";
}
if ( options.Key2 < 0 || options.Key2 > 1000)
{
vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
}
if (_config.Key2 != default)
{
if(_config.Key3 <= _config.Key2)
{
vor += "Key3 must be > than Key2.";
}
}
if (vor != null)
{
return ValidateOptionsResult.Fail(vor);
}
return ValidateOptionsResult.Success;
}
}
IValidateOptions
enables moving the validation code out of Program.cs
and into a class.
Using the preceding code, validation is enabled in Program.cs
with the following code:
using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
MyConfigOptions.MyConfig));
builder.Services.AddSingleton<IValidateOptions
<MyConfigOptions>, MyConfigValidation>();
var app = builder.Build();
Options validation also supports IValidatableObject. To perform class-level validation of a class within the class itself:
- Implement the
IValidatableObject
interface and its Validate method within the class. - Call ValidateDataAnnotations in
Program.cs
.
ValidateOnStart
Options validation runs the first time a TOption
instance is created. That means, for instance, when first
access to IOptionsSnapshot<TOptions>.Value
occurs in a request pipeline or when
IOptionsMonitor<TOptions>.Get(string)
is called on settings present. After settings are reloaded, validation runs again. The ASP.NET Core runtime uses OptionsCache<TOptions> to cache the options instance once it is created.
To run options validation eagerly, when the app starts, call ValidateOnStart<TOptions>(OptionsBuilder<TOptions>)in Program.cs
:
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.ValidateOnStart();
Options post-configuration
Set post-configuration with IPostConfigureOptions<TOptions>. Post-configuration runs after all IConfigureOptions<TOptions> configuration occurs:
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));
builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});
PostConfigure is available to post-configure named options:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
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>("Month", myOptions =>
{
myOptions.Name = "post_configured_name_value";
myOptions.Model = "post_configured_model_value";
});
var app = builder.Build();
Use PostConfigureAll to post-configure all configuration instances:
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));
builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});
Access options in Program.cs
To access IOptions<TOptions> or IOptionsMonitor<TOptions> in Program.cs
, call GetRequiredService on WebApplication.Services:
var app = builder.Build();
var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
.CurrentValue.Option1;
Additional resources
By Kirk Larkin and Rick Anderson.
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. For more information, see 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.
Bind hierarchical configuration
The preferred way to read related configuration values is using the options pattern. For example, to read the following configuration values:
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
}
Create the following PositionOptions
class:
public class PositionOptions
{
public const string Position = "Position";
public string Title { get; set; } = String.Empty;
public string Name { get; set; } = String.Empty;
}
An options class:
- Must be non-abstract with a public parameterless constructor.
- All public read-write properties of the type are bound.
- Fields are not bound. In the preceding code,
Position
is not bound. ThePosition
field is used so the string"Position"
doesn't need to be hard coded in the app when binding the class to a configuration provider.
The following code:
- Calls ConfigurationBinder.Bind to bind the
PositionOptions
class to thePosition
section. - Displays the
Position
configuration data.
public class Test22Model : PageModel
{
private readonly IConfiguration Configuration;
public Test22Model(IConfiguration configuration)
{
Configuration = configuration;
}
public ContentResult OnGet()
{
var positionOptions = new PositionOptions();
Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);
return Content($"Title: {positionOptions.Title} \n" +
$"Name: {positionOptions.Name}");
}
}
In the preceding code, by default, changes to the JSON configuration file after the app has started are read.
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 PositionOptions
class:
public class Test21Model : PageModel
{
private readonly IConfiguration Configuration;
public PositionOptions? positionOptions { get; private set; }
public Test21Model(IConfiguration configuration)
{
Configuration = configuration;
}
public ContentResult OnGet()
{
positionOptions = Configuration.GetSection(PositionOptions.Position)
.Get<PositionOptions>();
return Content($"Title: {positionOptions.Title} \n" +
$"Name: {positionOptions.Name}");
}
}
In the preceding code, by default, changes to the JSON configuration file after the app has started are read.
An alternative approach when using the options pattern is to bind the Position
section and add it to the dependency injection service container. In the following code, PositionOptions
is added to the service container with Configure and bound to configuration:
using ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
var app = builder.Build();
Using the preceding code, the following code reads the position options:
public class Test2Model : PageModel
{
private readonly PositionOptions _options;
public Test2Model(IOptions<PositionOptions> options)
{
_options = options.Value;
}
public ContentResult OnGet()
{
return Content($"Title: {_options.Title} \n" +
$"Name: {_options.Name}");
}
}
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.
Options interfaces
- Does not support:
- Reading of configuration data after the app has started.
- Named options
- Is registered as a Singleton and can be injected into any service lifetime.
- Is useful in scenarios where options should be recomputed on every request. For more information, see Use IOptionsSnapshot to read updated data.
- Is registered as Scoped and therefore can't be injected into a Singleton service.
- Supports named options
- 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:
- 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.
IOptionsMonitorCache<TOptions> is used by IOptionsMonitor<TOptions> to cache TOptions
instances. The IOptionsMonitorCache<TOptions> invalidates options instances in the monitor so that the value is recomputed (TryRemove). Values can be manually introduced with TryAdd. The Clear method is used when all named instances should be recreated on demand.
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 this GitHub issue and Improve the performance of configuration binding.
- 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 theIOptionsSnapshot<T>
object is constructed. Options snapshots are designed for use with transient and scoped dependencies.
The following code uses IOptionsSnapshot<TOptions>.
public class TestSnapModel : PageModel
{
private readonly MyOptions _snapshotOptions;
public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
{
_snapshotOptions = snapshotOptionsAccessor.Value;
}
public ContentResult OnGet()
{
return Content($"Option1: {_snapshotOptions.Option1} \n" +
$"Option2: {_snapshotOptions.Option2}");
}
}
The following code registers a configuration instance which MyOptions
binds against:
using SampleApp.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));
var app = builder.Build();
In the preceding code, changes to the JSON configuration file after the app has started are read.
IOptionsMonitor
The following code registers a configuration instance which MyOptions
binds against.
using SampleApp.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));
var app = builder.Build();
The following example uses IOptionsMonitor<TOptions>:
public class TestMonitorModel : PageModel
{
private readonly IOptionsMonitor<MyOptions> _optionsDelegate;
public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
{
_optionsDelegate = optionsDelegate;
}
public ContentResult OnGet()
{
return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
$"Option2: {_optionsDelegate.CurrentValue.Option2}");
}
}
In the preceding code, by default, changes to the JSON configuration file after the app has started are read.
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:
{
"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:
public class TopItemSettings
{
public const string Month = "Month";
public const string Year = "Year";
public string Name { get; set; } = string.Empty;
public string Model { get; set; } = string.Empty;
}
The following code configures the named options:
using SampleApp.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
builder.Configuration.GetSection("TopItem:Year"));
var app = builder.Build();
The following code displays the named options:
public class TestNOModel : PageModel
{
private readonly TopItemSettings _monthTopItem;
private readonly TopItemSettings _yearTopItem;
public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
{
_monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
_yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
}
public ContentResult OnGet()
{
return Content($"Month:Name {_monthTopItem.Name} \n" +
$"Month:Model {_monthTopItem.Model} \n\n" +
$"Year:Name {_yearTopItem.Name} \n" +
$"Year:Model {_yearTopItem.Model} \n" );
}
}
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.
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 ConfigureOptions
overloads that accept service dependencies are only available via OptionsBuilder
.
OptionsBuilder
is used in the Options validation section.
See Use AddOptions to configure custom repository for information adding a custom repository.
Use DI services to configure options
Services can be accessed from dependency injection 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<Service1, Service2, Service3, Service4, Service5>( (o, s, s2, s3, s4, s5) => o.Property = DoSomethingWith(s, s2, s3, s4, s5));
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 option values to be validated.
Consider the following appsettings.json
file:
{
"MyConfig": {
"Key1": "My Key One",
"Key2": 10,
"Key3": 32
}
}
The following class is used to bind to the "MyConfig"
configuration section and applies a couple of DataAnnotations
rules:
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; }
}
The following code:
- Calls AddOptions to get an OptionsBuilder<TOptions> that binds to the
MyConfigOptions
class. - Calls ValidateDataAnnotations to enable validation using
DataAnnotations
.
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations();
var app = builder.Build();
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.
The following code displays the configuration values or the validation errors:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IOptions<MyConfigOptions> _config;
public HomeController(IOptions<MyConfigOptions> config,
ILogger<HomeController> logger)
{
_config = config;
_logger = logger;
try
{
var configValue = _config.Value;
}
catch (OptionsValidationException ex)
{
foreach (var failure in ex.Failures)
{
_logger.LogError(failure);
}
}
}
public ContentResult Index()
{
string msg;
try
{
msg = $"Key1: {_config.Value.Key1} \n" +
$"Key2: {_config.Value.Key2} \n" +
$"Key3: {_config.Value.Key3}";
}
catch (OptionsValidationException optValEx)
{
return Content(optValEx.Message);
}
return Content(msg);
}
The following code applies a more complex validation rule using a delegate:
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Key2 != 0)
{
return config.Key3 > config.Key2;
}
return true;
}, "Key3 must be > than Key2."); // Failure message.
var app = builder.Build();
IValidateOptions<TOptions>
and IValidatableObject
The following class implements IValidateOptions<TOptions>:
public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
public MyConfigOptions _config { get; private set; }
public MyConfigValidation(IConfiguration config)
{
_config = config.GetSection(MyConfigOptions.MyConfig)
.Get<MyConfigOptions>();
}
public ValidateOptionsResult Validate(string name, MyConfigOptions options)
{
string? vor = null;
var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
var match = rx.Match(options.Key1!);
if (string.IsNullOrEmpty(match.Value))
{
vor = $"{options.Key1} doesn't match RegEx \n";
}
if ( options.Key2 < 0 || options.Key2 > 1000)
{
vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
}
if (_config.Key2 != default)
{
if(_config.Key3 <= _config.Key2)
{
vor += "Key3 must be > than Key2.";
}
}
if (vor != null)
{
return ValidateOptionsResult.Fail(vor);
}
return ValidateOptionsResult.Success;
}
}
IValidateOptions
enables moving the validation code out of Program.cs
and into a class.
Using the preceding code, validation is enabled in Program.cs
with the following code:
using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
MyConfigOptions.MyConfig));
builder.Services.AddSingleton<IValidateOptions
<MyConfigOptions>, MyConfigValidation>();
var app = builder.Build();
Options validation also supports IValidatableObject. To perform class-level validation of a class within the class itself:
- Implement the
IValidatableObject
interface and its Validate method within the class. - Call ValidateDataAnnotations in
Program.cs
.
ValidateOnStart
Options validation runs the first time an IOptions<TOptions>, IOptionsSnapshot<TOptions>, or IOptionsMonitor<TOptions> implementation is created. To run options validation eagerly, when the app starts, call ValidateOnStart in Program.cs
:
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.ValidateOnStart();
Options post-configuration
Set post-configuration with IPostConfigureOptions<TOptions>. Post-configuration runs after all IConfigureOptions<TOptions> configuration occurs:
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));
builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});
PostConfigure is available to post-configure named options:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
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>("Month", myOptions =>
{
myOptions.Name = "post_configured_name_value";
myOptions.Model = "post_configured_model_value";
});
var app = builder.Build();
Use PostConfigureAll to post-configure all configuration instances:
using OptionsValidationSample.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddOptions<MyConfigOptions>()
.Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));
builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
myOptions.Key1 = "post_configured_key1_value";
});
Access options in Program.cs
To access IOptions<TOptions> or IOptionsMonitor<TOptions> in Program.cs
, call GetRequiredService on WebApplication.Services:
var app = builder.Build();
var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
.CurrentValue.Option1;
Additional resources
By Kirk Larkin and Rick Anderson.
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. For more information, see the Options validation section.
This topic 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.
View or download sample code (how to download)
Bind hierarchical configuration
The preferred way to read related configuration values is using the options pattern. For example, to read the following configuration values:
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
}
Create the following PositionOptions
class:
public class PositionOptions
{
public const string Position = "Position";
public string Title { get; set; }
public string Name { get; set; }
}
An options class:
- Must be non-abstract with a public parameterless constructor.
- All public read-write properties of the type are bound.
- Fields are not bound. In the preceding code,
Position
is not bound. ThePosition
property is used so the string"Position"
doesn't need to be hard coded in the app when binding the class to a configuration provider.
The following code:
- Calls ConfigurationBinder.Bind to bind the
PositionOptions
class to thePosition
section. - Displays the
Position
configuration data.
public class Test22Model : PageModel
{
private readonly IConfiguration Configuration;
public Test22Model(IConfiguration configuration)
{
Configuration = configuration;
}
public ContentResult OnGet()
{
var positionOptions = new PositionOptions();
Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);
return Content($"Title: {positionOptions.Title} \n" +
$"Name: {positionOptions.Name}");
}
}
In the preceding code, by default, changes to the JSON configuration file after the app has started are read.
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 PositionOptions
class:
public class Test21Model : PageModel
{
private readonly IConfiguration Configuration;
public PositionOptions positionOptions { get; private set; }
public Test21Model(IConfiguration configuration)
{
Configuration = configuration;
}
public ContentResult OnGet()
{
positionOptions = Configuration.GetSection(PositionOptions.Position)
.Get<PositionOptions>();
return Content($"Title: {positionOptions.Title} \n" +
$"Name: {positionOptions.Name}");
}
}
In the preceding code, by default, changes to the JSON configuration file after the app has started are read.
An alternative approach when using the options pattern is to bind the Position
section and add it to the dependency injection service container. In the following code, PositionOptions
is added to the service container with Configure and bound to configuration:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<PositionOptions>(Configuration.GetSection(
PositionOptions.Position));
services.AddRazorPages();
}
Using the preceding code, the following code reads the position options:
public class Test2Model : PageModel
{
private readonly PositionOptions _options;
public Test2Model(IOptions<PositionOptions> options)
{
_options = options.Value;
}
public ContentResult OnGet()
{
return Content($"Title: {_options.Title} \n" +
$"Name: {_options.Name}");
}
}
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.
Options interfaces
- Does not support:
- Reading of configuration data after the app has started.
- Named options
- Is registered as a Singleton and can be injected into any service lifetime.
- Is useful in scenarios where options should be recomputed on every request. For more information, see Use IOptionsSnapshot to read updated data.
- Is registered as Scoped and therefore cannot be injected into a Singleton service.
- Supports named options
- 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:
- 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.
IOptionsMonitorCache<TOptions> is used by IOptionsMonitor<TOptions> to cache TOptions
instances. The IOptionsMonitorCache<TOptions> invalidates options instances in the monitor so that the value is recomputed (TryRemove). Values can be manually introduced with TryAdd. The Clear method is used when all named instances should be recreated on demand.
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. 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 theIOptionsSnapshot<T>
object is constructed. Options snapshots are designed for use with transient and scoped dependencies.
The following code uses IOptionsSnapshot<TOptions>.
public class TestSnapModel : PageModel
{
private readonly MyOptions _snapshotOptions;
public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
{
_snapshotOptions = snapshotOptionsAccessor.Value;
}
public ContentResult OnGet()
{
return Content($"Option1: {_snapshotOptions.Option1} \n" +
$"Option2: {_snapshotOptions.Option2}");
}
}
The following code registers a configuration instance which MyOptions
binds against:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));
services.AddRazorPages();
}
In the preceding code, changes to the JSON configuration file after the app has started are read.
IOptionsMonitor
The following code registers a configuration instance which MyOptions
binds against.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));
services.AddRazorPages();
}
The following example uses IOptionsMonitor<TOptions>:
public class TestMonitorModel : PageModel
{
private readonly IOptionsMonitor<MyOptions> _optionsDelegate;
public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
{
_optionsDelegate = optionsDelegate;
}
public ContentResult OnGet()
{
return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
$"Option2: {_optionsDelegate.CurrentValue.Option2}");
}
}
In the preceding code, by default, changes to the JSON configuration file after the app has started are read.
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:
{
"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:
public class TopItemSettings
{
public const string Month = "Month";
public const string Year = "Year";
public string Name { get; set; }
public string Model { get; set; }
}
The following code configures the named options:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<TopItemSettings>(TopItemSettings.Month,
Configuration.GetSection("TopItem:Month"));
services.Configure<TopItemSettings>(TopItemSettings.Year,
Configuration.GetSection("TopItem:Year"));
services.AddRazorPages();
}
The following code displays the named options:
public class TestNOModel : PageModel
{
private readonly TopItemSettings _monthTopItem;
private readonly TopItemSettings _yearTopItem;
public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
{
_monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
_yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
}
public ContentResult OnGet()
{
return Content($"Month:Name {_monthTopItem.Name} \n" +
$"Month:Model {_monthTopItem.Model} \n\n" +
$"Year:Name {_yearTopItem.Name} \n" +
$"Year:Model {_yearTopItem.Model} \n" );
}
}
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.
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 ConfigureOptions
overloads that accept service dependencies are only available via OptionsBuilder
.
OptionsBuilder
is used in the Options validation section.
See Use AddOptions to configure custom repository for information adding a custom repository.
Use DI services to configure options
Services can be accessed from dependency injection 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:services.AddOptions<MyOptions>("optionalName") .Configure<Service1, Service2, Service3, Service4, Service5>( (o, s, s2, s3, s4, s5) => o.Property = DoSomethingWith(s, s2, s3, s4, s5));
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 option values to be validated.
Consider the following appsettings.json
file:
{
"MyConfig": {
"Key1": "My Key One",
"Key2": 10,
"Key3": 32
}
}
The following class binds to the "MyConfig"
configuration section and applies a couple of DataAnnotations
rules:
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; }
}
The following code:
- Calls AddOptions to get an OptionsBuilder<TOptions> that binds to the
MyConfigOptions
class. - Calls ValidateDataAnnotations to enable validation using
DataAnnotations
.
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions<MyConfigOptions>()
.Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations();
services.AddControllersWithViews();
}
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.
The following code displays the configuration values or the validation errors:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly IOptions<MyConfigOptions> _config;
public HomeController(IOptions<MyConfigOptions> config,
ILogger<HomeController> logger)
{
_config = config;
_logger = logger;
try
{
var configValue = _config.Value;
}
catch (OptionsValidationException ex)
{
foreach (var failure in ex.Failures)
{
_logger.LogError(failure);
}
}
}
public ContentResult Index()
{
string msg;
try
{
msg = $"Key1: {_config.Value.Key1} \n" +
$"Key2: {_config.Value.Key2} \n" +
$"Key3: {_config.Value.Key3}";
}
catch (OptionsValidationException optValEx)
{
return Content(optValEx.Message);
}
return Content(msg);
}
The following code applies a more complex validation rule using a delegate:
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions<MyConfigOptions>()
.Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Key2 != 0)
{
return config.Key3 > config.Key2;
}
return true;
}, "Key3 must be > than Key2."); // Failure message.
services.AddControllersWithViews();
}
IValidateOptions for complex validation
The following class implements IValidateOptions<TOptions>:
public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
public MyConfigOptions _config { get; private set; }
public MyConfigValidation(IConfiguration config)
{
_config = config.GetSection(MyConfigOptions.MyConfig)
.Get<MyConfigOptions>();
}
public ValidateOptionsResult Validate(string name, MyConfigOptions options)
{
string vor=null;
var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
var match = rx.Match(options.Key1);
if (string.IsNullOrEmpty(match.Value))
{
vor = $"{options.Key1} doesn't match RegEx \n";
}
if ( options.Key2 < 0 || options.Key2 > 1000)
{
vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
}
if (_config.Key2 != default)
{
if(_config.Key3 <= _config.Key2)
{
vor += "Key3 must be > than Key2.";
}
}
if (vor != null)
{
return ValidateOptionsResult.Fail(vor);
}
return ValidateOptionsResult.Success;
}
}
IValidateOptions
enables moving the validation code out of StartUp
and into a class.
Using the preceding code, validation is enabled in Startup.ConfigureServices
with the following code:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MyConfigOptions>(Configuration.GetSection(
MyConfigOptions.MyConfig));
services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions
<MyConfigOptions>, MyConfigValidation>());
services.AddControllersWithViews();
}
Options post-configuration
Set post-configuration with IPostConfigureOptions<TOptions>. Post-configuration runs after all IConfigureOptions<TOptions> configuration occurs:
services.PostConfigure<MyOptions>(myOptions =>
{
myOptions.Option1 = "post_configured_option1_value";
});
PostConfigure is available to post-configure named options:
services.PostConfigure<MyOptions>("named_options_1", myOptions =>
{
myOptions.Option1 = "post_configured_option1_value";
});
Use PostConfigureAll to post-configure all configuration instances:
services.PostConfigureAll<MyOptions>(myOptions =>
{
myOptions.Option1 = "post_configured_option1_value";
});
Accessing options during startup
IOptions<TOptions> and IOptionsMonitor<TOptions> can be used in Startup.Configure
, since services are built before the Configure
method executes.
public void Configure(IApplicationBuilder app,
IOptionsMonitor<MyOptions> optionsAccessor)
{
var option1 = optionsAccessor.CurrentValue.Option1;
}
Don't use IOptions<TOptions> or IOptionsMonitor<TOptions> in Startup.ConfigureServices
. An inconsistent options state may exist due to the ordering of service registrations.
Options.ConfigurationExtensions NuGet package
The Microsoft.Extensions.Options.ConfigurationExtensions package is implicitly referenced in ASP.NET Core apps.