Intro

  If you are like me, you want to first establish a nice skeleton app that has everything just right before you start writing your actual code. However, as weird as it may sound, I couldn't find a way to use command line parameters with dependency injection, in the same simple way that one would use a configuration file with IOptions<T> for example. This post shows you how to use CommandLineParser, a nice library that handles everything regarding command line parsing, but in a dependency injection friendly way.

  In order to use command line arguments, we need to obtain them. For any .NET Core application or .NET Framework console application you get it from the parameters of the static Main method from Program. Alternately, you can use Environment.CommandLine, which is actually a string, not an array of strings, or Environment.GetCommandLineArgs(). But all of these are kind of nudging you towards some ugly code that either has a dependency on the static Environment, either has code early in the application to handle command line arguments, or stores the arguments somehow. What we want is complete separation of modules in our application.

Defining the command line parameters

  In order to use CommandLineParser, you write a class that contains the properties you expect from the command line, decorated with attributes that inform the parser what is the expected syntax for all. In this post I will use this:

// the way we want to use the app is
// FileUtil <command> [-loglevel loglevel] [-quiet] -output <outputFile> file1 file2 .. file10
public class FileUtilOptions
{
    // use Value for parameters with no name
    [Value(0, Required = true, HelpText = "You have to enter a command")]
    public string Command { get; set; }

    // use Option for named parameters
    [Option('l',"loglevel",Required = false, HelpText ="Log level can be None, Normal, Verbose")]
    public string LogLevel { get; set; }

    // use bool for named parameters with no value
    [Option('q', "quiet", Default = false, Required = false, HelpText = "Quiet mode produces no console output")]
    public bool Quiet { get; set; }

    // Required for required values
    [Option('o', "output", Required = true, HelpText = "Output file is required")]
    public string OutputFile { get; set; }

    // use Min/Max for enumerables
    [Value(1, Min = 1, Max = 10, HelpText = "At least one file name and at most 10")]
    public IEnumerable<string> Files { get; set; }
}

  At this point the blog post will split into two parts. One is very short and easy to use, thanks to commenter Murali Karunakaran. The other one is what I wrote in 2020 when I didn't know better. This second part is just a reminder of how much people can write when they don't have to :)

The short and easy solution

All you have to do is add your command line parameters class as options, then define what will happen when you request one instance of it:

// in ConfigureServices or wherever you define dependencies for injection
services
  .AddOptions<FileUtilOptions>()
  .Configure(opt => 
    Parser.Default.ParseArguments(() => opt, Environment.GetCommandLineArgs())
  );

// when needing the parameters
public SomeConstructor(IOptions<FileUtilOptions> options)
{
    _options = options.Value;
}

When an instance of FileUtilOptions is requested, the lambda will be executed, setting the options based on ParseArguments. If any issue, the parser will display the help to the console

CommandLineParserError.jpg

This process, however, does not throw any exceptions. The instance of FileUtilOptions requested will be provided empty or partially/incorrectly filled. In order to handle the errors, some more complex code is needed, and here is a silly example:

using (var writer = new StringWriter())
{
	var parser = new Parser(configuration =>
	{
		configuration.AutoHelp = true;
		configuration.AutoVersion = false;
		configuration.CaseSensitive = false;
		configuration.IgnoreUnknownArguments = true;
		configuration.HelpWriter = writer;
	});
	var result = parser.ParseArguments<T>(_args);
	result.WithNotParsed(errors => HandleErrors(errors, writer));
	result.WithParsed(value => _value = value);
}

// a possible way to handle errors
private static void HandleErrors(IEnumerable<Error> errors, TextWriter writer)
{
	if (errors.Any(e => e.Tag != ErrorType.HelpRequestedError && e.Tag != ErrorType.VersionRequestedError))
	{
		string message = writer.ToString();
		throw new CommandLineParseException(message, errors, typeof(T));
	}
}

Now, the original post follows:

Writing a lot more than necessary

  How can we get the arguments by injection? By creating a new type that encapsulates the simple string array.

// encapsulates the arguments
public class CommandLineArguments
{
    public CommandLineArguments(string[] args)
    {
        this.Args = args;
    }

    public string[] Args { get; }
}

// adds the type to dependency injection
services.AddSingleton<CommandLineArguments>(new CommandLineArguments(args));
// the generic type declaration is superfluous, but the code is easy to read

  With this, we can access the command line arguments anywhere by injecting a CommandLineArguments object and accessing the Args property. But this still implies writing command line parsing code wherever we need that data. We could add some parsing logic in the CommandLineArguments class so that instead of the command line arguments array it would provide us with a strong typed value of the type we want. But then we would put business logic in a command line encapsulation class. Why would it know what type of options we need and why would we need only one type of options? 

  What we would like is something like

