Fluent syntax for command creation
System.CommandLine is a nice library that convers a lot of edge cases, but in my opinion creating commands (adding handlers) is quite verbose and adds a lot of boilerplate code to the main function.
What I was thinking is that maybe it could be possible to add some extension methods to make creating commands more simple. For example, in my project I have something like this:
private static async Task<int> Main(string[] args)
{
var rootCommand = new RootCommand
{
new CommandBuilder("do-something", "Does something")
.WithOption("--type", CommandLineUtils.GetType)
.WithOption<bool>("--run-migrations")
.Build(DoSomethingAsync),
new CommandBuilder("do-another-thing", "Does another thing")
.WithArgument<string>()
.Build(DoAnotherThingAsync),
};
return await rootCommand.InvokeAsync(args)
.ConfigureAwait(false);
}
private static async Task DoSomethingAsync(SomeType type, bool runMigrations)
{
await using var services = BuildServiceProvider();
if (runMigrations)
services.RunMigrations();
await services.GetRequiredService<ISomethingService>()
.DoSomethingAsync(type);
}
private static async Task DoAnotherThingAsync(string arg)
{
await using var services = BuildServiceProvider();
await services.GetRequiredService<IAnotherService>()
.DoSomethingElseAsync(arg);
}
The CommandBuilder is a class that encapsulated the boiler plate code. For example:
namespace System.CommandLine;
public sealed class CommandBuilder
{
internal readonly string Name;
internal readonly string Description;
public CommandBuilder(string name, string description)
{
Name = name;
Description = description;
}
public CommandHandler1Builder<T> WithOption<T>(in string name, ParseArgument<T> parseArgument) =>
new(this, name, ArgType.Option, parseArgument);
}
And CommandHandler1Builder<T>, CommandHandler2Builder<T1, T2>, CommandHandler3Builder<T1, T2, T3>, etc. are used to create handlers, arguments and options. For example:
using System.CommandLine.Binding;
namespace System.CommandLine;
public sealed class CommandHandler1Builder<T> : CommandHandlerBuilderBase
{
internal readonly CommandBuilder CommandBuilder;
private readonly string _name;
private readonly ParseArgument<T>? _parseArgument;
internal CommandHandler1Builder(in CommandBuilder commandBuilder, in string name, in ArgType argType, in ParseArgument<T>? parseArgument = null)
: base(argType)
{
CommandBuilder = commandBuilder;
_name = name;
_parseArgument = parseArgument;
}
public Command Build(Func<T, Task> handler)
{
var command = new Command(CommandBuilder.Name, CommandBuilder.Description);
var value = AppendValueDescriptor(command);
command.SetHandler(handler, value);
return command;
}
public CommandHandler2Builder<T, T2> WithOption<T2>(in string name) =>
new(this, name, ArgType.Option);
public CommandHandler2Builder<T, T2> WithArgument<T2>(in string name) =>
new(this, name, ArgType.Argument);
internal IValueDescriptor<T> AppendValueDescriptor(in Command command) =>
AppendValueDescriptor(command, _name, _parseArgument);
}
using System.CommandLine.Binding;
namespace System.CommandLine;
public sealed class CommandHandler2Builder<T1, T2> : CommandHandlerBuilderBase
{
private readonly CommandHandler1Builder<T1> _commandBuilder;
private readonly string _name;
internal CommandHandler2Builder(CommandHandler1Builder<T1> commandBuilder, string name, ArgType argType)
: base(argType)
{
_commandBuilder = commandBuilder;
_name = name;
}
public Command Build(Func<T1, T2, Task> handler)
{
var command = new Command(_commandBuilder.CommandBuilder.Name, _commandBuilder.CommandBuilder.Description);
var value1 = _commandBuilder.AppendValueDescriptor(command);
var option2 = AppendValueDescriptor(command);
command.SetHandler(handler, value1, option2);
return command;
}
private IValueDescriptor<T2> AppendValueDescriptor(in Command command) =>
AppendValueDescriptor<T2>(command, _name);
}
Would it be something that you can be interested in? If yes, I might work on this feature and prepare a PR.
Funny, I just implemented the following classes in my own solution: CommandExtensions which contains extensions for Command and RootCommand to fluidly build up options, arguments, subcommands, and directives (for the RootCommand), as well as an OptionExtensions and ArgumentExtensions classes. And I was just thinking I should pull those into a separate project that could be uploaded as System.CommandLine.FluentExtensions NuGet package.
I have another project using an older version of System.CommandLine (2.0.0-beta4) that I now need to update, and it uses the GenericHostBuilder pattern, along with Dependency Injection. So, that will be fun to migrate to 😒. So I'd also like to come up with some patterns for more easily scaffolding a GenericHost application complete with command line handling. On the plus side, while I don't yet know how Dependency Injection will work with the command actions, I do think that the just-released v2.0.0 of System.CommandLine will make it easier, over all, to scaffold an application. It just requires a different way of thinking about how that's done compared to the previous version of the library.