diff --git a/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs b/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs new file mode 100644 index 0000000000..6191715e30 --- /dev/null +++ b/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs @@ -0,0 +1,183 @@ +using System.CommandLine.Binding; +using System.CommandLine.Builder; +using System.CommandLine.Invocation; +using System.CommandLine.IO; +using System.CommandLine.Parsing; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Xunit; + + +namespace System.CommandLine.Hosting.Tests +{ + public static class HostingHandlerTest + { + + [Fact] + public static async Task Constructor_Injection_Injects_Service() + { + var service = new MyService(); + + var parser = new CommandLineBuilder( + new MyCommand() + ) + .UseHost((builder) => { + builder.ConfigureServices(services => + { + services.AddTransient(x => service); + }) + .UseCommandHandler(); + }) + .Build(); + + var result = await parser.InvokeAsync(new string[] { "--int-option", "54"}); + + service.Value.Should().Be(54); + } + + [Fact] + public static async Task Parameter_is_available_in_property() + { + var parser = new CommandLineBuilder(new MyCommand()) + .UseHost(host => + { + host.ConfigureServices(services => + { + services.AddTransient(); + }) + .UseCommandHandler(); + }) + .Build(); + + var result = await parser.InvokeAsync(new string[] { "--int-option", "54"}); + + result.Should().Be(54); + } + + [Fact] + public static async Task Can_have_diferent_handlers_based_on_command() + { + var root = new RootCommand(); + + root.AddCommand(new MyCommand()); + root.AddCommand(new MyOtherCommand()); + var parser = new CommandLineBuilder(root) + .UseHost(host => + { + host.ConfigureServices(services => + { + services.AddTransient(_ => new MyService() + { + Action = () => 100 + }); + }) + .UseCommandHandler() + .UseCommandHandler(); + }) + .Build(); + + var result = await parser.InvokeAsync(new string[] { "mycommand", "--int-option", "54" }); + + result.Should().Be(54); + + result = await parser.InvokeAsync(new string[] { "myothercommand", "--int-option", "54" }); + + result.Should().Be(100); + } + + [Fact] + public static async Task Can_bind_to_arguments_via_injection() + { + var service = new MyService(); + var cmd = new RootCommand(); + cmd.AddCommand(new MyOtherCommand()); + var parser = new CommandLineBuilder(cmd) + .UseHost(host => + { + host.ConfigureServices(services => + { + services.AddSingleton(service); + }) + .UseCommandHandler(); + }) + .Build(); + + var result = await parser.InvokeAsync(new string[] { "myothercommand", "TEST" }); + + service.StringValue.Should().Be("TEST"); + } + + public class MyCommand : Command + { + public MyCommand() : base(name: "mycommand") + { + AddOption(new Option("--int-option")); // or nameof(Handler.IntOption).ToKebabCase() if you don't like the string literal + } + + public class MyHandler : ICommandHandler + { + private readonly MyService service; + + public MyHandler(MyService service) + { + this.service = service; + } + + public int IntOption { get; set; } // bound from option + public IConsole Console { get; set; } // bound from DI + + public Task InvokeAsync(InvocationContext context) + { + service.Value = IntOption; + return Task.FromResult(IntOption); + } + } + } + + public class MyOtherCommand : Command + { + public MyOtherCommand() : base(name: "myothercommand") + { + AddOption(new Option("--int-option")); // or nameof(Handler.IntOption).ToKebabCase() if you don't like the string literal + AddArgument(new Argument("One")); + } + + public class MyHandler : ICommandHandler + { + private readonly MyService service; + + public MyHandler(MyService service) + { + this.service = service; + } + + public int IntOption { get; set; } // bound from option + public IConsole Console { get; set; } // bound from DI + + public string One { get; set; } + + public Task InvokeAsync(InvocationContext context) + { + service.Value = IntOption; + service.StringValue = One; + return Task.FromResult(service.Action?.Invoke() ?? 0); + } + } + } + + public class MyService + { + public Func Action { get; set; } + + public int Value { get; set; } + + public string StringValue { get; set; } + } + } +} diff --git a/src/System.CommandLine.Hosting.Tests/HostingTests.cs b/src/System.CommandLine.Hosting.Tests/HostingTests.cs index c827befa35..73f9c9c1cf 100644 --- a/src/System.CommandLine.Hosting.Tests/HostingTests.cs +++ b/src/System.CommandLine.Hosting.Tests/HostingTests.cs @@ -232,5 +232,30 @@ private class MyOptions { public int MyArgument { get; set; } } + + private class MyService + { + public int SomeValue { get; set; } + } + + private class CommandExecuter + { + public CommandExecuter(MyService service) + { + Service = service; + } + + public MyService Service { get; } + + public void Execute(int myArgument) + { + Service.SomeValue = myArgument; + } + + public void SubCommand(int myArgument) + { + Service.SomeValue = myArgument; + } + } } } diff --git a/src/System.CommandLine.Hosting/HostingExtensions.cs b/src/System.CommandLine.Hosting/HostingExtensions.cs index a98cb680e8..b128e04c01 100644 --- a/src/System.CommandLine.Hosting/HostingExtensions.cs +++ b/src/System.CommandLine.Hosting/HostingExtensions.cs @@ -81,5 +81,40 @@ public static OptionsBuilder BindCommandLine( modelBinder.UpdateInstance(opts, bindingContext); }); } + + public static IHostBuilder UseCommandHandler(this IHostBuilder builder) + where TCommand : Command + where THandler : ICommandHandler + { + return builder.UseCommandHandler(typeof(TCommand), typeof(THandler)); + } + + public static IHostBuilder UseCommandHandler(this IHostBuilder builder, Type commandType, Type handlerType) + { + if (!typeof(Command).IsAssignableFrom(commandType)) + { + throw new ArgumentException($"{nameof(commandType)} must be a type of {nameof(Command)}", nameof(handlerType)); + } + + if (!typeof(ICommandHandler).IsAssignableFrom(handlerType)) + { + throw new ArgumentException($"{nameof(handlerType)} must implement {nameof(ICommandHandler)}", nameof(handlerType)); + } + + if (builder.Properties[typeof(InvocationContext)] is InvocationContext invocation + && invocation.ParseResult.CommandResult.Command is Command command + && command.GetType() == commandType) + { + invocation.BindingContext.AddService(handlerType, c => c.GetService().Services.GetService(handlerType)); + builder.ConfigureServices(services => + { + services.AddTransient(handlerType); + }); + + command.Handler = CommandHandler.Create(handlerType.GetMethod(nameof(ICommandHandler.InvokeAsync))); + } + + return builder; + } } } diff --git a/src/System.CommandLine/Invocation/ModelBindingCommandHandler.cs b/src/System.CommandLine/Invocation/ModelBindingCommandHandler.cs index d4a2665174..a2296ce216 100644 --- a/src/System.CommandLine/Invocation/ModelBindingCommandHandler.cs +++ b/src/System.CommandLine/Invocation/ModelBindingCommandHandler.cs @@ -65,8 +65,14 @@ public async Task InvokeAsync(InvocationContext context) object result; if (_handlerDelegate is null) { - var invocationTarget = _invocationTarget ?? - _invocationTargetBinder?.CreateInstance(bindingContext); + var invocationTarget = _invocationTarget ?? + bindingContext.ServiceProvider.GetService(_handlerMethodInfo!.DeclaringType); + if(invocationTarget is { }) + { + _invocationTargetBinder?.UpdateInstance(invocationTarget, bindingContext); + } + + invocationTarget ??= _invocationTargetBinder?.CreateInstance(bindingContext); result = _handlerMethodInfo!.Invoke(invocationTarget, invocationArguments); } else