Integrate command-line parsers into dotnet core configuration providers

Manuel Riezebosch
4 min readMar 11, 2020
Photo by Jay Zhang on Unsplash

TL;DR: https://github.com/riezebosch/CommandLiners

Working with stryker I really disliked the wacky javascript/typescript notation on the command line arguments. But they already found out themselves. So when I started to fiddle around to improve on the arguments parsing my first thought was:

How do I replace all of this with the out-of-the-box configuration providers available in dotnet core?

But then I discovered two problems on that side:

How are multiple arguments for the same option parsed with the default command line provider?

The answer: wack.

It’s a bit burden in the documentation, but it turns out the command line provider only understands the array format that is used to bind an array to a class: --test-project:0 my.tests.csproj --test-project:1 other.tests.csproj. I don’t want users of my application to worry about indices and stuff.

The other, a bit less of a problem, is that

The configuration providers in general don’t have support for argument validation or providing help text.

Something all of the command-line parser frameworks do have.

This where my journey started.

Why?

Having multiple sources of arguments turns out to be very useful when building dotnet core command-line applications. You specify the order of import and can:

  1. set default values on the configuration object
  2. override that values with that environment variables
  3. override that by values from local configuration files
  4. override that by command line arguments.

Whereas all the major command-line parsing frameworks only seem to support a single source of input an only take care of the command-line arguments.

Parsing the Arguments

What I needed to do was hook into the parsing process and provide the configuration source with values that could be treated as an array. I could not do that in the default CommandLineConfigurationProvider since the argument parsing and capturing values are tightly coupled.

My next step was to do the argument parsing myself and load that into a custom configuration provider.

What is essential there is that I really needed something that understands the difference in arguments: this is an option, this is the value of the said option, this is an option but it has no value, this is the long name of the same option, etc. But at the same time, it must not bind that information to an object since I want the raw values and leave the binding to the configuration builder and binder.

All the major parsing frameworks seemed to fail in the last part. So I started working on my own framework. I did some research on conventions and guidelines and found out that POSIX guidelines and GNU extension are widely accepted.

Eventually, it became a nice little framework where you could as easy as the default command-line provider hook the arguments into the configuration builder:

var args = new[] { "--test-project", "my.tests.csproj", "--test-project", "other.tests.csproj" };

var builder = new ConfigurationBuilder()
.AddJsonFile("config.json")
.AddPosixCommandLine(args);
.Build();

And:

var aliases = new Dictionary<string, string>
{
["d"] = nameof(Options.Debug),
["l"] = nameof(Options.Log)
};
var args = new[] { "-dl" };
var builder = new ConfigurationBuilder()
.AddPosixCommandLine(args, aliases)
.Build();

Which improved into:

var args = new[] { "-dl" };

var builder = new ConfigurationBuilder()
.AddCommandLineOptions(args.ToPosix(map => map
.Add("d", o => o.Debug)
.Add("l", o => Options.Log)));
.Build();

Integrating Existing Frameworks

Now I fixed the argument parsing and hooking into the configuration provider world. But argument validation and providing a help text was still left as an exercise for the reader.

Mono.Options

When revisiting Mono.Options I was triggered that it did not bind values to objects but used delegates for that where the user of the library had to assign the values from that. I was keen on trying to integrate this into the command-line options provider that I came up with.

It turned out that, after some refactoring, I only needed about 3 additional lines of code for that!

public class MapOptions<TOptions> 
{
private readonly IList<Option> _data = new List<Option>();
public IEnumerable<Option> Data => _data; public void Add<T>(string data, Expression<Func<TOptions, T>> to)
=> _data.Add(new OptionArgument(PropertySelector.Do(to), data));
}

With that you map the options you specify on the OptionSet to an MapOptions<TOptions> instance so that after parsing all options are available with specified values.

var map = new MapOptions<Simple>();
new OptionSet
{
{"t|test-project=", data => map.Add(data, x => x.TestProjects)}
}.Parse(new[] { "--test-project", "my.tests.csproj" });

So the quest was on.

CommandLineUtils

The next popular framework CommandLineUtils was a bit more work, but it turned out to also have a builder API where one can hook into the argument parsing process.

var map = new MapOptions<Simple>();
var app = new CommandLineApplication();
app.Option("-t|--test-project", "description for option")
.Map(map, to => to.TestProjects);

var result = app.Parse("--test-project", "my.tests.csproj");
var builder = new ConfigurationBuilder()
.AddCommandLineOptions(map.FromCommand(result.SelectedCommand))
.Build();

CommandLineParser

The third framework I tried was CommandLineParser. This is is a bit of a different beast, but it turned out to be possible to (sort of) integrate with the configuration providers. By using a factory method that returns an instance object which it binds values to:

var builder = new ConfigurationBuilder()
.AddJsonFile("config.json")
.Build();

var options = new Options();
builder.Bind(options);

Parser.Default.ParseArguments(() => options, new[] { "--test-project", "my.tests.csproj" });

Of course, this is no real integration since:

  • you can only bind it to an object and not use the configuration values in any other way
  • it is not capable to bind to an object graph
  • the factory function is not available when parsing verbs.

I’ve not found a way yet to integrate it more nicely, with or without the use of the bridge I built, but expect me to create a pull request in the near future.

Use it!

In my opinion, the current solution is nice, simple and strong enough to support most of the situations or parsers that allow for separation between parsing and binding.

Pick the framework that you’re familiar with or like the most and integrate it with the other configuration providers.

Try out and let’s improve!

--

--