Hello everyone! In this tutorial, we are going to learn about the Options Pattern in ASP.NET Core, understand its purpose, and explore how to use it effectively.
If you want to learn via video, here is the Youtube video of this blog post:
Problem with Weakly-Typed Configuration
Let's start by looking at a simple example. I have a GET endpoint named CityStatus
, which returns the name and population of a city. The data for name and population is being read from the appsettings file under the key CityStatus
.
Here's how the data in the appsettings file looks like:
{
"CityStatus": {
"Name": "Istanbul",
"Population": 20000
}
}
When I execute the endpoint, the response is the expected configuration data: Istanbul
and 20000
.
However, I don't have strong typing to the configuration keys in the controller's constructor. This is where the Options Pattern comes in.
Creating a Strongly Typed Class for Configuration
First, let's create a class for our CityStatus
configuration. I'll name this class CityStatusOptions
and define two properties: Name
and Population
.
public class CityStatusOptions
{
public string Name { get; set; }
public int Population { get; set; }
}
Next, we need to bind our CityStatusOptions
to the configuration. To do this, we'll use the Configure
method in the Startup
class.
services.Configure<CityStatusOptions>(Configuration.GetSection("CityStatus"));
Now, we can inject this strongly typed configuration using the IOptions<CityStatusOptions>
interface in the controller.
public CityStatusController(IOptions<CityStatusOptions> options)
{
_options = options.Value;
}
We can now access the data in the endpoint using the _options.Name
and _options.Population
properties.
Executing the endpoint again, I get the same response as before, but now with a strongly typed configuration.
Handling Configuration Updates with IOptionsSnapshot and IOptionsMonitor
If we update the population value in the appsettings file and execute the endpoint again without restarting the application, we'll notice that the Options Pattern still returns the old value.
This is because the Options Pattern reads the data once and always returns the same value. To handle updated configuration values, we can use two interfaces: IOptionsSnapshot<T>
and IOptionsMonitor<T>
.
Here's how we can inject these interfaces and return the updated values in the endpoint:
public CityStatusController(IOptions<CityStatusOptions> options,
IOptionsSnapshot<CityStatusOptions> optionsSnapshot,
IOptionsMonitor<CityStatusOptions> optionsMonitor)
{
_options = options.Value;
_optionsSnapshot = optionsSnapshot.Value;
_optionsMonitor = optionsMonitor.CurrentValue;
}
With IOptionsSnapshot<T>
and IOptionsMonitor<T>
, we can now get the updated values in the response when the configuration is changed.
Although both interfaces provide updated values, there is a key difference between them: their lifecycles.
IOptionsMonitor<T>
is a singleton and always returns the updated value, but it is always injected as a singleton.IOptionsSnapshot<T>
is scoped, and it reads the data from the configuration every time it is constructed.
Validating Configuration using Data Annotations
Another feature of the Options Pattern is validating configurations using data annotations. For example, to ensure the Population
value is not less than zero, we can use the [Range] attribute:
public class CityStatusOptions
{
public string Name { get; set; }
[Range(0, long.MaxValue)]
public int Population { get; set; }
}
To apply data validation, we need to bind the Options Pattern differently using the AddOptions
method in the Startup
class.
services.AddOptions<CityStatusOptions>()
.Bind(Configuration.GetSection("CityStatus"))
.ValidateDataAnnotations();
If we run the project and update the Population
value to a negative number, an exception will be thrown when accessing the endpoint.
However, if we want the application to throw an exception during bootstrapping if the initial configuration is invalid, we need to use the ValidateOnStart
method:
services.AddOptions<CityStatusOptions>()
.Bind(Configuration.GetSection("CityStatus"))
.ValidateDataAnnotations()
.ValidateOnStart();
Now, when running the project with an invalid initial configuration, the application throws an exception immediately.
Conclusion
In this tutorial, we learned about the Options Pattern in ASP.NET Core and its benefits for strongly typed configuration. We covered how to create a strongly typed class for configuration and how to handle configuration updates using IOptionsSnapshot<T>
and IOptionsMonitor<T>
, and how to validate configuration using data annotations.
Thank you for reading, and I hope this tutorial was helpful. Stay tuned for the next content; may the force be with you!