Skip to content
Carmel Eve By Carmel Eve Software Engineer I
A simple pattern for using System.CommandLine with dependency injection

I've recently started using the new System.CommandLine packages when building CLI tools. I think they're a really useful addition to the C# libraries, but there isn't a huge amount of documentation out there about how best to use them so I thought I'd share some of the patterns we've been building up.

So, first we install the NuGet package. Remember to turn "show pre-release" on as the package is still in beta!

The Introduction to Rx.NET 2nd Edition (2024) Book, by Ian Griffiths & Lee Campbell, is now available to download for FREE.

We then start by creating a command:

public class GreetCommand : Command
{
    private readonly GreetOptions options;

    public GreetCommand(GreetOptions options) : base("greet", "Says a greeting to the specified person.")
    {
        var name = new Option<string>("--name")
        {
            Name = "name",
            Description = "The name of the person to greet.",
            IsRequired = true
        };

        this.AddOption(name);

        this.Handler = CommandHandler.Create((string name) => this.HandleCommand(name));
        this.options = options;
    }

    private int HandleCommand(string name)
    {
        try
        {
            Console.WriteLine($"{this.options.Greeting} {name}!");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
            return 0;
        }

        return 1;
    }
}

This defines a single option, called name which will be passed into the tool. It also takes an Options class as a constructor argument, which we want to retrieve from the app settings and pass in via DI. The command then uses the greeting from the app settings, and the name which has been passed in to display a greeting.

So, in order to set up the dependency injection we use an extension method on the service provider:

public static class CliCommandCollectionExtensions
{
    public static IServiceCollection AddCliCommands(this IServiceCollection services)
    {
        Type greetCommandType = typeof(DeployCommand);
        Type commandType = typeof(Command);

        IEnumerable<Type> commands = greetCommandType
            .Assembly
            .GetExportedTypes()
            .Where(x => x.Namespace == greetCommandType.Namespace && commandType.IsAssignableFrom(x));

        foreach (Type command in commands)
        {
            services.AddSingleton(commandType, command);
        }

        services.AddSingleton(sp =>
        {
            return
               sp.GetRequiredService<IConfiguration>().GetSection("Deployment").Get<DeploymentOptions>()
               ?? throw new ArgumentException("Deployment configuration cannot be missing.");
        });

        return services;
    }
}

This will retrieve anything that implements Command which is defined in the same namespace as the GreetCommand, and add it to the service collection. In order to cover commands in other namespaces/assemblies this would need to be updated.

We then write our (fairly simple!) main body of the program:

internal static class Program
{
    /// <summary>
    /// The entry point for the program.
    /// </summary>
    /// <param name="args">The arguments.</param>
    /// <returns>When complete, an integer representing success (0) or failure (non-0).</returns>
    public static async Task<int> Main(string[] args)
    {
        ServiceProvider serviceProvider = BuildServiceProvider();
        Parser parser = BuildParser(serviceProvider);

        return await parser.InvokeAsync(args).ConfigureAwait(false);
    }

    private static Parser BuildParser(ServiceProvider serviceProvider)
    {
        var commandLineBuilder = new CommandLineBuilder();

        foreach (Command command in serviceProvider.GetServices<Command>())
        {
            commandLineBuilder.AddCommand(command);
        }

        return commandLineBuilder.UseDefaults().Build();
    }

    private static ServiceProvider BuildServiceProvider()
    {
        var services = new ServiceCollection();
        IConfigurationRoot config = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .Build();

        services.AddSingleton<IConfiguration>(config);
        services.AddCliCommands();

        return services.BuildServiceProvider();
    }
}

And add the necessary app setting to the appsettings.json file (remembering to set it to "copy if newer" to the output directory):

{
  "greet:Greeting": "Hello"
}

If we then go to the properties for our project, and use

greet --name "Carmel"

as the debug arguments, then the output will be:

Hello Carmel!

Programming C# 10 Book, by Ian Griffiths, published by O'Reilly Media, is now available to buy.

And there we have it! A simple pattern for using dependency injection with System.CommandLine. Of course, these packages are still prerelease so things might change, and hopefully in future there will be more in-built support for these patterns. But for now, hope this helps!

If you want to see how the tool all fits together, here's the GitHub repo!

FAQs

How do I create a command using System.CommandLine? Use the `Command` base class as shown here.
How do I use dependency injection with System.CommandLine? Set up an extension method on the `ServiceProvider`, then use the `ServiceProvider as shown here.

Carmel Eve

Software Engineer I

Carmel Eve

Carmel is a software engineer, LinkedIn Learning instructor and STEM ambassador.

Over the past four years she has been focused on delivering cloud-first solutions to a variety of problems. These have ranged from highly-performant serverless architectures, to web applications, to reporting and insight pipelines and data analytics engines.

In her time at endjin, she has written many blog posts covering a huge range of topics, including deconstructing Rx operators and mental well-being and managing remote working.

Carmel's first LinkedIn Learning course on how to prepare for the Az-204 exam - developing solutions for Microsoft Azure - was released in April 2021. Over the last couple of years she has also spoken at NDC, APISpecs and SQLBits. These talks covered a range of topics, from reactive big-data processing to secure Azure architectures.

She is also passionate about diversity and inclusivity in tech. She is a STEM ambassador in her local community and is taking part in a local mentorship scheme. Through this work she hopes to be a part of positive change in the industry.

Carmel won "Apprentice Engineer of the Year" at the Computing Rising Star Awards 2019.

Carmel worked at endjin from 2016 to 2021.