In this article we are going to cover some advance scenarios where Options pattern is very useful.
We have covered the basic understanding of Options pattern into Options pattern in ASP.NET Core: Introduction article. Please go through it if not familiar with it.
We will cover below scenarios which are handled using Options pattern:
Validation of configuration settings using data annotation.
Validation of configuration settings using custom validation logic.
Validation of configuration settings during application startup.
Handling command line arguments or environment variables using options pattern.
Monitoring the changes of configuration settings.
Let's dive directly into code to understand all how Options pattern is used to handle all these scenarios.
GitHub repo link for the code sample https://github.com/dkj468/OptionsPatternDemo
Validation of configuration settings using data annotation
Options pattern allows to bind the DataAnnotations
rules to class which represents the section of application settings. This is powerful mechanism to validate for non empty, range and many more things.
consider the FileOptions
class from our first article :
public class FileOptions {
public const string Key = "file";
[Required(AllowEmptyStrings =false)]
public string MaxSize { get; set; }
public string FileType { get; set; }
public bool CanModify { get; set; }
[Range(minimum:2, maximum:6)]
public int MaxFileCount { get; set; }
}
We have applied some validation rules to class properties which represents the configuration setting.
Now we need to modify Program.cs
file to validate these data annotations.
builder.Services
.AddOptions<FileOptions>().Bind(builder.Configuration.GetSection("file"))
.ValidateDataAnnotations(); // validate the annotations
With above code changes, application is ready to validate annotation rule when user tries to access the configuration settings via Options pattern.
if you just remove the MaxSize
property from appsettings.json
file and try to access this inside controller then Microsoft.Extensions.Options.OptionsValidationException
exception will be thrown with proper message.
Microsoft.Extensions.Options.OptionsValidationException: DataAnnotation validation failed for 'FileOptions' members: 'MaxSize' with the error: 'The MaxSize field is required.'.
[Required(AllowEmptyStrings =false, ErrorMessage ="MaxSize cannot be empty")]
public string MaxSize { get; set; }
Custom validation
You could write custom validation logic to validate configuration settings value.
builder.Services
.AddOptions<FileOptions>().Bind(builder.Configuration.GetSection("file"))
.Validate(fileOptions => {
if (string.IsNullOrEmpty(fileOptions.FileType) {
return false;
}
return true;
});
Validate on application startup
builder.Services
.AddOptions<FileOptions>().Bind(builder.Configuration.GetSection("file"))
.ValidateDataAnnotations()
.ValidateOnStart();
With .NET 8 , you could combine this with AddOptions
method.
builder.Services
.AddOptionsWithValidateOnStart<FileOptions>()
.Bind(builder.Configuration.GetSection("file"));
Handling command line arguments or environment variables
To manage command line arguments or environment variables using options pattern, we somehow need a way to use ASP.NET Core dependency injection mechanism to access the required data.
So far we have used AddOptions
method to tell ASP.NET Core to bind the C# class with a specific section of the appsettings.json
file. So this method only allows us to bind the configurations which are part of appsettings.json
file. We need a way to bind the configuration from other sources like command line, environment variables, database, xml or ini files etc.
To achieve this, we will set options pattern using IConfigurationOptions
. This is two step process:
Create a class which implements
IConfigureOptions
interface, this class will allow us to access the other configuration sources via dependency injection.Modify
Program.cs
to use the class created in above step.
let's see this with code:
First, we will modify the FileOptions class to include two new properties: FileMode
and Version
.
let's create class with implementation of IConfigureOptions
public class FileOptionsSetup : IConfigureOptions<FileOptions> {
private readonly IConfiguration _config;
public FileOptionsSetup(IConfiguration config) {
_config = config;
}
public void Configure(FileOptions options) {
_config.GetSection("file").Bind(options);
// bind the environment variable
options.filemode = Environment.GetEnvironmentVariable("filemode");
// bind the command line argument
options.version = _config.GetValue<string>("version");
}
}
In above code, we first bind the appsettings.json
section to FileOptions
object and then read the configuration from environment variable and command line respectively.
We could extend above code to include configurations from almost any source
Now let's tell our program.cs
to use above class for configurations.
builder.Services.ConfigureOptions<FileOptionsSetup>();
With all above code, our code is now powered to handle configuration from sources other than appsettings.json
Monitoring the changes in configuration settings
Let's recall how our code uses the IOptions
interface for injecting the FileOptions
class into controller constructor. There are two more interfaces which could be used to inject the options classes vai dependency injection into ASP.NET Core application.
Let's understand more about these in details :
IOptions
After running the program,
IOptions
doesn't monitor for changes into application configurations. It always gives the same value.This works as singleton and can be injected in any service lifetime.
IOptionsSnapshot
This is useful in scenario where configuration needs to be read for each request - It works as a scoped service.
This can be injected into a scoped or transient service but not in singleton service.
appsettings.json
is read for each request.
IOptionsMonitor
This works as singleton and can be injected in any service lifetime.
this interface has an
OnChnage
method which is triggered if there are changes inappsettings.json
public FileController(IConfiguration config, IOptionsMonitor<FileOptions> optionsMonitor) { _config = config; _optionsMonitor = optionsMonitor; _fileOptions = _optionsMonitor.CurrentValue; // notice here _optionsMonitor.OnChange(options => { // onchange method _fileOptions = options; }); }
๐ Use IOptions<T>
when you are not expecting changes in appsettings
content.
๐ Use IOptionsSnapshot<T>
when values are expected to change but want it to be consistent during a request.
๐ Use IOptionsMonitor<T>
when real time value is expected.
Conclusion
In this article we have learned about some advance use cases where options pattern is quite useful. All such scenarios are difficult to manage using IConfiguration
method.
Thanks for reading.