Using Options Pattern in .NET Core for Strongly Typed Configuration

Using Options Pattern in .NET Core for Strongly Typed Configuration

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.

City Status endpoint with IOptions

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.

City Status endpoint with OptionsSnapshot and OptionsMonitor

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!