.NET Core configuration, a deeper dive

In my previous post .NET Core Configuration Files I discussed a simple and resilient method of accessing configuration data with .NET Core. In this post I'll explore .NET Core configuration options in greater depth.

For this discussion let's start with the following appsettings.json configuration file.

{
  "favoriteBeerStyle": "IPA",
  "beerOfTheNow": {
    "name": "Blacksheep CDA",
    "abv": "6.7",
    "fnord": "nothing to see here",
    "brewery": {
      "name": "Lucky Labrador Brewing Company",
      "location": "Portland, OR",
      "rating": "9.5"
    }
  }
}



I previously discussed several ways to get information from such configuration. For example, consider the following where configuration is and instance of Microsoft.Extensions.Configuration.IConfiguration.

string fav1 = configuration.GetSection("favoriteBeerStyle")?.Value;  
string fav2 = configuration.GetSection("FAVORITEBEERSTYLE")?.Value;  
string fav3 = configuration["favoriteBeerStyle"];  



Each of the three fav variables will have the value "IPA". In my opinion the syntax used with fav3 is the simplest for the vast majority of cases but opinions vary.

I further discussed how .NET Core's configuration system makes reading complex data from config files simple and extremely resilient. Take the following code.

string current = configuration["beerOfTheNow:name"];  
string currentBrewery = configuration["beerOfTheNow:brewery:name"];  
string badName = configuration["beerOfTheNow:not_a_name"];  



After execution current will have the value "Blacksheep CDA", currentBrewery will have the value "Lucky Labrador Brewing Company" and badName will be null.

Sometimes reading configurations in this way can be useful and even appropriate. However, for most applications reading values directly from an instance of IConfiguration is not ideal. A better solution is to provide required configuration data to object instances using dependency injection (DI).

As an example take the definitions of the following simple classes.

public class TheBeer  
{
    public string Name { get; set; }
    public Brewery Brewery { get; set; }
    public double Abv { get; set; }
    public double Ibu { get; set; }
}

public class Brewery  
{
    public string Name { get; set; }
    public string Location { get; set; }
    public double Rating { get; set; }
}



Note that TheBeer class has many properties which match those found in the applicationsettings.json config file's "beerOfTheNow" section. Further, note that the two are not perfectly aligned. Finally, note that one of these properties is of type Brewery and that the Brewery class has properties which match the "brewery" sub-section of the "beerOfTheNow" section.

Now, take an ASP.NET Controller with a constructor dependency on TheBeer.

public class TodoController : Controller  
{
    private readonly TheBeer Beer;

    public TodoController(TheBeer beer)
    {
        Beer = beer;
    }



Let us further assume that we'd like the beer argument of to be an instance of a TheBeer hydrated with appropriate values from the "beerOfTheNow" section of the config file. A common use case would be to use DI to provide all instances of TodoController with an instance of TheBeer. A typical example adding this dependency to the DI pipeline might look like this.

public void ConfigureServices(IServiceCollection services)  
{
    var beer = new TheBeer(); // then hydrate with configuration
    services.AddSingleton(beer)
            .AddMvc();
}



There are a number of ways to hydrate the beer variable with data from the "beerOfTheNow" configuration section. It should be fairly obviously that loading properties one at a time over the TheBeer/Brewery graph object is less than ideal. Better options might be the IConfiguration extensions methods Bind or (the more elegant?) Get.

// NuGet: Microsoft.Extensions.Configuration.Binder
using Microsoft.Extensions.Configuration;

var beer1 = new TheBeer();  
configuration.GetSection("beerOfTheNow").Bind(beer);

var beer2 = configuration.GetSection("beerOfTheNow").Get<TheBeer>();  



In both cases the beer variables will be hydrated as expected based on available configuration. Properties with no matching configuration will have values set to property type defaults and configuration values that have no matching property will be ignored. Again, very resilient.

The last option I'll discuss utilizes features from the Options pattern in ASP.NET Core. This pattern covers quite a lot but I find the IServiceCollection extension method Configure particularly useful. The Configure method can be used instead of the AddSingleton to resolve configuration dependencies. The difference is that while AddSingleton resolves dependencies of type T, Configure resolves dependencies of IOptions<T>. For example, the constructor of the previously shown TodoController would need to be updated to accept an argument of type IOptions<TheBeer> instead of TheBeer.

public TodoController(IOptions<TheBeer> options)  
{
    Beer = options?.Value;
}



The IOptions interface is extremely simple and a default implementation is injected for you by the Configure method.

namespace Microsoft.Extensions.Options  
{
    public interface IOptions<out TOptions> where TOptions : class, new()
    {
        TOptions Value { get; }
    }
}



Both the AddSingleton and Configure extensions methods can be used to satisfy dependencies on configuration data.

// Resolve dependencies on TheBeer
services.AddSingleton(Configuration.GetSection("beerOfTheNow")?.Get<TheBeer>());

// Resolve dependencies on IOptions<TheBeer>
// NuGet: Microsoft.Extensions.Options.ConfigurationExtensions
using Microsoft.Extensions.DependencyInjection;

services.Configure<TheBeer>(Configuration.GetSection("beerOfTheNow"));  



I currently favor the Configure method to resolve configuration dependencies for the majority of .NET Core applications. However, there are MANY applications with complex configuration needs far outside the scope of the simple examples I've used here. For those, I'd say that the ASP.NET Core configuration system has an abundance of options for the developers who build and maintain such software. I'll leave those explorations to the reader...which likely means me.

References