Binding arguments strings to a complex type?
I have a root command with subcommands (verbs).
The subcommands accept 0 .. n strings as arguments -- they're command arguments, not option arguments.
var root = new RootCommand()
{
new Command("foo")
{
new Argument<string[]>("filter", Array.Empty<string>)
}
};
I can create a handler for that which expects string[] as a parameter.
But what I want is:
- The handler should take a more complex user-defined type (named
Filter), as its parameter - The command-line parser should use the
string[]to instantiate that type
I thought that I could do that, using Model Binding -- More complex types -- but I tried and failed (with a run-time exception like "Cannot convert string[] to Filter").
- Is this scenario supported, or is that only (as it says) for "binding
Optionarguments" and not for bindingArguments? - Or if it is supported, is there an example or maybe a unit test of its being done?
As a work-around I can convert/instantiate it myself within the command-handler; but I wanted it done automatically because there are many commands which all expect the same kind of arguments type.
My following explanations are based on System.CommandLine 2.0.0-beta1.21467.1 (as well as 2.0.0-beta1.21462.1). Older versions behave differently. I also cannot tell with authority whether the described behavior is intentional or a bug.
If you use complex model binding, make sure that the argument of your command handler does NOT match an option or argument name. Because if it does, System.CommandLine tries to convert the value from the respective commandline option/argument to the handler argument type (your complex type).
Only if the the handler argument name is different from any commandline option or argument, System.CommandLine will attempt to find constructor parameters or properties in the complex type that match commandline options and arguments.
For example, assuming your complex type is like:
public class MyComplexType
{
public MyComplexType(string[] filter)
{
this.Stuff = filter;
}
public string[] Stuff { get; private set; }
}
Setting up a command handler like this will fail:
fooCmd.Handler = CommandHandler.Create(
(MyComplexType filter) => Console.WriteLine(string.Join(", ", filter.Stuff))
);
because the handler argument name matches the filter string[] commandline argument name, thus System.CommandLine is trying to convert the string array to MyComplexType.
On the other hand, setting up the command handler like this will work (note that the handler argument name is now "foo"):
fooCmd.Handler = CommandHandler.Create(
(MyComplexType foo) => Console.WriteLine(string.Join(", ", foo.Stuff))
);
Since here the handler argument name itself does not match the commandline argument name "filter", System.CommandLine will not attempt a conversion. Rather, it will inspect MyComplexType, find the constructor, find the constructor parameter whose name matches the commandline argument name "filter" and thus be able to instantiate MyComplexType with the value(s) from the commandline.
P.S.: Even if my explanation helps you solve the issue, please keep the issue report here open so that the author of the library has a chance to see and evaluate whether the observed behavior would constitute a bug.
@elgonzo Thanks for your reply. And you're right! And knowing that solves my issue.
I made a "minimal example" posted below.
When I run this example using 2.0.0-beta1.20071.2
-
foo a bas a command-line works correctly -
bar a bas a command-line works correctly -
baz a bfails with the following exception:
Unhandled exception: System.ArgumentNullException: Value cannot be null.
at System.RuntimeType.MakeGenericType(Type[] instantiation)
at System.CommandLine.Binding.ArgumentConverter.ConvertStrings(IArgument argument, Type type, IReadOnlyCollection`1 arguments)
When I run this example using my build of yesterday's version
i.e. this version:
cwellsx@... MINGW64 ~/source/repos/3rdParty/command-line-api (main)
$ git log --oneline
313d87d3 (HEAD -> main, origin/main, origin/HEAD) rename source generator anchor to SetHandler (#1411)
4832a657 Refactor command handler generator (#1407)
Then I get the same result as above i.e. foo and bar succeed, and baz fails, except the exception has a very different call stack like this:
Unhandled exception: System.ArgumentException: Object of type 'System.Collections.Generic.List`1[System.String]' cannot be converted to type 'TestCommandLine.MyComplexType'.
at System.RuntimeType.TryChangeType(Object value, Binder binder, CultureInfo culture, Boolean needsSpecialCast)
at System.RuntimeType.CheckValue(Object value, Binder binder, CultureInfo culture, BindingFlags invokeAttr)
at System.Reflection.MethodBase.CheckArguments(Object[] parameters, Binder binder, BindingFlags invokeAttr, CultureInfo culture, Signature sig)
at System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Delegate.DynamicInvokeImpl(Object[] args)
at System.Delegate.DynamicInvoke(Object[] args)
at System.CommandLine.Invocation.ModelBindingCommandHandler.InvokeAsync(InvocationContext context) in C:\Users\cwells\source\repos\3rdParty\command-line-api\src\System.CommandLine\Invocation\ModelBindingCommandHandler.cs:line 79
Example code
Here's the code I tested:
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
namespace TestCommandLine
{
internal static class Program
{
private static int Main(string[] args)
{
var filter = new Argument<string[]>("filter", Array.Empty<string>);
var rootCommand = new RootCommand
{
new Command("foo", "simplest case, doesn't use MyComplexType")
{
filter
}.WithHandler(CommandHandler.Create<string[]>(filter =>
{
Console.WriteLine($"foo arguments: {string.Join(", ", filter)}");
})),
new Command("bar", "uses MyComplexType with parameter name not matching argument name")
{
filter
}.WithHandler(CommandHandler.Create<MyComplexType>(complex =>
{
Console.WriteLine($"bar arguments: {string.Join(", ", complex.Stuff)}");
})),
new Command("baz", "uses MyComplexType with parameter name matching argument name")
{
filter
}.WithHandler(CommandHandler.Create<MyComplexType>(filter =>
{
Console.WriteLine($"baz arguments: {string.Join(", ", filter.Stuff)}");
}))
};
return rootCommand.Invoke(args);
}
/// <summary>
/// Extension method to support a fluent interface
/// </summary>
/// <remarks>
/// This isn't currently implemented in the package itself but is an open issue:
/// <see href="https://github.com/dotnet/command-line-api/issues/1364" />
/// </remarks>
private static Command WithHandler(this Command command, ICommandHandler handler)
{
command.Handler = handler;
return command;
}
}
public class MyComplexType
{
public MyComplexType(string[] filter)
{
Stuff = filter;
}
public string[] Stuff { get; }
}
}
The name of the parameter of the constructor of the complex type is significant -- it must match the name of the Arguments.
This should be supported and looks like a bug.