How to use middleware in System.CommandLine

Important

System.CommandLine is currently in PREVIEW, and this documentation is for version 2.0 beta 4. Some information relates to prerelease product that may be substantially modified before it's released. Microsoft makes no warranties, express or implied, with respect to the information provided here.

This article explains how to work with middleware in command-line apps that are built with the System.CommandLine library. Use of middleware is an advanced topic that most System.CommandLine users won't need to consider.

Introduction to middleware

While each command has a handler that System.CommandLine will route to based on input, there's also a mechanism for short-circuiting or altering the input before your application logic is invoked. In between parsing and invocation, there's a chain of responsibility, which you can customize. A number of built-in features of System.CommandLine make use of this capability. This is how the --help and --version options short-circuit calls to your handler.

Each call in the pipeline can take action based on the ParseResult and return early, or choose to call the next item in the pipeline. The ParseResult can even be replaced during this phase. The last call in the chain is the handler for the specified command.

Add to the middleware pipeline

You can add a call to this pipeline by calling CommandLineBuilderExtensions.AddMiddleware. Here's an example of code that enables a custom directive. After creating a root command named rootCommand, the code as usual adds options, arguments, and handlers. Then the middleware is added:

var commandLineBuilder = new CommandLineBuilder(rootCommand);

commandLineBuilder.AddMiddleware(async (context, next) =>
{
    if (context.ParseResult.Directives.Contains("just-say-hi"))
    {
        context.Console.WriteLine("Hi!");
    }
    else
    {
        await next(context);
    }
});

commandLineBuilder.UseDefaults();
var parser = commandLineBuilder.Build();
await parser.InvokeAsync(args);

In the preceding code, the middleware writes out "Hi!" if the directive [just-say-hi] is found in the parse result. When this happens, the command's normal handler isn't invoked. It isn't invoked because the middleware doesn't call the next delegate.

In the example, context is InvocationContext, a singleton structure that acts as the "root" of the entire command-handling process. This is the most powerful structure in System.CommandLine, in terms of capabilities. There are two main uses for it in middleware:

  • It provides access to the BindingContext, Parser, Console, and HelpBuilder to retrieve dependencies that a middleware requires for its custom logic.
  • You can set the InvocationResult or ExitCode properties in order to terminate command processing in a short-circuiting manner. An example is the --help option, which is implemented in this manner.

Here's the complete program, including required using directives.

using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Parsing;

class Program
{
    static async Task Main(string[] args)
    {
        var delayOption = new Option<int>("--delay");
        var messageOption = new Option<string>("--message");

        var rootCommand = new RootCommand("Middleware example");
        rootCommand.Add(delayOption);
        rootCommand.Add(messageOption);

        rootCommand.SetHandler((delayOptionValue, messageOptionValue) =>
            {
                DoRootCommand(delayOptionValue, messageOptionValue);
            },
            delayOption, messageOption);

        var commandLineBuilder = new CommandLineBuilder(rootCommand);

        commandLineBuilder.AddMiddleware(async (context, next) =>
        {
            if (context.ParseResult.Directives.Contains("just-say-hi"))
            {
                context.Console.WriteLine("Hi!");
            }
            else
            {
                await next(context);
            }
        });

        commandLineBuilder.UseDefaults();
        var parser = commandLineBuilder.Build();
        await parser.InvokeAsync(args);
    }

    public static void DoRootCommand(int delay, string message)
    {
        Console.WriteLine($"--delay = {delay}");
        Console.WriteLine($"--message = {message}");
    }
}

Here's an example command line and resulting output from the preceding code:

myapp [just-say-hi] --delay 42 --message "Hello world!"
Hi!

See also

System.CommandLine overview