Hello everyone, in a previous article, we saw how to retrieve the configurations of our applications through the IConfiguration interface. Now, we will move on to using a method that, in my opinion, is certainly cleaner and more responsive: the Options Pattern.
Table of Contents:
- The Options Pattern
- Configuration with IOptionsSnapshot
- Configuration with IOptionsMonitor
- Configuring Named Options
- Choosing Between Configurations
The Options Pattern
The Options Pattern provides us with strongly typed access to our configurations, thanks to the use of classes, and centralized configuration through the use of configuration in the Program.cs file. Configuration retrieval is achieved through the use of the three main interfaces that this paradigm offers, namely:
- IOptions
- IOptionsSnapshot
- IOptionsMonitor
Let's try to create a simple basic configuration using the Options Pattern. First, let's add a fictitious configuration to our appsettings.json file for educational purposes:
"OptionsConfigurationBase": {
"Test1": "It's a test configuration"
}
Next, we will create our class with properties that match the ones in the previously configured appsettings section:
public class OptionsConfigurationBase
{
public const string ConfigurationName = nameof(OptionsConfigurationBase);
public string Test1 { get; set; }
}
Once done, let's configure our class in the Program.cs file using the extension method .Configure
builder.Services.Configure<OptionsConfigurationBase>(
builder.Configuration
.GetSection(OptionsConfigurationBase.ConfigurationName)
);
so that it can be subsequently injected through the Dependency Injection of .NET Core.
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
//.... other variable
private readonly ILogger<WeatherForecastController> _logger;
private readonly OptionsConfigurationBase _options;
public WeatherForecastController(ILogger<WeatherForecastController> logger, IOptions<OptionsConfigurationBase> options)
{
_logger = logger;
_options = options.Value ?? throw new ArgumentNullException(nameof(options));
}
// ...other methods
[HttpGet]
[Route("GetOptionsTest")]
public string GetOptionsTest()
{
return _options.Test1;
}
}
Now, this is certainly a great system; the only issue is that this configuration does not allow for reading changes at runtime. Therefore, any modifications made after the application starts will not be considered. To achieve this type of behavior, we'll need to use the IOptionsSnapshot interface.
Configuration with IOptionsSnapshot
This configuration is very useful when options need to be recalculated for each request. Essentially, this is what the IOptionsSnapshot interface does: it is registered as a Scoped service and caches the configurations for each request.
There are no changes in registration compared to the IOptions interface, except for the fact that in the constructor of our controller, we change IOptions to IOptionsSnapshot.
public WeatherForecastController(ILogger<WeatherForecastController> logger, IOptionsSnapshot<OptionsConfigurationBase> options)
{
_logger = logger;
_options = options.Value ?? throw new ArgumentNullException(nameof(options));
}
If, after the application is started, we change the value of the "Test1" parameter from:
"Test1": "It's a test configuration"
to:
"Test1": "It's a test configuration updated"
And if we make the request again after changing the "Test1" parameter to "It's a test configuration" we will see that the returned result will be "It's a test configuration updated".
This configuration is certainly useful, but it should be used with caution because it could potentially lead to performance issues since, as a Scoped service, it is recalculated for every request.
Configuration with IOptionsMonitor
Last but not least, the use of the IOptionsMonitor interface is certainly important. This service is configured as a Singleton service and will retrieve updated values of our configurations at any time. One key difference is found in the constructor when retrieving the injected service, instead of using the .Value property, we will use the .CurrentValue property:
public WeatherForecastController(ILogger<WeatherForecastController> logger, IOptionsMonitor<OptionsConfigurationBase> options)
{
_logger = logger;
_options = options.CurrentValue ?? throw new
ArgumentNullException(nameof(options));
}
Exactly as before, when making the initial call, we will receive the response with the value "It's a test configuration updated" However, if we update the property to "I'm a test configuration updated and retrieved with IOptionsMonitor" this value will be retrieved from our service.
Configuring Named Options
Named options are useful when we need multiple sections within a single configuration object.
Let's modify the appsettings.json file as follows:
"OptionsConfigurationBase": {
"NamedOptions1": {
"Test1": "I am the Named Option 1 configuration"
},
"NamedOptions2": {
"Test1": "I am the Named Option 2 configuration"
}
}
let's make a small modification to our class:
public class OptionsConfigurationBase
{
public const string NamedOptions1 = "NamedOptions1";
public const string NamedOptions2 = "NamedOptions2";
public string Test1 { get; set; }
}
finally, let's modify the Program.cs to support this new configuration option:
builder.Services.Configure<OptionsConfigurationBase>(
OptionsConfigurationBase.NamedOptions1,
builder.Configuration.GetSection($"{nameof(OptionsConfigurationBase)}:{OptionsConfigurationBase.NamedOptions1}")
);
builder.Services.Configure<OptionsConfigurationBase>(
OptionsConfigurationBase.NamedOptions2,
builder.Configuration.GetSection($"{nameof(OptionsConfigurationBase)}:{OptionsConfigurationBase.NamedOptions2}")
);
How do we retrieve configurations in the controller? By using the Get method associated with the IOptionsSnapshot interface, like this:
private readonly OptionsConfigurationBase _namedOptions1;
private readonly OptionsConfigurationBase _namedOptions1;
public WeatherForecastController(ILogger<WeatherForecastController> logger, IOptionsSnapshot<OptionsConfigurationBase> options)
{
_logger = logger;
_namedOptions1 = options.Value.Get(OptionsConfigurationBase.NamedOptions1)?? throw new ArgumentNullException(nameof(options));
_namedOptions1 = options.Value.Get(OptionsConfigurationBase.NamedOptions2)?? throw new ArgumentNullException(nameof(options));
}
Choosing Between Configurations
As we've seen, each of these interfaces has its own characteristics, and choosing between them is not always straightforward. However, we can do a quick review to help you choose the right interface based on your needs:
IOptions
- It doesn't support runtime reloading of configuration values.
- It's registered as a Singleton service in the .NET Core Dependency Injection container.
- It can be injected into any service, whether registered as Singleton, Scoped, or Transient.
- It doesn't support named options.
IOptionsSnapshot
- Useful when configurations need to be reconfigured for each request.
- Registered as Scoped, which means it cannot be injected into a Singleton service and might have performance issues.
- Supports named options.
IOptionsMonitor
- Registered as a Singleton service in the .NET Core Dependency Injection container.
- Supports named options.
- Dynamic changes reloaded for each request.
And with that, our overview of the Options Pattern in .Net Core comes to an end. I hope this article helps you choose the right interface and understand how this pattern makes configuration retrieval in our applications easy and dynamic.
You can find all the code used in the article (it's not much) in this repository:
If you enjoyed the article, please share it and give it a like to make it visible to others. I look forward to reading your comments where you share your experiences with this feature.
Happy Coding!
Top comments (6)
And for those who would see using Azure Key Vaults or Azure Function configuration a limitation for the options pattern, don't worry!
In Key Vaults, double dash (--) serves as hierarchical marker.
Ex. : OptionsConfigurationBase--Test1
is the same as
"OptionsConfigurationBase": { "Test1": "It's a test configuration" }
The same can be achieved in Function Apps with double underscore (__), and prievioulsy colons.
Thank you very much for this insight @michelsylvestre !!
good article
Nice article.
Thank you for your efforts.
Great article, insightful 💡
Useful article
Some comments may only be visible to logged-in visitors. Sign in to view all comments.