Photo by Olav Ahrens Røtne on Unsplash
How to Consume Dot Net 6 Configuration in Your Services Effectively
In Short: Implement IOptions, IOptionsSnapShot, and IOptionsMonitor for Effective Consumption
Previously on...
In my previous blog posts, I wrote about the modularity of the configuration that Dotnet 6 offers. About sources and sections and how to register them. I mentioned how to manually build the configuration object and how to use the default sources that Dotnet 6 offers. I also mentioned security-related information: using secrets.json, AppConfiguration Service and Azure key vault.
Context
When coaching individuals, I've observed that when transitioning from the DotNet Framework to Dotnet Core, people tend to instinctively want to work by what they know. That is not strange at all. In the article Why Do Your Employees Resist New Tech? (hbr.org)" are some reasons mentioned like lack of skills, costs, complexity, and infrastructure challenges. By writing these blog posts, I want to help inspire fellow developers to embrace the new way of working and actually... show how cool is the stuff that Microsoft has realised! Microsoft also promotes some good techniques on how to create architectures and code.
I notice that there is no little awareness about using the strongly typed classes that represent (part of) sections and how to validate the settings on the startup of an application. A big difference in design is the possibility to monitor changes and ensure that the application reacts to those changes, without the need to reboot the application.
In this post, I mention how to consume configuration, and the logic behind the different ways by showing sequence diagrams made in Mermaid Live. I also show how to debug a startup error on an Azure app service when a configuration setting does not validate.
Consume IConfiguration in Program.cs
I will use this way of working when setting up my services or consuming settings in the Program.cs
. However, I suggest binding sections to objects mentioned in my previous post. This ensures type safety and not working with strings.
To read from IConfiguration
or ConfigurationManager
I can use the GetValue
method and specify the configuration key, which can be composed of multiple sections separated by colons (:
). That colon will be there when there is a hierarchy defined. In the case of the configuration key --another--secret--value
, I need to read another--secret--value
. This is explained in the section above named "Prefix Dashes".
_config["my:secret:value"]
_config.GetValue<bool>("my:secret:value")
_config["another--secret--value"]
_config.GetValue<bool>("another--secret--value")
Another important thing to note is that when you use an IConfiguration
object or custom binding to a class, the validation of settings will not be triggered. The validation specified by ValidateOnStart
will be activated, as you might guess, at the start of the application. This means I cannot rely on validation because it will not be triggered before calling app.Run()
, app.Start()
or app.StartAsync
. After all, I am using configuration to setup my application.
Consume IOptions***<>
in the services
I may use the IConfiguration
and ConfigurationManager
objects to configure my services. The idea is that I use the IOptions<MySettings>
object in my services. DotNet will make the mapping between the sources and the IOptions
objects for me. This just works nicely!
When you want to cringe again, at how we need to do it in Net Framework, go read this old documentation: How to: Create Custom Configuration Sections Using ConfigurationSection | Microsoft Learn
Inject in a service IOptions<>
, IOptionsMonitor<>
or IOptionsSnapshot
Let me register MySettings
object using the .AddOptions
method.
services.AddOptions<MySettings>(Configuration.GetSection("MySettings"));
In a service or any other class where I want to use the configuration, I can inject IOptions<MySettings>
in the constructor:
public class MyController : Controller
{
private readonly IOptions<MySettings> _mySettings;
public MyController(IOptions<MySettings> mySettings)
{
_mySettings = mySettings;
}
public IActionResult Index()
{
return View();
}
}
That's it! Now I can use MySettings
configuration object in my code without worrying about parsing or loading configuration data myself.
Difference between IOptions
, IOptionsSnapshot
and IOptionsMonitor
IOptions
is the base interface for accessing configuration options and it is injected as a singleton service and cached for the lifetime of the application. It returns a single snapshot of the configured options at the time of its initialization.
using Microsoft.Extensions.Options;
public class MyService
{
private readonly IOptions<MySettings> _options;
public MyService(IOptions<MySettings> options)
{
_options = options;
}
public string GetOptionValue()
{
return _options.Value.MyOptionValue;
}
}
IOptionsSnapshot
, on the other hand, is a variation of IOptions that updates its values dynamically as it listens for changes in the underlying configuration system. This makes it more suitable for use in long-lived scopes such as request-scope services.
using Microsoft.Extensions.Options;
public class MyService
{
private readonly IOptionsSnapshot<MySettings> _options;
public MyService(IOptionsSnapshot<MySettings> options)
{
_options = options;
}
public string GetOptionValue()
{
return _options.Value.MyOptionValue;
}
}
IOptionsMonitor
is similar to IOptionsSnapshot
, but with the added capability of allowing registration for change notifications. It is useful when I want to monitor changes to the configuration data and perform actions based on the changes.
using Microsoft.Extensions.Options;
public class MyService: IDisposable
{
private readonly IOptionsMonitor<MySettings> _options;
private readonly IDisposable _optionsChangeToken;
public MyService(IOptionsMonitor<MySettings> options)
{
_options = options;
_optionsChangeToken = _options.OnChange((newOptions) => {
// Perform actions based on the new options
});
}
public string GetOptionValue()
{
return _options.CurrentValue.MyOptionValue;
}
public void Dispose()
{
_optionsChangeToken.Dispose();
}
}
Let me illustrate the above using some real use cases.
Suppose I have an ASP.NET Core application that uses IOptions
to access configurations related to the database connection string, cache settings, and other application settings. Since IOptions
is cached for the lifetime of the application, it is useful when I need to access these configuration settings multiple times throughout the lifetime of the application. The mermaid diagram below illustrates this.
When I have a long-lived scoped service such as a request-scope service, I would need to use IOptionsSnapshot
. For instance, suppose I have a request-scope service that provides caching for API responses, and I configure the cache duration using IOptionsSnapshot
. In this case, IOptionsSnapshot
ensures that the cache expiration time is updated in real-time as soon as the configuration value gets updated, making it more suitable for long-lived scoped services like request-scope services. The mermaid diagram below illustrates this.
Suppose I have an application where I need to monitor changes to the configuration data and perform actions based on the changes. In this case, I would use IOptionsMonitor
. For example, imagine that I have an email notification service in my application, and I have set the MySettings
section in the Azure AppService AppSettings. If at some point I need to change the values of the MySettings
section, it triggers change notifications, which the IOptionsMonitor
listens to and subsequently performs necessary actions like reconnecting to the new server. The mermaid diagram below illustrates this and shows the difference with the IOptionSnapshot as well.
IOptions StartupValidation Checkup in Azure Portal
Remember the following line of code:
serviceCollecton.AddOptions<MyViewOnSettings>("mysettings").ValidateOnStart().ValdiateDataAnnotations();
When a service encounters validation errors during startup, it will return an HTTP 500(.xx) response. When developing locally, I can run in debug mode to identify issues. To gain insight into what's happening in the cloud, Azure provides access to the Application Event Log, where I can find startup errors caused by the validation of options.
In the Azure App Service page, select the Diagnose and solve problems tab, then click on the Diagnostic Tools option. Within the Diagnostic Tools tab, choose Application Event Logs to view the list of events.
Read more about this on the following page: How to view Azure App Service Event Logs? - Bernard Lim | Azure | .NET | SharePoint | M365 (thebernardlim.com)
Do you want more?
While creating this post, I accessed a lot of sources. A couple of posts stood out.
Understanding IOptions, IOptionsMonitor, and IOptionsSnapshot in .NET 7 | Code4IT
How to view Azure App Service Event Logs? - Bernard Lim | Azure | .NET | SharePoint | M365 (thebernardlim.com)
Outro
This concludes the theory about Configuration. I mentioned in my first post I will make a drawing. That is underway. I am trying out some doodling techniques on my tablet. I will post it, with a summary and links to all these blog posts, later this year. For now, it is time to write about different topics
Did you like this three-part series? What would you change? What information are you missing? Let us connect and engage!