JasperFx / oakton

Parsing and Utilities for Command Line Tools in .Net
http://jasperfx.github.io/oakton
Apache License 2.0
308 stars 40 forks source link

Issue when using Custom Command Creator Pattern #76

Open jebright opened 1 year ago

jebright commented 1 year ago

When using the custom command pattern, as described here: https://jasperfx.github.io/oakton/guide/bootstrapping.html#custom-command-creators

The functionality as described in the "Improved Run Command" described here: https://jasperfx.github.io/oakton/guide/host/run.html#improved-run-command does not work. Instead of being able to see your commands or see their usage, you get an error.

For example if I execute this command:

dotnet run -- myconsoleapp

I get this error:

[red]Invalid usage[/] Unhandled exception. System.Reflection.TargetException: Non-static method requires a target. at System.Reflection.MethodBase.ValidateInvokeTarget(Object target) at System.Reflection.RuntimeMethodInfo.InvokeOneParameter(Object obj, BindingFlags invokeAttr, Binder binder, Object parameter, CultureInfo culture) at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture) at System.Reflection.PropertyInfo.SetValue(Object obj, Object value) at Oakton.Parsing.TokenHandlerBase.setValue(Object target, Object value) at Oakton.Argument.Handle(Object input, Queue1 tokens) at Oakton.Help.UsageGraph.<>c__DisplayClass20_0.<BuildInput>b__0(ITokenHandler h) at System.Linq.Enumerable.TryGetFirst[TSource](IEnumerable1 source, Func2 predicate, Boolean& found) at System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable1 source, Func2 predicate) at Oakton.Help.UsageGraph.BuildInput(Queue1 tokens, ICommandCreator creator) at Oakton.CommandFactory.HelpRun(Queue1 queue) at Oakton.CommandFactory.HelpRun(String commandName) at Oakton.CommandFactory.buildRun(Queue1 queue, String commandName) at Oakton.CommandFactory.BuildRun(IEnumerable`1 args) at Oakton.CommandExecutor.ExecuteAsync(String[] args) at Oakton.CommandExecutor.Execute(String[] args) at Program.

$(String[] args) in C:\Users\Joe\source\MyConsoleApp\Program.cs:line 33

dotnet run -- myconsoleapp ?

In addition if you specify the wrong arguments trying to run your console app, you also get a hard error. This command:

dotnet run --myconsoleapp test123

Gives this error:

[red]Error parsing input[/]System.FormatException: The input string 'test123' was not in a correct format. at void System.Number.ThrowOverflowOrFormatException(ParsingStatus status, ReadOnlySpan value, TypeCode type) at int System.Int32.Parse(string s) at object Oakton.Internal.Conversion.Conversions.<>cDisplayClass5_0`1.b0(string x) at bool Oakton.Argument.Handle(object input, Queue tokens) at bool Oakton.Help.UsageGraph.<>cDisplayClass20_0.b0(ITokenHandler h) at TSource System.Linq.Enumerable.TryGetFirst(IEnumerable source, Func<TSource, bool> predicate, out bool found) at TSource System.Linq.Enumerable.FirstOrDefault(IEnumerable source, Func<TSource, bool> predicate) at object Oakton.Help.UsageGraph.BuildInput(Queue tokens, ICommandCreator creator) at CommandRun Oakton.CommandFactory.buildRun(Queue queue, string commandName)

Unhandled exception. System.Reflection.TargetException: Non-static method requires a target. at System.Reflection.MethodBase.ValidateInvokeTarget(Object target) at System.Reflection.RuntimeMethodInfo.InvokeOneParameter(Object obj, BindingFlags invokeAttr, Binder binder, Object parameter, CultureInfo culture) at System.Reflection.RuntimePropertyInfo.SetValue(Object obj, Object value, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture) at System.Reflection.PropertyInfo.SetValue(Object obj, Object value) at Oakton.Parsing.TokenHandlerBase.setValue(Object target, Object value) at Oakton.Argument.Handle(Object input, Queue1 tokens) at Oakton.Help.UsageGraph.<>c__DisplayClass20_0.<BuildInput>b__0(ITokenHandler h) at System.Linq.Enumerable.TryGetFirst[TSource](IEnumerable1 source, Func2 predicate, Boolean& found) at System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable1 source, Func2 predicate) at Oakton.Help.UsageGraph.BuildInput(Queue1 tokens, ICommandCreator creator) at Oakton.CommandFactory.HelpRun(Queue1 queue) at Oakton.CommandFactory.HelpRun(String commandName) at Oakton.CommandFactory.buildRun(Queue1 queue, String commandName) at Oakton.CommandFactory.BuildRun(IEnumerable`1 args) at Oakton.CommandExecutor.ExecuteAsync(String[] args) at Oakton.CommandExecutor.Execute(String[] args) at Program.

$(String[] args) in C:\Users\Joe\source\MyConsoleApp\Program.cs:line 33

As long as I specify the proper parameter, Oakton does work. It is just when I try to take advantage of the improved run command to see how the command/inputs are supposed to be structured or if I make a mistake on the input parameters, Oakton seems to crash hard.

I am using version 6.0.0 of Oakton and a .NET 7 console app. My program.cs looks like this:

using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Oakton; using Oakton.Commands; using System.Reflection;

using IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { services.AddTransient(); services.AddTransient(); }) //.ConfigureAppConfiguration((hostingContext, config) => { }) .Build();

var executor = CommandExecutor.For(commandFactory => { commandFactory.RegisterCommands(typeof(Program).GetTypeInfo().Assembly); } , new DiCommandCreator(host.Services) ); return executor.Execute(args);

Any my DiCommandCreator class looks like this:

public class DiCommandCreator : ICommandCreator
{
    private readonly IServiceProvider serviceProvider;

    public DiCommandCreator(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }

    public IOaktonCommand CreateCommand(Type commandType)
    {
        return (IOaktonCommand)serviceProvider.GetService(commandType);
    }

    public object CreateModel(Type modelType)
    {
        return serviceProvider.GetService(modelType);
    }
}

I dont think it is anything wrong per se with my command or input (which is just a simple class with one integer property) because if I switch over to not use dependency injection and just use the "RunOaktonCommandsSynchronously" approach, everything works.

return Host.CreateDefaultBuilder(args) .RunOaktonCommandsSynchronously(args);

Seems like when using the custom command pattern, Oakton just does not work as expected? I need to use IOC in this project and hoping there may be some way to address these issues. Thank you.

jeremydmiller commented 1 year ago

Hey, sorry, honestly just seeing this. The idiomatic way now is to just spin up the IHost through a NetInput and use the application's main container. Would that help? That's all I use Oakton for at this point.