Diving into Azure

Configuration in ASP.NET Core

March 25, 2019 | 7 Minute Read

Configuration in your app can sometimes be a pain. Some values are set in code, some are extracted from the environment in which you're running and some are secret and can't be defined anywhere near a repository. ASP.NET Core helps you manage all these settings in a very smooth way

ASP.NET Core has a pluggable configuration system where configuration settings can be imported from a number of different sources. These sources are then merged together into one hierarchical configuration object.

When you create a new ASP.Net Core Web App in Visual Studio, it’ll automatically create a configuration file for you named appsettings.json. By default it only contains logging and CORS settings but we’re free to add any type of hierarchical configuratin that we need.

In this example I’ve added two extra sections (FirstServiceSettings and SecondServiceSettings) to the file that I’ll use as specific option groups for my web app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "FirstServiceSettings": {
    "AnotherSetting": 14
  },
  "SecondServiceSettings": {
    "MinRetries": 2,
    "MaxRetries": 40,
    "CostPerRetry": 0.02
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Options pattern

Settings are grouped together as classes in what’s called the options pattern. By separating options you can decouple different parts of your app so it only knows the settings it needs to know, in accordance with the theory of separation of concerns.

It’s not mandatory to create options classes for your options but it’s something you should consider. Here’s an example of a few settings classes that we’ll use in the examples through out this post.

1
2
3
4
5
6
7
8
9
10
11
12
public class FirstServiceSettings
{
  public string StringSetting { get; set; }
  public int AnotherSetting { get; set; }
}

public class SecondServiceSettings
{
  public int MaxRetries { get; set; }
  public int MinRetries { get; set; }
  public decimal CostPerRetry { get; set; }
}

Since the JSON file example previously shown only is one of many possible configuration providers, it’s not needed to include all the settings you need for your option classes. The JSON file might specify some basic values while a secrets manager brings in passwords.

Map classes to configuration

In the merged configuration settings, each option group that match a class is seen as a sub-option in the main hierarchy. In Startup.cs we therefore need to map a path to each of the option classes.

1
2
3
4
5
6
7
8
9
10
11
12
13
public Startup(IConfiguration configuration)
{
  Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
  services.Configure<FirstServiceSettings>(Configuration.GetSection("FirstServiceSettings"));
  services.Configure<SecondServiceSettings>(Configuration.GetSection("SecondServiceSettings"));
  ...
}

The option classes are now registered and we can use them in the code by injecting them using dependency injection.

Make options read-only

One problem with these option classes is that they are not read-only and can be changed locally where used (but still not globally). It is not possible for the configuration binder to set properties without a setter, but you can define the setter as private and still set these properties.

In this option class example below, both the public property with a private setter as well as the private property will be set with the values from the configuration settings.

1
2
3
4
5
public class ProtectedSettings
{
  public string HiddenSetting { get; private set; }
  private string VeryHiddenSetting { get; set; }
}

The default behaviour is to not change private properties, but by specifying the BindNonPublicProperties option you can override this behaviour.

1
2
3
4
5
6
7
8
public void ConfigureServices(IServiceCollection services)
{
  services.Configure<ProtectedSettings>(Configuration.GetSection("ProtectedSettings"), options =>
  {
      options.BindNonPublicProperties = true;
  });
  ...
}

The option values are now protected later on.

Configure options using a delegate

It’s also possible to set the configuration options in code by using a delegate.

1
2
3
4
services.Configure<SecondServiceSettings>(myOptions =>
{
    myOptions.MaxRetries = 55;
});

If you want to use both delegate and the configuration object when you set the values from the settings class you have to write a little extra code. The example below shows two ways of accessing specific values in the configuration object.

1
2
3
4
5
6
7
8
9
10
11
12
services.Configure<SecondServiceSettings>(delegateSettings => 
{
  delegateSettings.MaxRetries = 55;

  // Deep link to values one by one
  delegateSettings.MinRetries = Configuration.GetValue<int>("SecondServiceSettings:MinRetries");

  // Get the whole settings object and set them one by one
  var settings = new SecondServiceSettings();
  Configuration.Bind("SecondServiceSettings", settings);
  delegateSettings.CostPerRetry = settings.CostPerRetry;
});

You can of course also use a model mapper like AutoMapper, but we’re here quite early in the startup process so you might not want to do that here.

The delegate is not executed until the value object is requested in the injected option.

1
2
3
4
5
6
7
8
9
10
public class HomeController : Controller
{
  private readonly SecondServiceSettings _secondSettings;

  public HomeController(IOptionsSnapshot<SecondServiceSettings> secondSettings)
  {
      _secondSettings = secondSettings.Value;
  }
  ...
}

In the example above, line 7 would trigger the delegate in the configuration.

More information

More detailed information about other useful scenarios can be found on the Microsoft Docs site under the topics options and configuration.

Share via