public SomeClass(IOptions<MyCommandLineOptions> clOptions) {...}

  Now, we could use this system by writing more complicated that adds a ConfigurationSource and then declaring that certain types are command line options. But I don't want that either for several reasons:

  • writing configuration providers is complex code and at some moment in time one has to ask how much are they willing to write in order to get some damn arguments from the command line
  • declaring the types at the beginning does provide some measure of centralized validation, but on the other hand it's declaring types that we need in business logic somewhere in service configuration, which personally I do not like

  What I propose is adding a new type of IOptions, one that is specific to command line arguments:

// declare the interface for generic command line options
public interface ICommandLineOptions<T> : IOptions<T>
    where T : class, new() { }

// add it to service configuration
services.AddSingleton(typeof(ICommandLineOptions<>), typeof(CommandLineOptions<>));

// put the parsing logic inside the implementation of the interface
public class CommandLineOptions<T> : ICommandLineOptions<T>
    where T : class, new()
{
    private T _value;
    private string[] _args;

    // get the arguments via injection
    public CommandLineOptions(CommandLineArguments arguments)
    {
        _args = arguments.Args;
    }

    public T Value
    {
        get
        {
            if (_value==null)
            {
                // set the value by parsing command line arguments
            }
            return _value;
        }
    }

}

  Now, in order to make it work, we will use CommandLineParser which functions in a very simple way:

  • declare a Parser
  • create a POCO class that has properties decorated with attributes that define what kind of command line parameter they are
  • parse the command line arguments string array into the type of class declared above
  • get the value or handle errors

  Also, to follow the now familiar Microsoft pattern, we will write an extension method to register both arguments and the mechanism for ICommandLineOptions. The end result is:

// extension class to add the system to services
public static class CommandLineExtensions
{
    public static IServiceCollection AddCommandLineOptions(this IServiceCollection services, string[] args)
    {
        return services
            .AddSingleton<CommandLineArguments>(new CommandLineArguments(args))
            .AddSingleton(typeof(ICommandLineOptions<>), typeof(CommandLineOptions<>));
    }
}

public class CommandLineArguments // defined above

public interface ICommandLineOptions<T> // defined above

// full class implementation for command line options
public class CommandLineOptions<T> : ICommandLineOptions<T>
    where T : class, new()
{
    private T _value;
    private string[] _args;

    public CommandLineOptions(CommandLineArguments arguments)
    {
        _args = arguments.Args;
    }

    public T Value
    {
        get
        {
            if (_value==null)
            {
                using (var writer = new StringWriter())
                {
                    var parser = new Parser(configuration =>
                    {
                        configuration.AutoHelp = true;
                        configuration.AutoVersion = false;
                        configuration.CaseSensitive = false;
                        configuration.IgnoreUnknownArguments = true;
                        configuration.HelpWriter = writer;
                    });
                    var result = parser.ParseArguments<T>(_args);
                    result.WithNotParsed(errors => HandleErrors(errors, writer));
                    result.WithParsed(value => _value = value);
                }
            }
            return _value;
        }
    }

    private static void HandleErrors(IEnumerable<Error> errors, TextWriter writer)
    {
        if (errors.Any(e => e.Tag != ErrorType.HelpRequestedError && e.Tag != ErrorType.VersionRequestedError))
        {
            string message = writer.ToString();
            throw new CommandLineParseException(message, errors, typeof(T));
        }
    }
}

// usage when configuring dependency injection
services.AddCommandLineOptions(args);

Enjoy!

Final notes

Now there are some quirks in the implementation above. One of them is that the parser class generates the usage help by writing it to a TextWriter (default being Console.Error), but since we want this to be encapsulated, we declare our own StringWriter and then store the generated help if any errors. In the case above, I am storing the help text as the exception message, but it's the principle that matters.

Also, with this system one can ask for multiple types of command line options classes, depending on the module, without the need to declare said types at the configuration of dependency injection. The downside is that if you want validation of the command line options at the very beginning, you have to write extra code. In the way implemented above, the application will fail when first asking for a command line option that cannot be mapped on the command line arguments.

Note that the short style of a parameter needs to be used with a dash, the long one with two dashes:

  • -o outputFile.txt - correct (value outputFile.txt)
  • --output outputFile.txt - correct (value outputFile.txt)
  • -output outputFile.txt - incorrect (value output and outputFile.txt is considered an unnamed argument)

Comments

murali

