DEV Community

Cover image for OPTIONS PATTERN IN .NET 6
Maurizio8788
Maurizio8788

Posted on

OPTIONS PATTERN IN .NET 6

Hello everyone, in a previous article, we saw how to retrieve the configurations of our applications through the IConfiguration interface. Now, we will move on to using a method that, in my opinion, is certainly cleaner and more responsive: the Options Pattern.

Table of Contents:

The Options Pattern

The Options Pattern provides us with strongly typed access to our configurations, thanks to the use of classes, and centralized configuration through the use of configuration in the Program.cs file. Configuration retrieval is achieved through the use of the three main interfaces that this paradigm offers, namely:

  • IOptions
  • IOptionsSnapshot
  • IOptionsMonitor

Let's try to create a simple basic configuration using the Options Pattern. First, let's add a fictitious configuration to our appsettings.json file for educational purposes:


"OptionsConfigurationBase": {
  "Test1": "It's a test configuration"
}

Enter fullscreen mode Exit fullscreen mode

Next, we will create our class with properties that match the ones in the previously configured appsettings section:


public class OptionsConfigurationBase
{
    public const string ConfigurationName = nameof(OptionsConfigurationBase);
    public string Test1 { get; set; }
}

Enter fullscreen mode Exit fullscreen mode

Once done, let's configure our class in the Program.cs file using the extension method .Configure


builder.Services.Configure<OptionsConfigurationBase>(
    builder.Configuration
        .GetSection(OptionsConfigurationBase.ConfigurationName)
);

Enter fullscreen mode Exit fullscreen mode

so that it can be subsequently injected through the Dependency Injection of .NET Core.


[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    //.... other variable

    private readonly ILogger<WeatherForecastController> _logger;
    private readonly OptionsConfigurationBase _options;

    public WeatherForecastController(ILogger<WeatherForecastController> logger, IOptions<OptionsConfigurationBase> options)
    {
        _logger = logger;
        _options = options.Value ?? throw new ArgumentNullException(nameof(options));
    }

    // ...other methods

    [HttpGet]
    [Route("GetOptionsTest")]
    public string GetOptionsTest()
    {
        return _options.Test1;
    }
}


Enter fullscreen mode Exit fullscreen mode

Now, this is certainly a great system; the only issue is that this configuration does not allow for reading changes at runtime. Therefore, any modifications made after the application starts will not be considered. To achieve this type of behavior, we'll need to use the IOptionsSnapshot interface.

Configuration with IOptionsSnapshot

This configuration is very useful when options need to be recalculated for each request. Essentially, this is what the IOptionsSnapshot interface does: it is registered as a Scoped service and caches the configurations for each request.

There are no changes in registration compared to the IOptions interface, except for the fact that in the constructor of our controller, we change IOptions to IOptionsSnapshot.


public WeatherForecastController(ILogger<WeatherForecastController> logger, IOptionsSnapshot<OptionsConfigurationBase> options)
{
        _logger = logger;
        _options = options.Value ?? throw new ArgumentNullException(nameof(options));
}

Enter fullscreen mode Exit fullscreen mode

If, after the application is started, we change the value of the "Test1" parameter from:

"Test1": "It's a test configuration"
Enter fullscreen mode Exit fullscreen mode

to:

"Test1": "It's a test configuration updated"
Enter fullscreen mode Exit fullscreen mode

And if we make the request again after changing the "Test1" parameter to "It's a test configuration" we will see that the returned result will be "It's a test configuration updated".

This configuration is certainly useful, but it should be used with caution because it could potentially lead to performance issues since, as a Scoped service, it is recalculated for every request.

Configuration with IOptionsMonitor

Last but not least, the use of the IOptionsMonitor interface is certainly important. This service is configured as a Singleton service and will retrieve updated values of our configurations at any time. One key difference is found in the constructor when retrieving the injected service, instead of using the .Value property, we will use the .CurrentValue property:


public WeatherForecastController(ILogger<WeatherForecastController> logger, IOptionsMonitor<OptionsConfigurationBase> options)
{
        _logger = logger;
        _options = options.CurrentValue ?? throw new 
ArgumentNullException(nameof(options));
}

Enter fullscreen mode Exit fullscreen mode

Exactly as before, when making the initial call, we will receive the response with the value "It's a test configuration updated" However, if we update the property to "I'm a test configuration updated and retrieved with IOptionsMonitor" this value will be retrieved from our service.

