.NET 8 Series has started! Join Now for FREE

10 min read

Options Pattern in ASP.NET Core – Bind & Validate Configurations from appsettings.json

#dotnet

In this article, we will learn about using the Options Pattern in ASP.NET Core to Manage, Bind, and Validate your configurations from appsettings.json. Reading Configuration is a very common requirement that you can have in any kind of application. We usually set these configurable values in files, which in ASP.NET Core applications is the appsettings.json. I will build a simple ASP.NET Core Web API just to demonstrate the various use cases using the Options pattern.

Options pattern generally use classes to ensure that we have a strongly typed reference to the settings that we can load from appsettings.json and definitely boosts the developer experience and productivity.

Options Pattern – The Requirement

For this demonstration, let’s assume that we have an ASP.NET Core Web API (.NET 7), which for some wild reason should return us the weather information from the appsettings.json file. I will set up multiple endpoints, each of which will demonstrate a particular use case.

Here is how we would define the Weather details in the appsettings.json file.

"WeatherOptions": {
"City": "Trivandrum",
"State": "Kerala",
"Temperature": 22,
"Summary": "Warm"
}

Our goal is to read the data from this file during runtime and display it as a result while invoking the endpoints.

Via IConfiguration

There are multiple ways to retrieve configurations from appsettings.json of ASP.NET Core applications. Here is a basic way of binding with the configurations from the appsettings file in ASP.NET Core applications.

public class WeatherController : ControllerBase
{
private readonly IConfiguration _configuration;
public WeatherController(IConfiguration configuration)
{
_configuration = configuration;
}
[HttpGet("config")]
public IActionResult Get()
{
var city = _configuration.GetValue<string>("WeatherOptions:City");
var state = _configuration.GetValue<string>("WeatherOptions:State");
var temperature = _configuration.GetValue<int>("WeatherOptions:Temperature");
var summary = _configuration.GetValue<string>("WeatherOptions:Summary");
return Ok(new
{
City = city,
State = state,
Temperature = temperature,
Summary = summary
});
}
}

As you can see, we inject an instance of IConfiguration into the controller’s constructor. I have added a new Action method with the route as config. Here, I am individually pulling out the settings using its key from the _configuration variable. Although this is an easier way to achieve what we want, there are quite a lot of trade-offs in using this approach.

Here is the result when I call the /weather/config endpoint.

alt text

In this instance, we are relying on IConfiguration to load the required configurations from the appsettings.json. This can be bad for the security of the application since IConfigration has access to all the other configuration options as well which are not meant to be derived. You will also have to name your keys properly to avoid runtime errors.

Here are the other cons of using IConfiguration in ASP.NET Core applications.

  • No Validation: IConfiguration does not perform any validation over the configuration values, which might be fatal during the application runtime.
  • No Type-Safety: As mentioned earlier, the interface reads configurations as strings that have to be parsed manually. This also increases the chances of configuration-related issues.
  • No Default values: In case the required key is empty / not found in appsettings, there is no built-in way to return a default value that can be used throughout the application.

IConfiguration is a simple and lightweight approach for loading configurations in ASP.NET Core applications from the appsettings file. However, we need a more robust solution for handling a bit more advanced requirements like Validations, Type-Safety, and Reloading. This is where the Options Pattern comes in.

Getting Started with Options Pattern in ASP.NET Core

The Options pattern is a design pattern to manage and load configurations from the appsettings file in ASP.NET Core applications. It gives a way to build strongly typed configurations that can be used throughout the entire application with ease, providing additional features like validation, default values, reloading and so much more.

Here is how you would use the Options Pattern. First, define the required options classes that represent your configuration settings. In our case, the class will be named as WeatherOptions and will have properties as mentioned in appsettings. Validation logic can also be defined here, which we will go through in the next section.

Once the class is defined, we will have to register them into the application’s service container and bind the class with the data from appsettings. With that done, we can use these classes throughout the application using dependency injection. Let’s see it in action now.

First, let me add the WeatherOptions class.

public class WeatherOptions
{
public string? City { get; set; }
public string? State { get; set; }
public int? Weather { get; set; } = null;
public string? Summary { get; set; }
}

