Posted: August 26, 2018
Last modified: 21 December, 2020
Microsoft's documentation on their new configuration framework is hard to find for two reasons:
Disclaimer: my experiences are based on version 1.1.2 of the Microsoft.Extension.Configuration libraries. The latest version of the library at the date of publication is 2.1.1.
In this post I describe some of the problems I encountered and how I solved them when working with Microsoft.Extension.Configuration.
To read configuration, create an instance of the ConfigurationBuilder
class and add as many sources as you like.
Sources can be command line arguments, environment variables, INI files, JSON files and it is quite easy to implement
your own source class if needed. Finally, call the Build()
method to build an instance of the configuration object.
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddIniFile("config.ini")
.AddJsonFile("config.json")
.AddCommandLine(args)
.Build();
The configuration
object can be thought of as a Dictionary<string, string>
, with case-insensitive keys.
Imagine that config.ini
looks like this:
[logging]
level = DEBUG
format = XML
output = file
filename = logging.xml
Then this file will add the following key-value-pairs to the configuration:
logging:level = DEBUG
logging:format = XML
logging:output = file
logging:filename = logging.xml
logging
is a section withing the configuration. Nested sections are allowed. A nested section has a key
that looks like mail:smtp
and has keys like mail:smtp:host
and mail:smtp:port
.
Retrieving values from configuration
is possible with method GetValue()
like this:
var level = configuration.GetValue<string>("logging:level");
var format = configuration.GetValue("logging:format", "JSON");
// "JSON" is default value and determines type of returned value
And by using GetSection()
, there is no longer the need to prefix the key with the path to the section:
IConfiguration loggingSection = configuration.GetSection("logging");
var output = loggingSection.GetValue<string>("output");
var filename = loggingSection.GetValue<string>("filename");
One of the reasons why I looked into Microsoft.Extension.Configuration libraries is the option to bind a POCO to a section of the configuration, like this:
var configuration = new ConfigurationBuilder()
.AddIniFile("config.ini")
.Build();
var props = new LoggingProps();
configuration.GetSection("logging").Bind(props);
Imagine LoggingProps
has properties of type string
with names Level
, Format
, Output
and FileName
, then these properties will
be set with the values from config.ini
. Now the configured values are accessible like this:
SetLogLevel(props.Level);
if (props.Output == "file")
{
SetLogoutputToFile(props.Format, props.FileName);
}
Note that the names of the properties differ from the names in the keys by casing. Since the keys are considered to be case-insensitive, the binding works fine.
JSON supports single value objects and array objects. However, JSON files are not easily readable by humans. INI files are better readable. But how are arrays represented in an INI file?
If you want to store configuration of the same type of object multiple times in an INI file, then create a multivalue-key. A multivalue-key consists of a numbered subsection per value of the key:
[logging:0]
logging:level = DEBUG
logging:format = XML
logging:output = file
logging:filename = logging.xml
[logging:1]
logging:level = WARN
logging:format = TEXT
logging:output = email
logging:mailTo = devops@yourcompany.com
The configuration above can be bound as follows:
class LoggingPropsAggregate
{
public LoggingProps[] logging { get; set; } = new LoggingProps[0];
}
var configuration = new ConfigurationBuilder()
.AddIniFile("config.ini")
.Build();
var propsAggregate = new LoggingPropsAggregate();
configuration.Bind(propsAggregate);
// Logging can be accessed like this
var level = logging[0].Level;
class Duration {
public string Name { get; set; }
public TimeSpan Duration { get; set; } = TimeSpan.FromMinutes(1);
}
class Props {
public Duration[] Durations { get; set; }
}
PT60M
is the ISO-8601 format, which is not supported while binding. While binding, the format 0.01:00:00
is supported (days.hh:mm:ss
). Unfortunately,
when an array is bound and one of the array's elements' fields is of type TimeSpan
and cannot be bound, then the whole element is not bound.
This is shown in the following code fragment.
Given config.ini:
[Duration:0]
Name = hour
Duration = PT60M
[Duration:1]
Name = default
Then binding this file to an instance of Props
has this result:
var configuration = new ConfigurationBuilder()
.AddIniFile("config.ini")
.Build();
var props = new Props();
configuration.Bind(props);
// props.Duration[0] == null;
// props.Duration[1].Name == "default"
// props.Duration[2].Duration == TimeSpan.FromMinutes(1);
For types that cannot be bound by the Microsoft.Extension.Configuration libraries, one way to still allow these types to be passed in the configuration is to provide a writable property of type string and add a read-only property that builds an instance of the type using the string value.
Beware! While binding, also the read-only property will be read! This is done, in case the read-only property returns another object that needs to be bound to a subsection. If your read-only property throws an exception, because the string value it needs is not filled in yet, or it contains an invalid value, then binding will fail with an exception. So make sure that read-only properties you add never throw an exception.