Binding multivalue-keys with INI-files using Microsoft.Extension.Configurations

Binding multivalue-keys with INI-files using Microsoft.Extension.Configurations

Microsoft’s documentation on their new configuration framework is hard to find for two reasons:

  1. It is not clear what name you should search for? Microsoft.Extension.Configurations? ASP.NET configuration? Dotnet configuration?
  2. The documentation that is available omits many details.

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.

Summary of 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-insenstive 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");

Binding sections to a POCO

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-insenstive, the binding works fine.

How to represent an array of objects in an INI file?

JSON supports single value objects and array objects. But 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

How to bind to a multivalue-key?

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; 

What if one of the elements of the array cannot be fully initialized?

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);

What if the bound object has read-only properties?

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 onther 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.