ASP.NET Core Configuration â Options Pattern
ASP.NET Core Configuration â Options Pattern êŽë š
In this article, weâre going to cover another way of reading configuration data in .NET Core â the options pattern. The options pattern helps us group related configuration settings, and it provides strongly typed access to them. We are going to learn how the options pattern works and how we can improve our existing configuration access or even reload the configuration in real-time.
If youâve missed some of the basic configuration stuff, check out the ASP.NET Core Configuration Basics.
Info
The source code for this article can be found on the ASP.NET Core Configuration repo on GitHub (CodeMazeBlog/aspnet-core-configuration
). If you wish to follow along, use the basic-concepts
(CodeMazeBlog/aspnet-core-configuration
) branch. To check out the finished source code, check out the options-pattern
(CodeMazeBlog/aspnet-core-configuration
) branch.
Letâs dive in.
Why the Options Pattern?
In the previous article, weâve seen how we can bind configuration data to strongly typed objects. The options pattern gives us similar possibilities, but it offers a more structured approach and more features like validation, live reloading, and easier testing.
Once we configure the class containing our configuration we can inject it via dependency injection with IOptions<T>
and thus injecting only part of our configuration or rather only the part that we need.
If we need to reload the configuration without stopping the application, we can use the IOptionsSnapshot<T>
interface or the IOptionsMonitor<T>
interface depending on the situation. Weâll see when these interfaces should be used and why.
The options pattern also provides a good validation mechanism that uses the widely used DataAnotations attributes to check if the configuration abides by the logical rules of our application.
The testing of options is also easy because of the helper methods and easy to mock options classes.
So, in short, the options pattern helps us to:
- bind the configuration data to strongly typed objects
- group the configuration data in logical sections
- reload the configuration while the application is running
- validate the configuration
- inject only the needed parts of the configuration into different parts of the application
- test the configuration easier
Letâs see some real examples of the options pattern usage.
How to Use the Options Pattern to Read Configuration with IOptions Interface
Okay, letâs start by looking at the Index method of the home controller without any options pattern implemented:
public IActionResult Index()
{
var logLevelConfiguration = new LoggingLevelConfiguration();
_configuration.Bind("Logging:LogLevel", logLevelConfiguration);
var homeModel = new HomeModel
{
DefaultLogLevel = logLevelConfiguration.Default
};
return View(homeModel);
}
In this case, the configuration is injected through the IConfiguration interface in the HomeController constructor, so weâre actually able to access the whole configuration whether we need it or not.
After that, weâre binding the LogLevel
subsection of the configuration to the LoggingLevelConfiguration
class, in order to send it to the Index
view.
Okay, now letâs see how that works with IOptions<T>
interface.
Letâs add a few values to our appsettings.json file first:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"sqlConnection": "server=.; database=CodeMazeCommerce; Integrated Security=true"
},
"Pages": {
"HomePage": {
"WelcomeMessage": "Welcome to the ProjectConfigurationDemo Home Page",
"ShowWelcomeMessage": true,
"Color": "black"
}
},
"AllowedHosts": "*"
}
Weâll be using the HomePage
subsection to configure our Index
view of the HomeController
.
Next, we need a class that will contain these properties, so letâs create it in the Models folder of the project:
public class TitleConfiguration
{
public string WelcomeMessage { get; set; }
public bool ShowWelcomeMessage { get; set; }
public string Color { get; set; }
}
We need to make sure these property names match those of the appsettings.json file section.
Now we can modify the HomeController
to support the options pattern. First, letâs inject IOptions<HomePageController>
instead of IConfiguration
as we did before:
private readonly TitleConfiguration _homePageTitleConfiguration;
public HomeController(ILogger<HomeController> logger,
IOptions<TitleConfiguration> homePageTitleConfiguration)
{
_logger = logger;
_homePageTitleConfiguration = homePageTitleConfiguration.Value;
}
As you can see weâre accessing the configuration data via the Value
property of the IOptions
interface.
We are going to change the HomeModel class to reflect these changes too:
public class HomeModel
{
public TitleConfiguration Configuration { get; set; }
}
We need to change the Index method too:
public IActionResult Index()
{
var homeModel = new HomeModel
{
Configuration = _homePageTitleConfiguration
};
return View(homeModel);
}
And we should add some logic to the view so we can use these properties weâve defined:
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
@{
if (Model.Configuration.ShowWelcomeMessage)
{
<h1 class="display-4" style="color:@Model.Configuration.Color">@Model.Configuration.WelcomeMessage</h1>
}
}
</div>
This should give us a big red title right in the middle of our Home Page.
Now, the only thing left to do is to actually register and configure the TitleConfiguration
in our Startup
class:
services.Configure<TitleConfiguration>(Configuration.GetSection("Pages:HomePage"));
This should register our HomePage configuration
Now if we run our application the result is pretty clear:
Great! Weâre getting the right properties to the view.
But if we want to change the title to the color green for example, we need to restart the application to do it.
But thereâs a better way to do it! And itâs called IOptionsSnapshot
.
Using IOptionsSnapshot to Read the Updated Configuration
IOptionsSnapshot contains the values just for the lifetime of a request. So that means itâs registered as a scoped service in our application and we can use it only with scoped and transient dependencies. We cannot inject it into singleton services!
If we need to change the configuration without restarting the application, we need to implement IOptionsSnapshot<T>
because IOptions<T>
doesnât support it.
Letâs modify our code to use IOptionsSnapshot<T>
instead of IOptions<T>
.
Changing our code, in this case, is easy, we just need to change the constructor of the HomeController
:
public HomeController(ILogger<HomeController> logger,
IOptionsSnapshot<TitleConfiguration> homePageTitleConfiguration)
{
_logger = logger;
_homePageTitleConfiguration = homePageTitleConfiguration.Value;
}
If we run the application, weâll get the same home page as we did before, big red title.
But to test IOptionsSnapshot
, letâs go to our appsettings.json file and change the color of the text to âblueâ:
"Pages": {
"HomePage": {
"WelcomeMessage": "Welcome to the ProjectConfigurationDemo Home Page",
"ShowWelcomeMessage": true,
"Color": "blue"
}
},
Now, we just need to refresh the HomePage and our title will be blue if we did everything correctly:
Great, weâve successfully modified our application to reload the configuration data dynamically.
Using IOptionsMonitor for Singleton Services
There is one problem with our current solution, and weâve already mentioned it. IOptionsSnapshot
is not suitable to be injected into services registered as a singleton in our application.
To demonstrate this, letâs create a simple service and try to inject IOptionsSnapshot
into it.
First, letâs extend our Home Page title configuration a bit. Weâll add a new configuration property UseRandomTitleColor
to our TitleConfiguration
class:
public class TitleConfiguration
{
public string WelcomeMessage { get; set; }
public bool ShowWelcomeMessage { get; set; }
public string Color { get; set; }
public bool UseRandomTitleColor { get; set; }
}
And weâll change appsettings.json
to reflect it:
"Pages": {
"HomePage": {
"WelcomeMessage": "Welcome to the ProjectConfigurationDemo Home Page",
"ShowWelcomeMessage": true,
"Color": "blue",
"UseRandomTitleColor": true
}
},
After that, weâll need a service we can register as a singleton, so letâs create a new one. Weâll call it ITitleColorService
and weâll create it in a separate folder called Services
:
public interface ITitleColorService
{
string GetTitleColor();
}
ITitleColorService
declares just one method and thatâs GetTitleColor
, which should return a random color from the list of defined colors. So letâs create TitleColorService
in the same folder and implement this interface:
public class TitleColorService : ITitleColorService
{
private readonly string[] _colors = { "red", "green", "blue", "black", "purple", "yellow", "brown", "pink" };
private readonly TitleConfiguration _homePageTitleConfiguration;
public TitleColorService(IOptionsSnapshot<TitleConfiguration> homePageTitleConfiguration)
{
_homePageTitleConfiguration = homePageTitleConfiguration.Value;
}
public string GetTitleColor()
{
var random = new Random();
var colorIndex = random.Next(7);
return _homePageTitleConfiguration.UseRandomTitleColor ?
_colors[colorIndex] :
_homePageTitleConfiguration.Color;
}
}
Itâs a simple implementation, that returns one of the colors from the array of colors if UseRandomTitleColor
is set to true, and the value of the Color
property if itâs set to false. Take note that weâre using IOptionsSnapshot
to inject our configuration.
Now we need to register our service with our application, and we do that in Startup class, in the ConfigureServices
method:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<TitleConfiguration>(Configuration.GetSection("Pages:HomePage"));
services.TryAddSingleton<ITitleColorService, TitleColorService>();
services.AddControllersWithViews();
}
And finally, letâs change HomeController
to override the title color:
private readonly ILogger<HomeController> _logger;
private readonly TitleConfiguration _homePageTitleConfiguration;
private readonly ITitleColorService _titleColorService;
public HomeController(ILogger<HomeController> logger,
IOptionsSnapshot<TitleConfiguration> homePageTitleConfiguration,
ITitleColorService titleColorService)
{
_logger = logger;
_homePageTitleConfiguration = homePageTitleConfiguration.Value;
_titleColorService = titleColorService;
}
public IActionResult Index()
{
var homeModel = new HomeModel
{
Configuration = _homePageTitleConfiguration
};
homeModel.Configuration.Color = _titleColorService.GetTitleColor();
return View(homeModel);
}
Thatâs about it. Or is it?
IOptionsMonitor<T>
Purpose
Now if we run the application, it crashes and we get the following exception message:
Note
Some services are not able to be constructed (Error while validating the service descriptor âServiceType: ProjectConfigurationDemo.Services.ITitleColorService Lifetime: Singleton ImplementationType: ProjectConfigurationDemo.Services.TitleColorServiceâ: Cannot consume scoped service âMicrosoft.Extensions.Options.IOptionsSnapshot`1[ProjectConfigurationDemo.Models.TitleConfiguration]â from singleton âProjectConfigurationDemo.Services.ITitleColorServiceâ.)
This happens because ASP.NET Core is trying to prevent us from making a mistake of referencing a scoped service from a singleton. Itâs a classic mistake and it could result in unexpected behavior otherwise. To put it in simple terms if the parent is a singleton, we canât create a child service per page loaded. Child service has to be singleton or transient instead.
And thatâs exactly where IOptionsMonitor
comes in.
Letâs go back to our service and replace IOptionsSnapshot
with IOptionsMonitor
:
public TitleColorService(IOptionsMonitor<TitleConfiguration> homePageTitleConfiguration)
{
_homePageTitleConfiguration = homePageTitleConfiguration.CurrentValue;
}
The only thing we need to modify is the constructor. IOptionsMonitor
uses the CurrentValue
instead of Value
retrieving the configuration values.
Now if we run the application again weâre not getting the exception we did before. If we refresh the page, our title screen shows up in different colors weâve defined previously.
Great!
There is one more concept we need to cover.
Named Options
Named options are not the feature we need to use very often, but there is a specific case where it can come in handy.
Letâs imagine we have a configuration like this one:
"Pages": {
"HomePage": {
"WelcomeMessage": "Welcome to the ProjectConfigurationDemo Home Page",
"ShowWelcomeMessage": true,
"Color": "black",
"UseRandomTitleColor": true
},
"ProductPage": {
"WelcomeMessage": "Welcome to the ProjectConfigurationDemo Product Page",
"ShowWelcomeMessage": true,
"Color": "black",
"UseRandomTitleColor": false
}
},
We have the same configuration structure for the different configuration subsections of the section âPagesâ. Both the âHomePageâ and the âProductPageâ have the exact same configuration properties.
Instead of adding another configuration class thatâs exactly the same as our TitleConfiguration
weâve already implemented, we can use that class to map different subsections of the Pages section and just name them differently to be able to differentiate between each other.
This might be a bit confusing so letâs implement it. In our Startup
class we should configure:
services.Configure<TitleConfiguration>("HomePage", Configuration.GetSection("Pages:HomePage"));
services.Configure<TitleConfiguration>("ProductPage", Configuration.GetSection("Pages:ProductPage"));
Now both subsections are mapped to the same configuration class, which makes sense. We donât want to create multiple classes with the same properties and just name them differently. This is a much better way of doing it.
Calling the specific option is now done using the Get()
method, so we need to refactor our TitleColorService
class a bit:
public class TitleColorService : ITitleColorService
{
private readonly string[] _colors = { "red", "green", "blue", "black", "purple", "yellow", "brown", "pink" };
private readonly IOptionsMonitor<TitleConfiguration> _titleConfiguration;
public TitleColorService(IOptionsMonitor<TitleConfiguration> titleConfiguration)
{
_titleConfiguration = titleConfiguration;
}
public string GetTitleColor(string pageTitleConfiguration)
{
var random = new Random();
var colorIndex = random.Next(7);
var titleConfiguration = _titleConfiguration.Get(pageTitleConfiguration);
return titleConfiguration.UseRandomTitleColor ?
_colors[colorIndex] :
titleConfiguration.Color;
}
}
We need to change the ITitleColorService
interface as well:
public interface ITitleColorService
{
string GetTitleColor(string pageTitleConfiguration);
}
And change the HomeController
accordingly:
public HomeController(ILogger<HomeController> logger,
IOptionsSnapshot<TitleConfiguration> homePageTitleConfiguration,
ITitleColorService titleColorService)
{
_logger = logger;
_homePageTitleConfiguration = homePageTitleConfiguration.Get("HomePage");
_titleColorService = titleColorService;
}
public IActionResult Index()
{
var homeModel = new HomeModel
{
Configuration = _homePageTitleConfiguration
};
homeModel.Configuration.Color = _titleColorService.GetTitleColor("HomePage");
return View(homeModel);
}
Thatâs it, now if we run the application, weâll see exactly the same result as before. The only thing here we want to change is to use string constants rather than string literals to get the subsections, as to avoid runtime errors. But you can play around a bit and try it out.
Letâs summarize.
Choosing Between IOptions, IOptionsSnapshot, and IOptionsMonitor
So in short
IOptions<T>
- Is the original Options interface and itâs better than binding whole Configuration
- Does not support configuration reloading
- Is registered as a singleton service and can be injected anywhere
- Binds the configuration values only once at the registration, and returns the same values every time
- Does not support named options
IOptionsSnapshot<T>
- Registered as a scoped service
- Supports configuration reloading
- Cannot be injected into singleton services
- Values reload per request
- Supports named options
IOptionsMonitor<T>
- Registered as a singleton service
- Supports configuration reloading
- Can be injected into any service lifetime
- Values are cached and reloaded immediately
- Supports named options
Having said that, we can see that if we donât want to enable live reloading or we donât need named options, we can simply use IOptions<T>
. If we do, we can use either IOptionsSnapshot<T>
or IOptionsMonitor<T>
, but IOptionsMonitor<T>
can be injected into other singleton services while IOptionsSnapshot<T>
cannot.
Sometimes the nature of the project dictates that we shouldnât change our configuration âon the flyâ. Sometimes itâs needed. Be careful when choosing the âright optionâ, no pun intended đ
Conclusion
In this article, weâve learned what options are and why theyâre good for us. Weâve also covered different ways to implement options, as well as the pros and cons of each one. As weâve mentioned, this is a powerful mechanism and you need to decide carefully which one is the best for your concrete project.
In the next article, weâll cover options validation, and you can find other parts of this series on the ASP.NET Core Web API page.