What is Options Pattern
The Options pattern is a design pattern used in ASP.NET Core that enables developers to configure and manage application settings in a centralized and type-safe manner. It involves creating strongly-typed configuration classes and using them to inject configuration settings into controllers and services, making it easier to manage and update application settings as needed.
All these leads to cleaner code and more robust configuration management in ASP.NET Core apps. Defining Options classes encapsulates configuration and makes it easy to manage.
Working with Options Pattern
Define the Configuration source
Configurations can come from multiple sources such as environment variables, Azure Key Vault, command-line arguments, etc. For simplicity, we will use appSettings.json
file as the Configuration source.
{
"MySettings": {
"Setting1": "Value1",
"Setting2": 100
}
}
Define the Configuration Class
Create a Class
to represent the configurations defined in the appSettings.json
file. The Options
pattern allows you to bind the configuration from appsettings.json
, and various other sources, to this strongly-typed classes.
public class MySettings
{
public string Setting1 { get; set; }
public int Setting2 { get; set; }
}
Bind the Configuration Class and Source in Startup.cs
In the ConfigureServices
method, register the settings class with the Dependency Injection container and bind it to the configuration section. The Options will now be available in the app’s dependency injection container.
public void ConfigureServices(IServiceCollection services)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) // configure appSettings.json as one of the source
.AddEnvironmentVariables(); // configure environment variable as one of the source
services.Configure<MySettings>(Configuration.GetSection("MySettings"));
}
Access Options via Dependency Injection
To access configured options values in your application services, inject IOptions<TOptions>
into your constructor. The IOptions<TOptions>
interface provides access to the configured options values for TOptions
. When IOptions<TOptions>
is injected, it comes prepopulated with the configured values.
public class MyController : Controller
{
private readonly MySettings _settings;
public MyController(IOptions<MySettings> settings)
{
_settings = settings.Value;
}
public IActionResult Index()
{
// Access settings here
var setting1Value = _settings.Setting1;
var setting2Value = _settings.Setting2;
return View();
}
}
IOptions<TOptions>
loads values from the source only during application startup and then cached. It caches those values and only the cached value is used during the lifetime of the application. This is to improve overall application performance.
Post-Configure Options
The IPostConfigureOptions<TOptions>
interface allows you to configure options after all configuration and bindings have been applied. This is useful to configure options based on values from other options.
To use it:
- Create a class that implements
IPostConfigureOptions<TOptions>
- Implement the
PostConfigure
method. This gets passed the options instance after all configuration has occurred. - Add your IPostConfigureOptions implementation as a service in DI.
- The options framework will call PostConfigure after configuring the options instance.
For example:
public class MyPostConfigurer : IPostConfigureOptions<MyOptions>
{
public void PostConfigure(MyOptions options)
{
// Modify options here based on values from other options
}
}
The main use cases for this are:
- Setting default values based on other options values
- Validating options based on other options
- Resolving options dependencies across options types
This provides a flexible way to tweak final option values after all configuration and DI binding has occurred.
Options Validation
Options
classes can be validated using Data Annotation attributes or manually validated by implementing IPostConfigureOptions
.
Automatic Validation Using Data Annotations
The options classes can be validated automatically by decorating the properties with DataAnnotation attributes like Required
, StringLength
, Range
etc. For example, below code will validate the Options
class on application startup and throw a OptionsValidationException
if validation fails.
public class MyOptions {
[Required]
public string ImportantSetting { get; set; }
[StringLength(12, ErrorMessage = "Name is too long")]
public string Name { get; set; }
[Range(0, 5)]
public int Count { get; set; }
}
Manual Validation
For more complex validation scenarios, the options classes can implement the IPostConfigureOptions
interface.
public class MyOptions : IPostConfigureOptions<MyOptions> {
public void PostConfigure(string name, MyOptions options) {
if (options.ImportantSetting == null) {
throw new OptionsValidationException(...);
}
}
}
The PostConfigure
method allows you to manually validate the options. This runs after all the options bindings from the dependency injection container. Manual validation allows for more advanced scenarios compared to automatic validation attributes.
Reloading configuration values
When you use IOptions
, the configuration settings are read from the configuration sources during app startup and then cached for the lifetime of the application. So if the configuration settings are changed after the application startup, the application will never be able to get the latest value till the next start up. To overcome this, ASP.NET Core provides two other interfaces.
IOptionsSnapshot
IOptionsSnapshot
is a more advanced interface built on top of IOptions
. When you use IOptionsSnapshot
, the configuration settings are read from the configuration sources whenever the application starts or restarts, just like IOptions
. However, if the configuration settings change at runtime, the new values are automatically read and made available to your application. This is particularly useful in scenarios where you need to change configuration settings on the fly without restarting the application.
IOptionsMonitor
IOptionsMonitor
is an even more advanced interface built on top of IOptions
and IOptionsSnapshot
. IOptionsMonitor
provides ability to observe changes to configuration settings also allows you to subscribe to changes to the configuration settings, enabling your application to react to changes in real-time.
The choice between IOptions
, IOptionsSnapshot
, and IOptionsMonitor
depends on the specific needs of your application. If you have simple configuration settings, IOptions
will be a good choice. If you need to reload configuration settings at runtime, IOptionsSnapshot
will be a better fit. And if you need the most advanced features and real-time updates, IOptionsMonitor
will be the way to go.
Benefits of Options Pattern
- Strongly typed configuration – Options classes provide strongly typed access to configuration rather than using magic strings. This enables IDE intellisense and avoids runtime errors due to typos.
- Centralized configuration – Configuration values are defined and accessed from options classes rather than being scattered throughout the code. This keeps the configuration centralized and avoids duplication.
- Options validation – Options classes allow validating configuration values on application startup. This helps catch configuration errors early.
- Options post-configuration – Additional configuration can be done after binding, allowing tweaking of option values.
- Options reload – By using
IOptionsSnapshot
andIOptionsMonitor
you can have the latest configuration values.
Key Takeaways
- Define options classes to match logical configuration groups. Avoid putting all config in one large options class.
- Use validation on options classes to enforce invariants and business rules for configuration.
- Consider post-configuration to handle cross-cutting concerns among options.
- Reconfiguring options allows adjusting configuration at runtime. Use this judiciously based on app requirements.
Using the options pattern the right way helps build more robust, maintainable configuration in ASP.NET Core apps. Just be sure to scope options properly, validate configuration, and leverage best practices like post-config and reconfiguring where appropriate.