Options Pattern In .NET 6.0

Introduction

Reading from configuration file is one of the most common requirements when it comes to software development. With options pattern in .NET this can be achieved in an elegant manner using the options interfaces. The various options interfaces exposed in .NET enables mapping configuration settings to strongly typed classes that can be accessed across various service lifetimes. In this article, we will explore the different ways to implement options pattern across transient, scoped, and singleton service lifetimes.

Setup

Create an ASP.NET WebAPI 6.0 app and add the following configuration setting in the appsettings.json file

  "Units": {
    "Temp": "Celsius",
    "Distance": "Miles"
  }

Create a UnitOptions class corresponding to the setting created in the previous step

public class UnitOptions
{
	public string Temp { get; set; } = String.Empty;
	public string Distance { get; set; } = String.Empty;
}

Bind the UnitOptions class to the corresponding section in appsettings.json by registering configuration instance in Program.cs (If you are using previous version of .NET, add the following line in Startup.cs)

builder.Services.Configure<UnitOptions>(builder.Configuration.GetSection("Units"));

IOptions

IOptions is singleton and hence can be used to read configuration data within any service lifetime. Being singleton, it cannot read changes to the configuration data after the app has started.

To demonstrate this let’s create a transient service to read the unit options from the configuration file using IOptions interface as follows:-

public interface ITransientService
{
	UnitOptions GetUnits();
}

public class TransientService : ITransientService
{
    private readonly UnitOptions _unitOptions;

    public TransientService(IOptions<UnitOptions> unitOptions)
    {
        _unitOptions = unitOptions.Value;
    }
    public UnitOptions GetUnits()
    {
        return _unitOptions;
    }
}

Add the transient service to DI container in Program.cs

builder.Services.AddTransient<ITransientService, TransientService>();

Hook the service to the controller

[Route("api/[controller]")]
[ApiController]
public class OptionsDemoController : ControllerBase
{
    private readonly ITransientService _transientService;

    public OptionsDemoController(TransientService transientService)
    {
        _transientService = transientService;
    }

    [HttpGet]
    [Route("/units/transient")]
    public IActionResult GetUnitsTransient() => Ok(_transientService.GetUnits());
}

Run the app and hit the controller action. You should be able to see the values being fetched from the configuration file.

IOptions

While the app is still running, change the value of distance unit from ‘Miles’ to ‘Kilometres’ in the appsettings.json file and hit the same API controller action again. The response does not change. This is because IOptions cannot read changes to the config data while the app is still running.

Revert changes to the appsettings.json file before proceeding with next steps.

IOptionsSnapshot

IOptionsSnapshot is scoped and hence it can be used only with transient and scoped service lifetimes. Being scoped, it can recompute config data for each request.

Create a scoped (or transient) service with an injected IOptionsSnapshot instance as follows:-

public interface IScopedService
{
	UnitOptions GetUnits();
}

public class ScopedService : IScopedService
{
    private readonly UnitOptions _unitOptions;

    public ScopedService(IOptionsSnapshot<UnitOptions> unitOptions)
    {
        _unitOptions = unitOptions.Value;
    }

    public UnitOptions GetUnits()
    {
        return _unitOptions;
    }
}

Add the scoped service to DI container in Program.cs

builder.Services.AddScoped<IScopedService, ScopedService>();

Hook the service to the controller

[Route("api/[controller]")]
[ApiController]
public class OptionsDemoController : ControllerBase
{
    private readonly IScopedService _scopedService;
    private readonly ITransientService _transientService;

    public OptionsDemoController(ITransientService transientService, IScopedService scopedService)
    {
        _transientService = transientService;
        _scopedService = scopedService;
    }

    [HttpGet]
    [Route("/units/scoped")]
    public IActionResult GetUnitsScoped() => Ok(_scopedService.GetUnits());

    [HttpGet]
    [Route("/units/transient")]
    public IActionResult GetUnitsTransient() => Ok(_transientService.GetUnits());
}

Run the app and hit the controller action. You should be able to see the values being fetched from the configuration file.

IOptionsSnapshot

While the app is still running, change the value of distance unit from ‘Miles’ to ‘Kilometres’ in the appsettings.json file and hit the same API controller action again. The response reflects the changes to the config data.

IOptionsSnapshot

If you try to add IOptionsSnapshot to any singleton service, you would encounter a runtime exception because of the service lifetime mismatch.

Revert changes to the appsettings.json file before proceeding with next steps.

IOptionsMonitor

IOptionsMonitor is singleton and hence can be used to read configuration data in any service lifetime. However, as opposed to IOptions, it can retrieve current config data at any time.

Create a singleton service with an injected IOptionsMonitor instance as follows:-

public interface ISingletonService
{
    UnitOptions GetUnits();
}

public class SingletonService : ISingletonService
{
    private readonly IOptionsMonitor<UnitOptions> _unitOptions;

    public SingletonService(IOptionsMonitor<UnitOptions> unitOptions)
    {
        _unitOptions = unitOptions;
    }
    public UnitOptions GetUnits()
    {
        return _unitOptions.CurrentValue;
    }
}

Add the service to DI container in Program.cs

builder.Services.AddSingleton<ISingletonService, SingletonService>();

Hook the service to controller

[Route("api/[controller]")]
[ApiController]
public class OptionsDemoController : ControllerBase
{
    private readonly ITransientService _transientService;
    private readonly IScopedService _scopedService;
    private readonly ISingletonService _singletonService;

    public OptionsDemoController(ITransientService transientService, IScopedService scopedService, ISingletonService singletonService)
    {
        _transientService = transientService;
        _scopedService = scopedService;
        _singletonService = singletonService;
    }

    [HttpGet]
    [Route("/units/transient")]
    public IActionResult GetUnitsTransient() => Ok(_transientService.GetUnits());


    [HttpGet]
    [Route("/units/scoped")]
    public IActionResult GetUnitsScoped() => Ok(_scopedService.GetUnits());

    [HttpGet]
    [Route("/units/singleton")]
    public IActionResult GetUnitsSingleton() => Ok(_singletonService.GetUnits());
}

Run the app and hit the controller action. You should be able to see the values being fetched from the configuration file.

IOptionsMonitor

While the app is still running, change the value of distance unit from ‘Miles’ to ‘Kilometres’ in the appsettings.json file and hit the same API controller action again. The response reflects the changes to the config data.

IOptionsMonitor

Summary

The options pattern provides us with various options to read the config data using strongly types classes. Depending upon service lifetime and recomputation requirements of the config data, one can use IOptions, IOptionsSnapshot, and IOptionsMonitor interfaces to read config data. Prefer using the options pattern over other methods to read config data.

References

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-6.0