We can combine the ErrorHandling and Registration to get best of both // in ConfigureServices or wherever you define dependencies for injection services .AddOptions&lt;FileUtilOptions&gt;() .Configure(opt =&gt; { using (var writer = new StringWriter()) { var parser = new Parser(configuration =&gt; { configuration.AutoHelp = true; configuration.AutoVersion = false; configuration.CaseSensitive = false; configuration.IgnoreUnknownArguments = true; configuration.HelpWriter = writer; }); var result = parser.ParseArguments(() =&gt; opt, Environment.GetCommandLineArgs()); result.WithNotParsed(errors =&gt; HandleErrors&lt;FileUtilOptions&gt;(errors, writer)); } }); private static void HandleErrors&lt;T&gt;(IEnumerable&lt;Error&gt; errors, TextWriter writer) { if (errors.Any(e =&gt; e.Tag != ErrorType.HelpRequestedError &amp;&amp; e.Tag != ErrorType.VersionRequestedError)) { string message = writer.ToString(); throw new CommandLineParseException(message, errors, typeof(T)); } } // when needing the parameters public SomeConstructor(IOptions&lt;FileUtilOptions&gt; options) { _options = options.Value; }

murali

Siderite

I&#39;ve fixed the comment display and also updated the post with your contribution. Thanks a lot for your comments, Murali!

Siderite

murali

For AddOptions use TemplateParameter FileOptions. For HostedService use CommandLineService as template parameter and in CommandLineService.cs for Ioptions use FileOptions as templateParameter. As soon as I use anglerackets, it is removed

murali

murali

1. Define a class to use for options // the way we want to use the app is // FileUtil &lt;command&gt; [-loglevel loglevel] [-quiet] -output &lt;outputFile&gt; file1 file2 .. file10 public class FileUtilOptions { // use Value for parameters with no name [Value(0, Required = true, HelpText = &quot;You have to enter a command&quot;)] public string Command { get; set; } // use Option for named parameters [Option(&#39;l&#39;,&quot;loglevel&quot;,Required = false, HelpText =&quot;Log level can be None, Normal, Verbose&quot;)] public string LogLevel { get; set; } // use bool for named parameters with no value [Option(&#39;q&#39;, &quot;quiet&quot;, Default = false, Required = false, HelpText = &quot;Quiet mode produces no console output&quot;)] public bool Quiet { get; set; } // Required for required values [Option(&#39;o&#39;, &quot;output&quot;, Required = true, HelpText = &quot;Output file is required&quot;)] public string OutputFile { get; set; } // use Min/Max for enumerables [Value(1, Min = 1, Max = 10, HelpText = &quot;At least one file name and at most 10&quot;)] public IEnumerable&lt;string&gt; Files { get; set; } } //Add registrations in program.cs or Startup.cs services.AddOptions&lt;FileUtilOptions&gt;().Configure(opt =&gt; Parser.Default.ParseArguments(() =&gt; opt, args); //args is from Main(string[] args) function. services.AddHostedService&lt;CommandLineService&gt;(); //CommandLineService.cs public class CommandLineService : BackgroundService { private readonly FileUtilOptions options; public CommandLineService (IOptions&lt;FileUtilOptions&gt; fileUtilOptions) { this.options = fileUtilOptions.Value; } }

murali

Siderite

That looks promising. Can you link to a blog post or expand the code?

Siderite

murali

Its easy without so much of code. Just one line services.AddOptions&lt;FileUtiOptions&gt;().Configure(opt =&gt; Parser.Default.ParseArguments(() =&gt; opt, args); Then inject IOptions&lt;FileUtiOptions&gt; fileOptions in any service you need

murali

Siderite

Thanks, Geoff! Although I wouldn&#39;t add all of those constructors, no matter what exception best practices suggest. Also, String.Format? What are we, animals? We have string interpolation now :)

Siderite

Geoff DeFilippi

Maybe try something like this for the CommandLineParserException: public class CommandLineParseException : Exception { public CommandLineParseException() { } public CommandLineParseException(string message) : base(message) { } public CommandLineParseException(string message, Exception innerException) : base(message, innerException) { } public CommandLineParseException(string message, IEnumerable&lt;Error&gt; errors, Type type) : base(message) { var sb = new StringBuilder(); foreach(var error in errors) { sb.Append(error.Tag.ToString()); } message = string.Format(&quot;{0}: {1}: Errors: {2}&quot;, type.Name, message, sb.ToString()); } }

Geoff DeFilippi

michiel

nice post, very creative! could you please add implementation details of CommandLineParseException class?

michiel

Post a comment