Options Pattern in .NET Core with Examples

Introduction

In.NET, application settings can be configured using the Options pattern. It lets you define options classes with robust typing that your application can readily use. This implies you can take a more type-safe and flexible approach to establishing values instead of relying on magic strings or hard-coded values.

The configuration mainly comes from the application json file, environment variables, and many other options.

Here are the application.json settings that we use in our application.

"JwtConfigSettings": {
  "Issuer": "Jaimin",
  "Audience": "Jaimin",
  "ValidateExpiration": true,
  "SigninKey": "Jaimin sign in key"
}

If we don’t use the options pattern in our application, then we need to inject the IConfiguration interface and get the value from the configuration.

Here is the code to get the configuration value.

[Route("api/[controller]")]
[ApiController]
public class ConfigController : ControllerBase
{
    private readonly IConfiguration _configuration;

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

    [HttpGet]
    public IActionResult Get()
    {
        var issuer = _configuration["JwtConfigSettings:Issuer"];
        var validateExpiration = _configuration["JwtConfigSettings:ValidateExpiration"];
        var signinKey = _configuration["JwtConfigSettings:SignInKey"];

        return Ok(new { issuer, validateExpiration, signinKey });
    }
}

Here is the output value for the API response.

Output value for the API Response

We add type-safety and greater flexibility for accessing and utilizing configuration with the options pattern.

To use the pattern, we must first construct a class that represents our alternatives.

    public class JwtConfigSettings
    {
        public const string Key = "JwtConfigSettings";

        public string Issuer { get; set; }

        public string Audience { get; set; }

        public bool ValidateExpiration { get; set; }

        public string SignInKey { get; set; }
    }

A few tips to note

  • The names of the properties must correspond to those specified in your configuration method.
  • The option/setting section name in the configuration defines the constant Key. Some people would like not to have it, naming the section JwtConfigSettings, for example, using the nameof(JwtConfigSettings) to bind the section. I think naming a property as a key property is cleaner.

The Application portion of the configuration needs to be bound to our configuration object at this point. We may use the IServiceCollection to accomplish it in the Program.cs file during the services definition:

builder.Services
  .AddOptions<JwtConfigSettings>()
  .Bind(builder.Configuration.GetSection(JwtConfigSettings.Key));

We could also do another trick to bind the configuration.

builder.Services.Configure<JwtConfigSettings>(builder.Configuration.GetSection(JwtConfigSettings.Key));

or

var jwtConfigSettings = new JwtConfigSettings();

builder.Configuration.GetSection(JwtConfigSettings.Key)
    .Bind(jwtConfigSettings);

When you connect a configuration in your service, .NET provides you with this configuration access mechanism. Three methods exist for gaining access to your option:

  1. IOptions<T>
  2. IOptionsSnapshot<T>
  3. IOptionsMonitor<T>

IOptions

At the beginning of the program, this interface—which is registered as a Singleton—will read the settings. This implies that the settings associated with this interface will only be modified upon program restart.

It is intended for use in situations when the choices data is read-only and rarely changes.

public class ConfigController
{
    private readonly JwtConfigSettings _jwtConfigSettings;

  
    public ConfigController(IOptions<JwtConfigSettings> jwtConfigSettings)
    {
        _jwtConfigSettings = jwtConfigSettings.Value;
    }
}

IOptionsSnapshot

You cannot use this interface inside singleton services since it is registered as Scoped and reads the settings on each request.

When you need to know an option's most recent value throughout a request, it can be helpful.

public class ConfigController
{
    private readonly JwtConfigSettings _jwtConfigSettings;
 
    public ConfigController(IOptionsSnapshot<JwtConfigSettings> jwtConfigSettings)
    {
        _jwtConfigSettings = jwtConfigSettings.Value;
    }
}

IOptionMonitor

You can retrieve the most recent value of an option using this interface. You may control option notifications for an option as well.

It can be injected into any service because it is registered as a Singleton.

There are two approaches you can apply it to:

public class ConfigController
{
    private readonly IOptionsMonitor<JwtConfigSettings> _jwtConfigSettings;
  
    public ConfigController(IOptionsMonitor<JwtConfigSettings> jwtConfigSettings)
    {
        _jwtConfigSettings = jwtConfigSettings;
    }

    private void PrintConfig()
    {
        Console.WriteLine(jwtConfigSettings.CurrentValue.ValidateExpiration);
    }
}

or

public class ConfigController
{
    private readonly IOptionsMonitor<JwtConfigSettings> _jwtConfigSettings;

    private readonly IDisposable _changeListener;
   
    public ConfigController(IOptionsMonitor<JwtConfigSettings> jwtConfigSettings)
    {
        _jwtConfigSettings = jwtConfigSettings;

        _changeListener = jwtConfigSettings.OnChange(options =>
        {
          var jwtOptions = options;
        });
    }

    private void PrintConfig()
    {
        Console.WriteLine(jwtConfigSettings.ValidateExpiration);
    }

    ~ConfigController()
    {
      _changeListener?.Dispose();
    }
}

Keep in mind that the registered listener receives an IDisposble from the OnChange function. To correctly clean up the memory utilized and tidy up the listener, we use the destructor method ~ConfigController().

API Config

In the above example, I have used the IOptions<T> pattern.

We learned the new technique and evolved together.

Happy coding. :)