Here, I have added the properties that we had earlier added as part of our appsettings section. Next, let’s do the service registration of IOptions. Open up your Program.cs and add the following soon after the AddControllers() extension method.

builder.Services.AddOptions<WeatherOptions>().BindConfiguration(nameof(WeatherOptions));

This line of code registers an instance of WeatherOptions, binds it with values it can get from the appsettings file, and make the WeatherOptions data available throughout the entire application. Note that we have bounded the configuration to the Section name, which is also WeatherOptions. Now, the WeatherOptions instance can be injected into the classes and it’s values can be fetched using the IOptions interface.

Let’s go back to our WeatherController and add in a new API endpoint, this time named as GetFromOptionsPattern (“options”).

public class WeatherController : ControllerBase
{
private readonly WeatherOptions _weatherOptions;
public WeatherController(IOptions<WeatherOptions> weatherOptions)
{
_weatherOptions = weatherOptions.Value;
}
[HttpGet("options")]
public IActionResult GetFromOptionsPattern()
{
return Ok(new
{
_weatherOptions.City,
_weatherOptions.State,
_weatherOptions.Temperature,
_weatherOptions.Summary
});
}
}

As you see in the above snippet, we are injecting the WeatherOptions into the constructor of the controller, whose values will be fetched from IOptions<WeatherOptions>. Finally, in the action method, we will be able to access the values of the WeatherOptions section from appsettings seamlessly. Neat, yeah?

Here is the response I get from the weather/options endpoint.

alt text

Data Annotations & Validations

If there comes a requirement, wherein, certain properties from the appsettings have to be validated before usage, how do we do it? The Options Pattern provides a nice way to validate these configurations too, using Data Annotations.

Let’s assume that we have to apply the following validation rules on our properties.

  • City SHOULD NOT be empty.
  • State SHOULD NOT be empty.
  • Temperature SHOULD be between 0 and 100.

To achieve this, open up the WeatherOptions class and add these basic Data Annotations Attribute over the required properties.

public class WeatherOptions
{
[Required(AllowEmptyStrings = false)]
public string? City { get; set; }
[Required(AllowEmptyStrings = false)]
public string? State { get; set; }
[Range(0, 100)]
public int? Temperature { get; set; } = null;
public string? Summary { get; set; }
}

Once done, we should let know our Options Service registration to enable Data Annotation Validations. Simply open up Program.cs and add the ValidateDataAnnotations extension to our previously added Options registration.

builder.Services.AddOptions<WeatherOptions>().BindConfiguration(nameof(WeatherOptions))
.ValidateDataAnnotations();

With that done, I changed the temperature value in appsettings.json to a random negative value. Now when you run the application and navigate to the endpoint, you will be able to see the following exception.

alt text

Note that this doesn’t break the application, but just throws an error and makes the related endpoint/controller unusable. To fix this, you will have to provide valid values in your appsettings.json file.

Startup Validation

There are several use cases where you don’t want your application to even boot up if there are validation issues with the configurations. This is a much better way to fail the entire application if there are validation-related issues, rather than failing during the runtime of the application.

I am using this approach in my new FullStackHero .NET Microservices Boilerplate (revamp branch) – https://github.com/fullstackhero/dotnet-microservices-boilerplate/tree/revamp (Announcement soon).

You will just need to Add the ValidateOnStart extension to the previously added options service registration.

builder.Services.AddOptions<WeatherOptions>().BindConfiguration(nameof(WeatherOptions))
.ValidateDataAnnotations()
.ValidateOnStart();

With this change done, If you try to run the application, this would happen.

alt text

The application would fail to start since the provided configurations in the appsettings file are not valid.

Custom Validation

You can also add your own custom validation logic against the WeatherOptions class. For example, if you expect the value of the City to be always “Kerala”, you can do the following.

builder.Services.AddOptions<WeatherOptions>().BindConfiguration(nameof(WeatherOptions))
.Validate(options =>
{
if (options.State != "Kerala") return false;
return true;
});

Reloading Configurations – Options Interfaces

There can be scenarios or requirements where you would always want the latest value from appsettings.json. For instance, if the DevOps person changes the value of Temperature in appsettings / WeatherOptions, you want the latest value to be reflected from your API response, you will not be able to do it with the IOptions interface. Unfortunately, the IOptions interface loads the configuration values only once, during the application startup. For this purpose, we have the following 2 interfaces.

  • IOptionsSnapshot – this is a scoped service that gives a snapshot of options at the time the constructor is invoked.
  • IOptionsMonitor – this is a singleton service that gets the current value at any time.
