In this post, we learn how to use the options pattern in ASP.NET Core. The idea is that instead of directly using IConfiguration and having to hardcode sections and attributes of our configuration providers, we instead use a strongly typed option.
To use the options pattern, we have three services: IOptions, IOptionsSnapshot, and IOptionsMonitor. In this post, we’ll look at the differences between these three options.
What we want to avoid
We know we can access values from a configuration provider (such as appsettings.json or an environment variable) using the IConfiguration service. For example, if we have our connection strings placed like this:
"ConnectionStrings": {
"DB": "This is a connection string",
"Redis": "Redis connectoin"
},
To get the value of DB we can say:
var connectionString = configuration.GetSection("ConnectionStrings").GetValue<string>("DB");
However, I don’t like having to hardcode the section name, ConnectionStrings, and the property name, DB, because we could make mistakes when placing these strings.
An alternative is to use the options pattern to have a strongly typed way of accessing the connection string. The idea is that we’ll create a class in which we’ll place the values we want to obtain from the configuration providers. This way, we can avoid using “magic strings.” Let’s see.
Options Pattern
The first thing we’ll do is create a class that contains the section’s properties. In our case, we’ll be working with the ConnectionStrings section. Therefore, we’ll create the following class:
public class ConnectionStrings
{
public const string Section = "ConnectionStrings";
[Required]
public required string DB { get; set; }
[Required]
public required string Redis { get; set; }
}
Notice how we’ve conveniently added the section name as a constant field. This is so we always have it handy. We’ve also added two properties, DB and Redis, which correspond to the properties found in our appsettings.json. Finally, we’ve added the [Required] attribute to both properties to validate that this information is always present. Now, let’s configure this class to get the corresponding values. To do this, go to the Program class and, in the area where we configure our services, add the following code:
builder.Services.AddOptions<ConnectionStrings>()
.Bind(builder.Configuration.GetSection(ConnectionStrings.Section))
.ValidateDataAnnotations()
.ValidateOnStart();
With this, we can use the options pattern. The easiest way is to inject an IOptions into the controller’s constructor where we want to use it:
private readonly IOptions<ConnectionStrings> options;
public WeatherForecastController(IOptions<ConnectionStrings> options)
{
this.options = options;
}
Then, we can get the connection string as follows:
var connectionString = options.Value.DB;
Look at the difference! The code is now much cleaner than it was when we used IConfiguration directly.
Options, Options, and More Options
There are 3 options when using the options pattern:
- IOptions
- IOptionsSnapshot
- IOptionsMonitor
Let’s explore these 3 options.
The first, IOptions, can be used anywhere. This is characterized by being the fastest of the three, since it internally caches the values of the configuration providers. Therefore, it’s ideal for that “hot path” of your application, where performance is extremely important. However, this advantage is also the source of its problem. IOptions doesn’t allow you to retrieve new values in the app if the configuration provider is updated. For static values or values that rarely change, such as a connection string, IOptions is perfect. For values that we want to be able to change while the app is running? In that case, we have two options: IOptionsSnapshot and IOptionsMonitor.
IOptionsSnapshot allows us to retrieve the current value found in the configuration providers. Let’s look at an example. When creating a new Web API with ASP.NET Core, we are given an endpoint called WeatherForecast. It contains the following code:
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
This code instantiates 5 WeatherForecasts and returns them as an array to the client. Let’s assume I want that 5 to come from a settings provider, as it’s a value we can change at will, and without having to recompile the app, we want to be able to get the current value. To do that, we’ll create a section in appsettings.json:
"APIConfigurations": {
"weatherForecastsToReturn": 3
},
Then, we create the following class:
public class APIConfigurations
{
public const string Section = "APIConfigurations";
public required int WeatherForecastsToReturn { get; set; }
}
Now, let’s do the binding in the Program class:
builder.Services.AddOptions<APIConfigurations>()
.Bind(builder.Configuration.GetSection(APIConfigurations.Section))
.ValidateDataAnnotations()
.ValidateOnStart();
Finally, in our endpoint, we’re going to use IOptionsSnapshot, because I always want to get the current value found in the configuration provider:
app.MapGet("/weatherforecast", (IOptionsSnapshot<APIConfigurations> optionsSnapshot) =>
{
var weatherForecastsToReturn = optionsSnapshot.Value.WeatherForecastsToReturn;
var forecast = Enumerable.Range(1, weatherForecastsToReturn).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
With this, we can run the app and test the previous endpoint. And if we go to appsettings and update the value of the WeatherForecasts_count and save, without having to recompile, we’ll see that subsequent HTTP requests bring the correct number of elements into the WeatherForecasts array.
It’s important to note that the lifetime of IOptionsSnapshot is Scoped, meaning we always get a new instance per HTTP request. This means we can’t inject it directly into a singleton. In the latter case, we use IOptionsMonitor.
IOptionsMonitor is similar to IOptionsSnapshot in that it allows us to obtain the current value found in the configuration provider. However, there are two key differences: IOptionsMonitor is a singleton, and it allows for reactivity. The latter means we can execute code as soon as the value in the configuration provider is changed. Let’s look at an example.
Let’s suppose we have a Job that will run recursively. And we want to use the options pattern within it. Since a Job is a singleton, we can’t directly inject IOptionsSnapshot, and if we always want to get the current value from a configuration provider, we must use IOptionsMonitor.
First, let’s create a section for this example in appsettings.json:
"Notifications": {
"NotificationType": "sms"
},
We create the model class:
public class Notifications
{
public const string Section = "Notifications";
[Required]
public required string NotificationType { get; set; }
}
Then, we configure it as an option:
builder.Services.AddOptions<Notifications>()
.Bind(builder.Configuration.GetSection(Notifications.Section))
.ValidateDataAnnotations()
.ValidateOnStart();
Next, we create the class that represents the recurring Job that will use the options pattern and react to changes in the corresponding values of the configuration provider:
public class RecurringTask : BackgroundService
{
private Notifications notifications;
public RecurringTask(IOptionsMonitor<Notifications> optionsMonitor)
{
notifications = optionsMonitor.CurrentValue;
optionsMonitor.OnChange(newNotifications =>
{
if (newNotifications.NotificationType != notifications.NotificationType)
{
Console.WriteLine($"THE NOTIFICATION TYPE HAVE BEEN UPDATED TO {newNotifications.NotificationType}");
}
notifications = newNotifications;
});
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (true)
{
// Execute task...
Console.WriteLine($"The notification type is: {notifications.NotificationType}");
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
As we can see, we can use OnChange to react to changes made to the corresponding values. Finally, to test the code above, we configure the class as a Job in the Program class:
builder.Services.AddHostedService<RecurringTask>();
Now we can test it and see if it works. If we change the notification type at runtime, for example, to “email,” we’ll see the alert appear in the console and the value updated and used in the recurring task.
Learn more
If you want to learn more about building Web APIs, I have two courses for you (both in discount):
Minimal APIs with Entity Framework Core: https://felipe-gavilan.azurewebsites.net/api/Redireccion?curso=minimal-ef-eng
Minimal APIs with Dapper: https://felipe-gavilan.azurewebsites.net/api/Redireccion?curso=minimal-dapper-eng
Thanks!