.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
- .NET Core Configuration Files - Developing Dane, December, 2016
- Options pattern in ASP.NET Core 2.2, Microsoft documentation
- Exploration source code - stupid-configs branch of the StupidTodo exploratory application's source code repository