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!