Do you use IConfiguration, IOptions or IOptionsSnapshot?

Published: Monday 15 July 2024

When injecting configuration settings from appsettings.json into an ASP.NET Core app, there are a number of ways you can do it.

We are going to have a look at injecting IConfiguration, IOptions and IOptionsSnapshot to see which one we should use.

Adding configuration values to appsettings

To begin with, we are going to add product configuration values to appsettings.json.

C# coding challenges

C# coding challenges

Our online code editor allows you to compile the answer.

We'll include the currency, whether there is a discount applied and whether we are going to show reviews.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Product": {
    "Currency": "USD",
    "Discount": 7,
    "Reviews": true,
  }
}

Using IConfiguration

We can inject the IConfiguration instance into any class that supports dependency injection. There doesn't need to be any extra configuration settings added in Program.cs to use this.

Once the IConfiguration instance has been injected, we can call the GetValue method from the reference. The GetValue method requires a generic type, and we can add the type that we are expecting.

For example, if we want to call Discount from the Product configuration values, we would add decimal as the generic type.

Afterwards, we add the configuration key to get the value. If the key has parent configuration keys, they are added first and separated by a colon.

As all the product configuration values have a parent key of Product, we would add that first followed by a colon, followed by the configuration value that we wanted to store.

So for showing product reviews, this would be Product:Reviews.

// ConfigurationController.cs
public class ConfigurationController : ControllerBase
{
	private readonly IConfiguration _configuration;

	public ConfigurationController(
		IConfiguration configuration)
	{
		_configuration = configuration;
	}

	[HttpGet]
	public IActionResult GetProduct()
	{
		return Ok(new
		{
			Currency = _configuration.GetValue<string>("Product:Currency"),
			Discount = _configuration.GetValue<decimal>("Product:Discount"),
			Reviews = _configuration.GetValue<bool>("Product:Reviews"),
		});
	}
}

When we test this endpoint, it outputs the configuration values. And if we change the configuration values in appsettings.json and rerun the endpoint, it updates the values without restarting the application.

The problem with this method is that any extra configuration values added to Product would need to be updated in all places where it's being used.

Bind the configuration values to a class

Another option is that we can bind the configuration values to a class.

We'll create a ProductOptions class that will contain properties for our configuration values and the type that we are expecting.

// ProductOptions.cs
public class ProductOptions
{
	public string Currency { get; init; }

	public decimal Discount { get; init; }

	public bool Reviews { get; init; }
}

There are two ways we can bind the values from appsettings.json into this class.

We can either get them from IConfiguration and provide the class as the generic type. Or we can bind them to an existing instance of ProductOptions.

// ConfigurationController.cs
public class ConfigurationController : ControllerBase
{
    private readonly IConfiguration _configuration;

    public ConfigurationController(
        IConfiguration configuration)
    {
        _configuration = configuration;
    }

    ...

	[HttpGet("with-configuration-get")]
	public IActionResult GetProductWithConfigurationGet()
	{
		var productOptions = _configuration.GetSection("Product").Get<ProductOptions>();

		return Ok(productOptions);
	}

	[HttpGet("with-configuration-bind")]
	public IActionResult GetProductWithConfigurationBind()
	{
		var productOptions = new ProductOptions();
		_configuration.GetSection("Product").Bind(productOptions);

		return Ok(productOptions);
	}
}

You can inject IOptions if you are using dependency injection

If you are using dependency injection, you can add the options to the IoC container by providing the configuration key.

Here, we are adding the ProductOptions and binding them to the Product section from the configuration.

// Program.cs
builder.Services.AddOptions<ProductOptions>()
    .Bind(builder.Configuration.GetSection("Product"));

Then you can inject them by calling IOptions, adding the class with the configuration value properties as the generic type.

// ConfigurationController.cs
[Route("api/[controller]")]
[ApiController]
public class ConfigurationController : ControllerBase
{
	private readonly IConfiguration _configuration;
	private readonly ProductOptions _productOptionsValue;

	public ConfigurationController(
	   IConfiguration configuration,
	   IOptions<ProductOptions> productOptions
	{
	   _configuration = configuration;
	   _productOptionsValue = productOptions.Value;
	}

	...


	[HttpGet("with-ioptions")]
	public IActionResult GetProductWithIOptions()
	{
		return Ok(_productOptionsValue);
	}
}

However, when you try to change the configuration values when using IOptions, you have to restart the application before the configuration values change.

Is there another way we can do it?

Using IOptionsSnapshot will update configuration values

Instead of using IOptions, we can use IOptionsSnapshot. IOptionsSnapshot works in a similar way to IOptions, but it updates the configuration values without restarting the application.

The reason being is that IOptions is set up as a singleton lifetime scope in dependency injection, whereas IOptionsSnapshot is set up as scoped.

// ConfigurationController.cs
public class ConfigurationController : ControllerBase
{
	private readonly IConfiguration _configuration;
	private readonly ProductOptions _productOptionsValue;
	private readonly ProductOptions _productOptionsSnapshotValue;

	public ConfigurationController(
		IConfiguration configuration,
		IOptions<ProductOptions> productOptions,
		IOptionsSnapshot<ProductOptions> productOptionsSnapshot)
	{
		_configuration = configuration;
		_productOptionsValue = productOptions.Value;
		_productOptionsSnapshotValue = productOptionsSnapshot.Value;
	}

	...

	[HttpGet("with-ioptions-snapshot")]
	public IActionResult GetProductWithIOptionsSnapshot()
	{
		return Ok(_productOptionsSnapshotValue);
	}
}

Adding a nested configuration class

IOptions and IOptionsSnapshot has support for nested configuration values.

To demonstrate this, we've added a Category section to appsettings.json.

"Product": {
	"Currency": "USD",
	"Discount": 19,
	"Reviews": true,
	"Images": true,
	"Category": {
	  "IncludeImage": true
	}
}

Then we need to create a class for the category options and include any properties where we are expecting configuration values.

// CategoryOptions.cs
public class CategoryOptions
{
	public bool IncludeImage { get; init; }
}

This class can then be added as a property type in ProductOptions.

// ProductOptions.cs
public class ProductOptions
{
	public string Currency { get; init; }

	public decimal Discount { get; init; }

	public bool Reviews { get; init; }

	// Will bind the configuration from Product:Category in appsettings.json
	public CategoryOptions Category { get; init; }
}

You can watch our video where we go through IConfiguration, IOptions and IOptionsSnapshot and test each one to see the expected output.

It's best to use IOptionsSnapshot

In conclusion, it's best to use IOptionsSnapshot if you're using dependency injection.

It has the benefits of binding configuration values to a class, changing configuration values without restarting the application and being able to use nested configuration values.

Although IConfiguration also allows you to change configuration values without restarting the application, there is a lot more code and potentially repeating code to reach the same result.