Configuration in .NET Core part 1/3
May 08, 2021
In this three-part series, I will be exploring the new mechanism for configuring your application in .NET Core. What I personally really like about this one is, that it is very flexible. Most likely you will first come across it when dealing with ASP .NET Core applications where it is preconfigured for you as part of WebHost setup. Similarly, there is also some default configuration available for .NET Generic Host. While Generic Host can also be used for console apps, it might happen that you would want a more customized or more lightweight setup for your application. Either way, it pays off to know more about the configuration system.
As mentioned I like the flexibility it provides. Whether your configuration sits in an environment variable, JSON file, XML file, or brought in by carrier pigeons, it can attach itself to those sources or allows you to easily extend it to a new data source, while still providing the same interface for your application to consume. Regarding carrier pigeons, I will get back to that in a later post where I will describe how to implement a custom configuration provider.
As mentioned there are already many configuration sources that are provided by Microsoft as NuGet packages and are per convention named Microsoft.Extensions.Configuration.*. Configuration sources from other providers also tend to follow the naming convention of *.Extensions.Configuration.* for their NuGet packages.
So let’s start by adding an in-memory and environment variables configuration source. For this, we need to add the Microsoft.Extensions.Configuration.EnvironmentVariables Nuget package to our project. This automatically pulls in also the Microsoft.Extensions.Configuration package, where the base configuration implementation resides. As a side note, in my examples, I will be using the latest stable packages, which at the time of writing is version 5.0.0.
And here is the code that configures both settings sources:
var inMemorySettings = new Dictionary<string, string>
{
{"MySetting", "Setting from in memory"},
{"Subsection:MyIntValue", "4"}
};
var configBuilder = new ConfigurationBuilder()
.AddInMemoryCollection(inMemorySettings)
.AddEnvironmentVariables("DOTNET_");
var configurationRoot = configBuilder.Build();
using var configDispose = configurationRoot as IDisposable;
var mySetting = configurationRoot["MySetting"];
var mySubsectionSetting =
configurationRoot["Subsection:MyIntValue"];
int.TryParse(mySubsectionSetting,
out var myIntValue);
Console.WriteLine($"Using my setting: {mySetting}");
Console.WriteLine($"Using my subsection setting:" +
$" {mySubsectionSetting} => parsed value {myIntValue}");
We will also configure one of the settings via environment variable using launch settings:
{
"profiles": {
"CustomConfiguration": {
"commandName": "Project",
"environmentVariables": {
"DOTNET_Subsection:MyIntValue": "1"
}
}
}
}
Do note the DOTNET_ prefix used to set the environment variable. More about this later when we discuss code. If we start the application, we get the following output in the console:
Using my setting: Setting from in memory
Using my subsection setting: 1 => parsed value 1
We were able to read the setting and in the case of MyIntValue
also override
it with the setting from environment variables. So let’s discuss the code. There
we configure both sources and then access settings stored in them via
configuration root. Some key points to note:
- configuration root acts as a
Dictionary<string, string>
that aggregates all the entries from configured sources. Order of configuration matters, as the latest source, will override the duplicate entries. - ConfigurationBuilder is typically configured with extension methods that are provided in the NuGet packages which implement a configuration source. So if you get a code snippet and can’t find the AddXXX method, search for the used NuGet package.
- for environment variables, the same prefix was configured as with default configuration for WebHost and Generic Host. It is in general good practice to use some sort of prefix if you plan to use custom variables so that names don’t end up overlapping with variables from other applications.
- Configuration root needs to be disposed to release held resources. Currently
IConfigurationRoot
doesn’t inherit fromIDisposable
, but its implementation does, so hence this cast is needed. IfIDisposable
is not implemented, theusing
statement will skip the disposal. - Note the usage of
:
to access a subsection value. You can add as many sections to your configuration as you like.
When dealing with configuration sections, it can be cumbersome to always have to
use the full key for the section. Fortunately you can use
IConfigurationSection
, where you can the access values with their relative
names. Let’s see how this would look like in the case of MyIntValue
:
var mySubsection = configurationRoot.GetSection("Subsection");
var mySubsectionSetting = mySubsection["MyIntValue"];
int.TryParse(mySubsectionSetting,
out var myIntValue);
You can also write to configuration, but all this does is modify the underlying dictionary entry. This allows you to overwrite the value programmatically, but it will not persist the change into any of the configuration sources. Similarly, you could also write into a source and thus modify a dictionary entry. But again none of the current Microsoft implementations persist anything into the underlying source, e.g. a file.
configurationRoot["MySetting"] = "Setting from code";
As you can see from the samples so far, we are still dealing with string values, that need to be parsed, which again leads to repetitive and often error-prone code. Fortunately, there is again an out-of-the-box solution available, that enables us to parse values directly into our classes. For this, we need to add the Microsoft.Extensions.Configuration.Binder NuGet package. Let’s also add two classes that will represent our configuration structure:
public class MySettings
{
public string MySetting { get; set; }
public MySubsection Subsection { get; set; }
}
public class MySubsection
{
public int MyIntValue { get; set; }
public int[] IntArray { get; set; }
}
In order to bind our configuration to these classes, we add the following code:
var settings = new MySettings();
configurationRoot.Bind(settings);
Console.WriteLine($"Using my setting: {settings.MySetting}");
Console.WriteLine($"Using my subsection setting:" +
$" parsed value {settings.Subsection.MyIntValue}");
Note that the Bind
method can bind both to IConfigurationRoot
as well as
IConfiguration
, meaning you could also bind directly to a section in your
configuration. There is even an overload where you can provide the key to the
desired configuration section. Also noteworthy is, that if no match is found for
a configuration key or a class property, those are simply ignored and in the
case of the properties default value is returned. An example here being the
MySubsection.IntArray
. Alternative to Bind
is the Get<T>
method, which is
just a shorthand for newing up your object and binding:
var settings = configurationRoot.Get<MySettings>();
Since I will not cover dependency injection in this blog post, I should also mention the options pattern that is often used for accessing configuration in conjunction with dependency injection.
Sources:
- Code samples
- Configuration in ASP.NET Core on Microsoft Docs
- Building Cross-platform Applications with .NET Core on Pluralsight