Configuring Named Options

Named options are useful when we need multiple sections within a single configuration object.

Let's modify the appsettings.json file as follows:


"OptionsConfigurationBase": {
  "NamedOptions1": {
    "Test1": "I am the Named Option 1 configuration"
  },
  "NamedOptions2": {
    "Test1": "I am the Named Option 2 configuration"
  }
}

Enter fullscreen mode Exit fullscreen mode

let's make a small modification to our class:


public class OptionsConfigurationBase
{
    public const string NamedOptions1 = "NamedOptions1";
    public const string NamedOptions2 = "NamedOptions2";

    public string Test1 { get; set; }
}

Enter fullscreen mode Exit fullscreen mode

finally, let's modify the Program.cs to support this new configuration option:


builder.Services.Configure<OptionsConfigurationBase>(
    OptionsConfigurationBase.NamedOptions1,
    builder.Configuration.GetSection($"{nameof(OptionsConfigurationBase)}:{OptionsConfigurationBase.NamedOptions1}")
);

builder.Services.Configure<OptionsConfigurationBase>(
    OptionsConfigurationBase.NamedOptions2,
    builder.Configuration.GetSection($"{nameof(OptionsConfigurationBase)}:{OptionsConfigurationBase.NamedOptions2}")
);

Enter fullscreen mode Exit fullscreen mode

How do we retrieve configurations in the controller? By using the Get method associated with the IOptionsSnapshot interface, like this:


private readonly  OptionsConfigurationBase _namedOptions1;
private readonly  OptionsConfigurationBase _namedOptions1;

public WeatherForecastController(ILogger<WeatherForecastController> logger, IOptionsSnapshot<OptionsConfigurationBase> options)
{
        _logger = logger;
        _namedOptions1 = options.Value.Get(OptionsConfigurationBase.NamedOptions1)?? throw new ArgumentNullException(nameof(options));
        _namedOptions1 = options.Value.Get(OptionsConfigurationBase.NamedOptions2)?? throw new ArgumentNullException(nameof(options));
}

Enter fullscreen mode Exit fullscreen mode

Choosing Between Configurations

As we've seen, each of these interfaces has its own characteristics, and choosing between them is not always straightforward. However, we can do a quick review to help you choose the right interface based on your needs:

IOptions

  • It doesn't support runtime reloading of configuration values.
  • It's registered as a Singleton service in the .NET Core Dependency Injection container.
  • It can be injected into any service, whether registered as Singleton, Scoped, or Transient.
  • It doesn't support named options.

IOptionsSnapshot

  • Useful when configurations need to be reconfigured for each request.
  • Registered as Scoped, which means it cannot be injected into a Singleton service and might have performance issues.
  • Supports named options.

IOptionsMonitor

  • Registered as a Singleton service in the .NET Core Dependency Injection container.
  • Supports named options.
  • Dynamic changes reloaded for each request.

And with that, our overview of the Options Pattern in .Net Core comes to an end. I hope this article helps you choose the right interface and understand how this pattern makes configuration retrieval in our applications easy and dynamic.

You can find all the code used in the article (it's not much) in this repository:

If you enjoyed the article, please share it and give it a like to make it visible to others. I look forward to reading your comments where you share your experiences with this feature.

Happy Coding!

Top comments (6)

Collapse
 
michelsylvestre profile image
Michel Sylvestre

And for those who would see using Azure Key Vaults or Azure Function configuration a limitation for the options pattern, don't worry!

In Key Vaults, double dash (--) serves as hierarchical marker.
Ex. : OptionsConfigurationBase--Test1
is the same as
"OptionsConfigurationBase": { "Test1": "It's a test configuration" }

The same can be achieved in Function Apps with double underscore (__), and prievioulsy colons.

Collapse
 
maurizio8788 profile image
Maurizio8788 • Edited

Thank you very much for this insight @michelsylvestre !!

Collapse
 
hasanelsherbiny profile image
Hasan Elsherbiny

good article

Collapse
 
pbouillon profile image
Pierre Bouillon

Great article, insightful πŸ’‘

Collapse
 
yogini16 profile image
yogini16

Nice article.
Thank you for your efforts.

Collapse
 
Sloan, the sloth mascot
Comment deleted