From 0ebcd512df720eace1ec00c3857421c666cddc96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20H=C3=B8is=C3=A6ther=20Rasch?= Date: Mon, 19 Jul 2021 12:22:52 +0200 Subject: [PATCH 1/2] Add HostedCommandHandler helper class --- .../HostingHandlerTest.cs | 26 +++++++ .../HostedCommandHandler.cs | 68 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/System.CommandLine.Hosting/HostedCommandHandler.cs diff --git a/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs b/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs index 6191715e30..658aa0a428 100644 --- a/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs +++ b/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs @@ -113,6 +113,27 @@ public static async Task Can_bind_to_arguments_via_injection() service.StringValue.Should().Be("TEST"); } + [Fact] + public static void Throws_When_Injected_HandlerType_is_not_ICommandHandler() + { + new object().Invoking(_ => + { + var handlerWrapper = HostedCommandHandler.CreateFromHost( + typeof(MyNonCommandHandler)); + }).Should().ThrowExactly( + because: $"{typeof(MyNonCommandHandler)} does not implement {typeof(ICommandHandler)}" + ); + } + + [Fact] + public static void Throws_When_Injected_HandlerType_is_null() + { + new object().Invoking(_ => + { + var handlerWrapper = HostedCommandHandler.CreateFromHost(null); + }).Should().ThrowExactly(); + } + public class MyCommand : Command { public MyCommand() : base(name: "mycommand") @@ -179,5 +200,10 @@ public class MyService public string StringValue { get; set; } } + + public class MyNonCommandHandler + { + public static int DoSomething() => 0; + } } } diff --git a/src/System.CommandLine.Hosting/HostedCommandHandler.cs b/src/System.CommandLine.Hosting/HostedCommandHandler.cs new file mode 100644 index 0000000000..dae5368be0 --- /dev/null +++ b/src/System.CommandLine.Hosting/HostedCommandHandler.cs @@ -0,0 +1,68 @@ +using System.CommandLine.Hosting; +using System.Threading.Tasks; + +using Microsoft.Extensions.DependencyInjection; + +namespace System.CommandLine.Invocation +{ + /// + /// Proviveds helper methods to initialize a command handler that uses + /// Dependency Injection from the .NET Generic Host to materialize + /// the handler. + /// + /// + public static class HostedCommandHandler + { + private class HostedCommandHandlerWrapper : ICommandHandler + where THostedCommandHandler : ICommandHandler + { + public Task InvokeAsync(InvocationContext context) + { + var host = context.GetHost(); + var handler = host.Services.GetRequiredService(); + return handler.InvokeAsync(context); + } + } + + /// + /// Creates an instance that when invoked + /// will forward the to an instance of + /// obtained from the DI-container + /// of the .NET Generic Host used in the invocation pipeline. + /// + /// A command handler service type implementing that has been registered with the .NET Generic Host DI-container. + /// A wrapper object that implements the interface by forwarding the call to to the implementation of . + /// is . + /// does not implement the interface type. + public static ICommandHandler CreateFromHost(Type commandHandlerType) + { + _ = commandHandlerType ?? throw new ArgumentNullException(nameof(commandHandlerType)); + Type wrapperHandlerType; + try + { + wrapperHandlerType = typeof(HostedCommandHandlerWrapper<>) + .MakeGenericType(commandHandlerType); + } + catch (ArgumentException argExcept) + { + throw new ArgumentException( + paramName: nameof(commandHandlerType), + message: $"{commandHandlerType} does not implement the {typeof(ICommandHandler)} interface.", + innerException: argExcept); + } + return (ICommandHandler)Activator.CreateInstance(wrapperHandlerType); + } + + /// + /// Creates an instance that when invoked + /// will forward the to an instance of + /// obtained from the DI-container + /// of the .NET Generic Host used in the invocation pipeline. + /// + /// A command handler service type that has been registered with the .NET Generic Host DI-container. + /// A wrapper object that implements the interface by forwarding the call to to the implementation of . + public static ICommandHandler CreateFromHost() + where TCommandHandler : ICommandHandler => + new HostedCommandHandlerWrapper(); + } +} \ No newline at end of file From 50b949adff0742e1b3845671c294193f08ef8fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20H=C3=B8is=C3=A6ther=20Rasch?= Date: Mon, 19 Jul 2021 12:23:29 +0200 Subject: [PATCH 2/2] Replace UseCommandHandler with HostedCommandHandler and IOptions --- .../HostingHandlerTest.cs | 101 ++++++++++++------ .../HostingExtensions.cs | 35 ------ 2 files changed, 66 insertions(+), 70 deletions(-) diff --git a/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs b/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs index 658aa0a428..676af7a195 100644 --- a/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs +++ b/src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs @@ -1,16 +1,15 @@ 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; @@ -18,44 +17,51 @@ 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() + new MyCommand { Handler = HostedCommandHandler.CreateFromHost() } ) - .UseHost((builder) => { - builder.ConfigureServices(services => + .UseHost((builder) => + { + builder.ConfigureServices((context, services) => { + services.AddTransient(); + services.AddOptions() + .BindCommandLine(); services.AddTransient(x => service); - }) - .UseCommandHandler(); + }); }) .Build(); - var result = await parser.InvokeAsync(new string[] { "--int-option", "54"}); + var result = await parser.InvokeAsync(new string[] { "--int-option", "54" }); service.Value.Should().Be(54); + result.Should().Be(54); } [Fact] public static async Task Parameter_is_available_in_property() { - var parser = new CommandLineBuilder(new MyCommand()) + var parser = new CommandLineBuilder( + new MyCommand { Handler = HostedCommandHandler.CreateFromHost() } + ) .UseHost(host => { host.ConfigureServices(services => { + services.AddTransient(); + services.AddOptions() + .BindCommandLine(); services.AddTransient(); - }) - .UseCommandHandler(); + }); }) .Build(); - var result = await parser.InvokeAsync(new string[] { "--int-option", "54"}); + var result = await parser.InvokeAsync(new string[] { "--int-option", "54" }); result.Should().Be(54); } @@ -65,20 +71,30 @@ public static async Task Can_have_diferent_handlers_based_on_command() { var root = new RootCommand(); - root.AddCommand(new MyCommand()); - root.AddCommand(new MyOtherCommand()); + root.AddCommand(new MyCommand + { + Handler = HostedCommandHandler.CreateFromHost() + }); + root.AddCommand(new MyOtherCommand + { + Handler = HostedCommandHandler.CreateFromHost() + }); var parser = new CommandLineBuilder(root) .UseHost(host => { host.ConfigureServices(services => { + services.AddTransient(); + services.AddOptions() + .BindCommandLine(); + services.AddTransient(); + services.AddOptions() + .BindCommandLine(); services.AddTransient(_ => new MyService() { Action = () => 100 }); - }) - .UseCommandHandler() - .UseCommandHandler(); + }); }) .Build(); @@ -96,15 +112,20 @@ public static async Task Can_bind_to_arguments_via_injection() { var service = new MyService(); var cmd = new RootCommand(); - cmd.AddCommand(new MyOtherCommand()); + cmd.AddCommand(new MyOtherCommand + { + Handler = HostedCommandHandler.CreateFromHost() + }); var parser = new CommandLineBuilder(cmd) .UseHost(host => { host.ConfigureServices(services => { + services.AddTransient(); + services.AddOptions() + .BindCommandLine(); services.AddSingleton(service); - }) - .UseCommandHandler(); + }); }) .Build(); @@ -141,22 +162,27 @@ 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 MyOptions + { + public int IntOption { get; set; } // bound from option + public IConsole Console { get; set; } // bound from DI + } + public class MyHandler : ICommandHandler { private readonly MyService service; + private readonly MyOptions options; - public MyHandler(MyService service) + public MyHandler(MyService service, IOptions options) { this.service = service; + this.options = options.Value; } - 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); + service.Value = options.IntOption; + return Task.FromResult(options.IntOption); } } } @@ -169,24 +195,29 @@ public MyOtherCommand() : base(name: "myothercommand") AddArgument(new Argument("One")); } + public class MyOptions + { + public int IntOption { get; set; } // bound from option + public IConsole Console { get; set; } // bound from DI + public string One { get; set; } + } + public class MyHandler : ICommandHandler { private readonly MyService service; + private readonly MyOptions options; - public MyHandler(MyService service) + public MyHandler(MyService service, IOptions options) { this.service = service; + this.options = options.Value; } - 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; + service.Value = options.IntOption; + service.StringValue = options.One; return Task.FromResult(service.Action?.Invoke() ?? 0); } } diff --git a/src/System.CommandLine.Hosting/HostingExtensions.cs b/src/System.CommandLine.Hosting/HostingExtensions.cs index faa10a73c1..804602763f 100644 --- a/src/System.CommandLine.Hosting/HostingExtensions.cs +++ b/src/System.CommandLine.Hosting/HostingExtensions.cs @@ -82,41 +82,6 @@ public static OptionsBuilder BindCommandLine( }); } - 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; - } - public static InvocationContext GetInvocationContext(this IHostBuilder hostBuilder) { _ = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder));