Skip to content

Strongly Typed Configurations with Options Pattern

strongly typed configurations with options pattern

In this post, we’ll see how to read the configuration file using the options pattern. The options pattern is recommended to read the app configuration in ASP.NET Core.

Before we see how to make configurations strongly typed the options pattern, we’ll see how to read the configuration settings using the IConfiguration interface.

Reading Configuration settings with IConfiguration in ASP.NET Core

Let’s say we have the following configuration in our appsettings.json file

{
  "Pattern": {
    "Name": "Options Pattern",
    "Version":  1
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

And here’s how you read it

public class PrintOptionsController : ControllerBase
{
    private IConfiguration _configuration;

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

    [HttpGet]
    public List<string> GetOptions()
    {
        var options = new List<string>();

        options.Add(_configuration["Pattern:Name"]);
        options.Add(_configuration["Pattern:Version"]);
        options.Add(_configuration["Logging:LogLevel:Default"]);

        return options;
    }
}

To read nested options, we’ve to use a colon (:) as a separator between the nested properties. Ex: Pattern:Name, Pattern:Version

If we run this, we should see this as the result

["Options Pattern","1","Information"]

This is working fine.

But, there’s a problem here. Every configuration value is a string no matter how you declare it in the appsettings.json file.

And we’ve to hard code or store the configuration keys somewhere in the constants file so that we don’t have to hardcode it every time we use it.

This is not great because we have to manually parse the configuration values to the appropriate type every time.

So, the preferred way to read configuration is to use strongly typed classes with Options pattern.

What is the Options pattern?

The options pattern uses classes to provide strongly typed access to groups of related settings.


From Microsoft

The Options pattern adheres to the following software principles:

  1. Encapsulation or Interface Segregation Principle (ISP): Classes that depend on configuration settings rely only on the configuration settings that they use.
  2. Separation of Concerns: As each class is tied to different settings in the configuration file they aren’t dependent or coupled.

Rules for Options class to work

Our options class should be

  • Non-abstract and should have a public parameterless constructor
  • All public read-write properties of the type are bound

Reading appsettings.json with Options pattern

The properties in our Options class should correspond to what we have in our appsettings.json file. We had the following JSON in our appsettings.json file which we wanted to be strongly typed.

"Pattern": {
    "Name": "Options Pattern",
    "Version":  1
  }

So, our PatternOptions class should have two properties. Name and Version.

public class PatternOptions
{
    public const string SectionName = "Pattern";

    public string Name { get; set; }
    public int Version { get; set; }
}

We had the constant SectionName, which is not bound to the configurations. This is just to avoid hardcoding while configuring options pattern in the startup class.

Now, let’s configure our PatternOptions in the startup class.

In our ConfigureServices method in the startup class, add the following line

services.Configure<PatternOptions>(Configuration.GetSection(PatternOptions.SectionName));

That’s it! Our PatternOptions class is now good to go. We can inject our class anywhere and read the configurations.

Injecting our PatternOptions class with IOptions interface

Instead of reading the configuration from IConfiguration interface we will now read from IOptions<T>. The T is our PatternOptions.

So, let’s remove the IConfiguration injection from our PrintOptionsController and inject IOptions with PatternOptions as a type.

public class PrintOptionsController : ControllerBase
{
    private PatternOptions _patternOptions;

    public PrintOptionsController(IOptions<PatternOptions> options)
    {
        _patternOptions = options?.Value;
    }

    [HttpGet("withOptionsPattern")]
    public PatternOptions GetOptionsWithIOptions()
    {
        return _patternOptions;
    }
}

Within the constructor, we have assigned options?.Value to _patternOptions variable. Or we could only get the .Value only in the action method we need.

If we run the app, we should get our options from the configuration file.

{"name":"Options Pattern","version":5}

What are Named options?

Named options are used when we have multiple sections that bind to the same properties.

Let’s say we have the following in our appsettings.json file

"Seasons": {
    "Winter": {
      "Month":  "December", 
      "Temperature": 25
    },
    "Summer": {
      "Month":  "April", 
      "Temperature": 45
    },
    "Monsoon": {
      "Month": "July",
      "Temperature": 35
    } 
  }

Now, if we’ve to make options pattern out of this, we’d have to create 3 different classes (1 for winter, 1 for summer, and the other for monsoon) and bind these in the section configuration in the startup class.

But, with Named options, we can have the following class and use it for all seasons.

public class SeasonsOptions
{
    public const string Winter = "Winter";
    public const string Summer = "Summer";
    public const string Monsoon = "Monsoon";

    public string Month { get; set; }
    public int Temperature { get; set; }
}

Notice we have the common properties for all 3 seasons (Month and Temperature) and we have the constant variables for season names (these are not bound to the configuration as said earlier we’ll use them for configuration).

And in the startup, we can register the sections individually using the same SeasonsOptions class.

services.Configure<SeasonsOptions>(SeasonsOptions.Winter, Configuration.GetSection("Seasons:Winter"));
services.Configure<SeasonsOptions>(SeasonsOptions.Summer, Configuration.GetSection("Seasons:Summer"));
services.Configure<SeasonsOptions>(SeasonsOptions.Monsoon, Configuration.GetSection("Seasons:Monsoon"));

Once we are done with the setup we can modify our print options controller to be something like this

public class PrintOptionsController : ControllerBase
{
    private SeasonsOptions _winterSeasonOptions;
    private SeasonsOptions _summerSeasonOptions;
    private SeasonsOptions _monsoonSeasonOptions;

    public PrintOptionsController(IOptionsSnapshot<SeasonsOptions> namedWinterOptions)
    {

        _winterSeasonOptions = namedWinterOptions.Get(SeasonsOptions.Winter);
        _summerSeasonOptions = namedWinterOptions.Get(SeasonsOptions.Summer);
        _monsoonSeasonOptions = namedWinterOptions.Get(SeasonsOptions.Monsoon);
    }

    [HttpGet("getSeasonOptions")]
    public List<SeasonsOptions> GetAllSeasons()
    {
        return new List<SeasonsOptions>
        {
            _winterSeasonOptions,
            _summerSeasonOptions,
            _monsoonSeasonOptions
        };
    }
}

Notice we are using IOptionsSnapshot instead of IOptions here. This is because the IOptions interface does not support Named options.

IOptionsSnapshot interface is generally used when options should be recomputed on every request. Yes, there’s a performance penalty with this as it is a scoped service and needs to be recomputed per request. So, use it with caution.

Now, if we run the app we should see all the seasons and their temperatures.

[{"month":"December","temperature":25},{"month":"April","temperature":45},{"month":"July","temperature":35}]

That’s it! Thanks for reading.

Please comment below and let me know your thoughts on the options pattern in ASP.NET Core.

References


Recent Posts on coderethinked.com:

1 thought on “Strongly Typed Configurations with Options Pattern”

  1. Pingback: The Morning Brew - Chris Alcock » The Morning Brew #3532

Leave a Reply

Your email address will not be published.