How to configure true dependency injection in System.CommandLine
Tags: programming, cli, dotnet, csharp
Published: 2023-06-20 12:35:00

[System.CommandLine](https://learn.microsoft.com/en-us/dotnet/standard/commandline/) is the **official .NET library that provides common functionality for command-line applications**. This includes features like argument parsing, automatic help text generation, tab autocomplete, suggestions, corrections, sub-commands, user cancellation, and much more. **Many official .NET tools** are built on top of *System.CommandLine*, including the [.NET CLI](https://learn.microsoft.com/en-us/dotnet/core/tools/), [Kiota](https://learn.microsoft.com/en-us/openapi/kiota/overview), [Tye](https://github.com/dotnet/tye), numerous [Azure tools](https://github.com/search?q=%22Include%3D%5C%22System.CommandLine%5C%22%22+org%3AAzure&type=code), and other [.NET additional tools](https://learn.microsoft.com/en-us/dotnet/core/additional-tools/).

Despite the fact that the library **has been in preview for several years**, its use in numerous .NET CLI programs inspires confidence regarding its maturity. The public C# API, though not final, is intuitive, the documentation is okay, and [many examples and tests in the GitHub repository](https://github.com/dotnet/command-line-api/) can assist you in getting started.

However, there is **one aspect of System.CommandLine that I dislike**: its lack of built-in support for dependency injection, like in any good modern .NET application. It makes it quite difficult to unit test and introduce decoupling in general.

To be fair, a service provider is indeed present within *System.CommandLine*. This provider implements `IServiceProvider` and can be accessed through the `InvocationContext.BindingContext.ServiceProvider` property. But upon [closer inspection](https://github.com/dotnet/command-line-api/blob/2.0.0-beta4.22272.1/src/System.CommandLine/Binding/BindingContext.cs), you'll notice that this is not your typical, modern, fully-featured service provider. In fact, it's a *pseudo* service provider backed by a simple dictionary of factories (`Dictionary<Type, Func<IServiceProvider, object?>>`). Its functionality is severely limited, and it's not exactly user-friendly when it comes to service registration. Don't take my word for it, make your own idea by reading [the documentation](https://learn.microsoft.com/en-us/dotnet/standard/commandline/dependency-injection).

Fortunately, *System.CommandLine* (in its current version, `2.0.0-beta4.22272.1`) is **highly extensible**. In this blog post, I'll show you **how to integrate true dependency injection** using the official [Microsoft.Extensions.DependencyInjection](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection/) NuGet package.

> *Please note that the System.CommandLine public C# API might change in the future, making this blog post eventually obsolete. I'll try to keep it up-to-date if that happens.*

## Implementing true dependency injection with middlewares

*System.CommandLine* comes with a built-in **chain of responsibility pattern**. This feature allows us to incorporate **custom behavior before and after the execution of our commands**. Several handy defaults are included out of the box, such as automatic help generation, suggestions, error handling, and parsing. Each of these features is managed by its respective middleware, arranged [in a specific sequence](https://github.com/dotnet/command-line-api/blob/2.0.0-beta4.22272.1/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs). You can also [insert your own middleware](https://github.com/dotnet/command-line-api/blob/2.0.0-beta4.22272.1/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs#L418-L456) using the `CommandLineBuilder.AddMiddleware(middleware, order?)` method.

```csharp
// This is the list of the default, built-in middlewares:
// https://github.com/dotnet/command-line-api/blob/2.0.0-beta4.22272.1/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs#L266
public static CommandLineBuilder UseDefaults(this CommandLineBuilder builder)
{
    return builder
        .UseVersionOption()
        .UseHelp()
        .UseEnvironmentVariableDirective()
        .UseParseDirective()
        .UseSuggestDirective()
        .RegisterWithDotnetSuggest()
        .UseTypoCorrections()
        .UseParseErrorReporting()
        .UseExceptionHandler()
        .CancelOnProcessTermination();
}
```

Our objective here is to **insert a middleware that enables us to register dependencies** using *Microsoft.Extensions.DependencyInjection* and then make them accessible through `InvocationContext.BindingContext`. Later, your command handlers will be able to access the registered dependencies.

To begin, let's **install these three NuGet packages**: [System.CommandLine](https://www.nuget.org/packages/System.CommandLine), [System.CommandLine.NamingConventionBinder](https://www.nuget.org/packages/System.CommandLine.NamingConventionBinder), and [Microsoft.Extensions.DependencyInjection](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection). Now, let's delve into the code:

```csharp
using System.CommandLine.Invocation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace System.CommandLine.Builder;

internal static class DependencyInjectionMiddleware
{
    public static CommandLineBuilder UseDependencyInjection(this CommandLineBuilder builder, Action<ServiceCollection> configureServices)
    {
        return UseDependencyInjection(builder, (_, services) => configureServices(services));
    }

    // This overload allows you to conditionally register services based on the command line invocation context
    // in order to improve startup time when you have a lot of services to register.
    public static CommandLineBuilder UseDependencyInjection(this CommandLineBuilder builder, Action<InvocationContext, ServiceCollection> configureServices)
    {
        return builder.AddMiddleware(async (context, next) =>
        {
            // Register our services in the modern Microsoft dependency injection container
            var services = new ServiceCollection();
            configureServices(context, services);
            var uniqueServiceTypes = new HashSet<Type>(services.Select(x => x.ServiceType));

            services.TryAddSingleton(context.Console);

            await using var serviceProvider = services.BuildServiceProvider();

            // System.CommandLine's service provider is a "fake" implementation that relies on a dictionary of factories,
            // but we can still make sure here that "true" dependency-injected services are available from "context.BindingContext".
            // https://github.com/dotnet/command-line-api/blob/2.0.0-beta4.22272.1/src/System.CommandLine/Invocation/ServiceProvider.cs
            context.BindingContext.AddService<IServiceProvider>(_ => serviceProvider);

            foreach (var serviceType in uniqueServiceTypes)
            {
                context.BindingContext.AddService(serviceType, _ => serviceProvider.GetRequiredService(serviceType));

                // Enable support for "context.BindingContext.GetServices<>()" as in the modern dependency injection
                var enumerableServiceType = typeof(IEnumerable<>).MakeGenericType(serviceType);
                context.BindingContext.AddService(enumerableServiceType, _ => serviceProvider.GetServices(serviceType));
            }

            await next(context);
        });
    }
}
```

The key here is to create a proper `ServiceProvider` and then ensure every registered service is accessible through the *pseudo* service provider. Now, we can use our `UseDependencyInjection` extension method in our main program:

```csharp
var helloCommand = new Command("hello");
helloCommand.Handler = CommandHandler.Create<Dependency, IConsole>((dependency, console) =>
{
    console.WriteLine($"Hello {dependency.GetName()}!");
});

var rootCommand = new RootCommand { helloCommand };

var builder = new CommandLineBuilder(rootCommand).UseDefaults().UseDependencyInjection(services =>
{
    // True dependency injection using Microsoft.Extensions.DependencyInjection
    services.AddSingleton<Dependency>();
});

return builder.Build().Invoke(args);
```

In this program, we use the `CommandHandler.Create<TDep1, ... TDepN>((dep1, ... depN) => { })` generic method provided by the *System.CommandLine.NamingConventionBinder* package. This method lets us pull instances registered in the *pseudo* service provider with ease. Because we've "forwarded" our registered services into this *pseudo* service provider, they're now easily accessible.

## Automatic command options class binding and dependency-injected command options handler

With true dependency injection now at our disposal, we can take things a step further. We can introduce a **pattern for handling command options that is completely separate and easy to test**. To do this, we'll bring in a few interfaces and a new generic base `Command<,>` class:

```csharp
using System.CommandLine.NamingConventionBinder;
using Microsoft.Extensions.DependencyInjection;

namespace System.CommandLine;

public interface ICommandOptions
{
}

public interface ICommandOptionsHandler<in TOptions>
{
    Task<int> HandleAsync(TOptions options, CancellationToken cancellationToken);
}

public abstract class Command<TOptions, TOptionsHandler> : Command
    where TOptions : class, ICommandOptions
    where TOptionsHandler : class, ICommandOptionsHandler<TOptions>
{
    protected Command(string name, string description)
        : base(name, description)
    {
        this.Handler = CommandHandler.Create<TOptions, IServiceProvider, CancellationToken>(HandleOptions);
    }

    private static async Task<int> HandleOptions(TOptions options, IServiceProvider serviceProvider, CancellationToken cancellationToken)
    {
        // True dependency injection happening here
        var handler = ActivatorUtilities.CreateInstance<TOptionsHandler>(serviceProvider);
        return await handler.HandleAsync(options, cancellationToken);
    }
}
```

The goal here is **to draw a clear line** between:

* The declaration of the command and its CLI arguments (like the `--to` argument in the next example),
* The options that represent the parsed arguments as a simple [POCO](https://en.wikipedia.org/wiki/Plain_old_CLR_object) (created by *System.CommandLine.NamingConventionBinder*),
* The options handler, which can handle that POCO with first-class cancellation support and dependency injection in its constructor.

Let's consider an example with a command that says hello to someone:

```csharp
public class HelloCommand : Command<HelloCommandOptions, HelloCommandOptionsHandler>
{
    // Keep the hard dependency on System.CommandLine here
    public HelloCommand()
        : base("hello", "Say hello to someone")
    {
        this.AddOption(new Option<string>("--to", "The person to say hello to"));
    }
}

public class HelloCommandOptions : ICommandOptions
{
    // Automatic binding with System.CommandLine.NamingConventionBinder
    public string To { get; set; } = string.Empty;
}

public class HelloCommandOptionsHandler : ICommandOptionsHandler<HelloCommandOptions>
{
    private readonly IConsole _console;

    // Inject anything here, no more hard dependency on System.CommandLine
    public HelloCommandOptionsHandler(IConsole console)
    {
        this._console = console;
    }

    public Task<int> HandleAsync(HelloCommandOptions options, CancellationToken cancellationToken)
    {
        this._console.WriteLine($"Hello {options.To}!");
        return Task.FromResult(0);
    }
}
```

Here's what the program looks like now:

```csharp
var rootCommand = new RootCommand
{
    new HelloCommand()
};

var builder = new CommandLineBuilder(rootCommand).UseDefaults().UseDependencyInjection(services =>
{
    // Register your services here and use them in your DI-activated command handlers
    // [...]
});

return builder.Build().Invoke(args);
```

## Conclusion

You now have **a clear separation of concerns and decoupling** between your CLI arguments and the actual work being done. You can separately test the binding of the CLI arguments to your options POCO, and the options handler with easy dependency mocking. Not only that, but your **main program looks clean and lean**, giving you a better view of the command hierarchy.

I'd love to hear your thoughts on this approach, and if you're already using *System.CommandLine*, please share how you're applying it in your projects!