public class WeatherController : ControllerBase
{
private readonly WeatherOptions _options;
private readonly WeatherOptions _optionsSnapshot;
private readonly WeatherOptions _optionsMonitor;
public WeatherController(IOptions<WeatherOptions> options, IOptionsSnapshot<WeatherOptions> optionsSnapshot, IOptionsMonitor<WeatherOptions> optionsMonitor)
{
_options = options.Value;
_optionsSnapshot = optionsSnapshot.Value;
_optionsMonitor = optionsMonitor.CurrentValue;
}
[HttpGet("options")]
public IActionResult GetFromOptionsPattern()
{
var response = new
{
options = new { _options.City, _options.State, _options.Temperature, _options.Summary },
optionsSnapshot = new { _optionsSnapshot.City, _optionsSnapshot.State, _optionsSnapshot.Temperature, _optionsSnapshot.Summary },
optionsMonitor = new { _optionsMonitor.City, _optionsMonitor.State, _optionsMonitor.Temperature, _optionsMonitor.Summary }
};
return Ok(response);
}
}

In the above code, I have added 2 more instances of the WeatherOptions to demonstrate the usage of the 2 new interfaces related to the Options, we mentioned earlier. I have also modified the response. Now it would print back the data from each of the 3 interfaces so that we get a good idea.

Run the application, and invoke the /weather/options endpoint.

alt text

Now, without stopping the application, I changed the value of temperature in the appsettings.json file from 20 to 30, and I tried to reload the endpoint. You can see that the value returned from IOptions has not changed yet, because as we discussed earlier, IOptions would read the configurations only one time.

But the values of IOptionsSnapshot and IOptionsMonitor have changed to the newly updated values.

alt text

IOptionsMonitor vs. IOptionsSnapshot?

The major difference is the lifetime of these instances. IOptionsMonitor is registered as Singleton, whereas the IOptionsSnapshot is registered as Scoped.

When to use IOptions, IOptionsMonitor, and IOptionsSnapshot?

  • Prefer to use IOptions, when you are not expecting your configuration values to change.
  • Use IOptionsSnapshot when you expect your values to change, but want them to be uniform for the entire request cycle.
  • Use IOptionsMonitor when you need real-time options data.

How to Load Configurations in Program.cs?

This has been a common requirement for me while developing my boilerplate applications. This is how to access strongly-typed configurations in Program.cs

var weatherOptions = builder.Configuration.GetSection(nameof(WeatherOptions)).Get<WeatherOptions>();

That’s a wrap for this article.

Thank you for visiting. If you like my content and code, support me by buying a couple of coffees so that I can find enough time to research & write new articles. Cheers!

Summary

In this article, we learned about the Options Pattern in ASP.NET Core by building a simple Web API that can load configurations from the appsettings.json file for weather-related data. We also looked into Data Annotation based Validations for your Configurations, along with Validation at Application Startup. It’s also possible to implement Custom Validation Logic with the Options Pattern in ASP.NET Core. Finally, we looked into Configuration reloading and various other Options interface like IOptionsSnapshot & IOptionsMonitor and how they differ from each other, and when to use which interface.

Make sure to share this article with your colleagues if it helped you! Helps me get more eyes on my blog as well. Thanks!

Stay Tuned. You can follow this newsletter to get notifications when I publish new articles – https://newsletter.codewithmukesh.com. Do share this article with your colleagues and dev circles if you found this interesting. Thanks!

Source Code ✌️
Grab the source code of the entire implementation by clicking here. Do Follow me on GitHub .
Support ❤️
If you have enjoyed my content and code, do support me by buying a couple of coffees. This will enable me to dedicate more time to research and create new content. Cheers!
Share this Article
Share this article with your network to help others!
What's your Feedback?
Do let me know your thoughts around this article.

Boost your .NET Skills

I am starting a .NET 8 Zero to Hero Series soon! Join the waitlist.

Join Now

No spam ever, we are care about the protection of your data. Read our Privacy Policy