Description
Over the course of beta 2 and beta 4, a couple of conventions were removed because they'd led to too much confusion. We still haven't found the right balance of simplicity and composability, though. The conversation has come up in a number of different issues, so let's use this one to focus the discussion and refine the design. I'll post a few proposals below.
Recap
-
In beta 2, we removed
CommandHandler.Create
from System.CommandLine (and moved it to System.CommandLine.NamingConventionBinder) because its use of naming convention wasn't obvious, and naming mismatches between option or argument names and their associated handler parameters became the most common source of problems people had while trying to use the API. (It was also a reflection-heavy API and needed to be removed in order to support trimming and NativeAOT compilation.) We introduced theCommand.SetHandler
extension methods as a replacement. -
By beta 4, a problem with a different convention had become apparent in
SetHandler
, which was that itsparams IValueDescriptor[]
parameter, designed to handle binding parameters from either theParseResult
orServiceProvider
, was confusing people as well.SetHandler
could be called without passing any options or arguments to bind to, leading it to try to resolve everything from the service provider, which is the less common case. The fix in beta 4 was to change theparams
parameter to aIValueDescriptor<T>
for each generic parameter. This made code a little less verbose because it plays more nicely with C# type inference. But removing the service provider fallback led to more ceremony for accessing injected objects.
Current SetHandler
problems
1. Verbosity
The most common criticism now of SetHandler
is that it requires too much ceremony to use. The problem is illustrated by this code snippet, which needs to reference option
three times:
var option = new Option<int>("-i"); // 1
var command = new RootCommand
{
option // 2
};
command.SetHandler(i => { }, option); // 3
An API that composes these together is needed, but we hope to avoid too much coupling between the handling and parsing APIs. The parser API defines the grammar with a good deal of flexibility, while the handler API defines how to bind symbols regardless of their position in the parsed input. A convenience method that sets up both at once will likely prevent people from creating specific parser configurations, but as these won't be the majority case, a convenience API is warranted.
2. Service provider access is unclear
In the beta2 version of SetHandler
, parameters of commonly-used types such as ParseResult
, CancellationToken
, and IConsole
would be implicitly bound from the service provider. The beta 4 version requires that the IValueDescriptor<T>
parameter count match the generic type parameter count (e.g. public static void SetHandler<T1, T2>(this Command command, Action<T1, T2> handle, IValueDescriptor<T1> symbol1, IValueDescriptor<T2> symbol2
). This makes it unclear what to pass to get instances of these other types. This can be done by implementing a custom BinderBase<T>
but this also requires too much ceremony.
3. Directly returning exit codes isn't supported (#1570)
A detailed explanation is in the comment here: #1570 (comment)