Skip to content

Commit fed6c16

Browse files
authored
Easier addition of injection friendly handler (#671)
* Easier addition of injection friendly handler * Using command inheritance to indicate handler * adding call to BindInvocationHandler in hosting setup * Making HandlerType nullable * Registering the commandhandler via IHostBuilder * Throw exception on bad arguments * Propper order of checking implemented types * Testing for Argument binding * Attempting to resolve handler instance using serviceProvider * Async hosting tests * nicer not null check * Putting testing logic into test instead of helpers
1 parent 08d0408 commit fed6c16

File tree

4 files changed

+251
-2
lines changed

4 files changed

+251
-2
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
using System.CommandLine.Binding;
2+
using System.CommandLine.Builder;
3+
using System.CommandLine.Invocation;
4+
using System.CommandLine.IO;
5+
using System.CommandLine.Parsing;
6+
using System.Linq;
7+
using System.Threading.Tasks;
8+
using FluentAssertions;
9+
10+
using Microsoft.Extensions.Configuration;
11+
using Microsoft.Extensions.DependencyInjection;
12+
using Microsoft.Extensions.Hosting;
13+
using Microsoft.Extensions.Options;
14+
using Xunit;
15+
16+
17+
namespace System.CommandLine.Hosting.Tests
18+
{
19+
public static class HostingHandlerTest
20+
{
21+
22+
[Fact]
23+
public static async Task Constructor_Injection_Injects_Service()
24+
{
25+
var service = new MyService();
26+
27+
var parser = new CommandLineBuilder(
28+
new MyCommand()
29+
)
30+
.UseHost((builder) => {
31+
builder.ConfigureServices(services =>
32+
{
33+
services.AddTransient(x => service);
34+
})
35+
.UseCommandHandler<MyCommand, MyCommand.MyHandler>();
36+
})
37+
.Build();
38+
39+
var result = await parser.InvokeAsync(new string[] { "--int-option", "54"});
40+
41+
service.Value.Should().Be(54);
42+
}
43+
44+
[Fact]
45+
public static async Task Parameter_is_available_in_property()
46+
{
47+
var parser = new CommandLineBuilder(new MyCommand())
48+
.UseHost(host =>
49+
{
50+
host.ConfigureServices(services =>
51+
{
52+
services.AddTransient<MyService>();
53+
})
54+
.UseCommandHandler<MyCommand, MyCommand.MyHandler>();
55+
})
56+
.Build();
57+
58+
var result = await parser.InvokeAsync(new string[] { "--int-option", "54"});
59+
60+
result.Should().Be(54);
61+
}
62+
63+
[Fact]
64+
public static async Task Can_have_diferent_handlers_based_on_command()
65+
{
66+
var root = new RootCommand();
67+
68+
root.AddCommand(new MyCommand());
69+
root.AddCommand(new MyOtherCommand());
70+
var parser = new CommandLineBuilder(root)
71+
.UseHost(host =>
72+
{
73+
host.ConfigureServices(services =>
74+
{
75+
services.AddTransient<MyService>(_ => new MyService()
76+
{
77+
Action = () => 100
78+
});
79+
})
80+
.UseCommandHandler<MyCommand, MyCommand.MyHandler>()
81+
.UseCommandHandler<MyOtherCommand, MyOtherCommand.MyHandler>();
82+
})
83+
.Build();
84+
85+
var result = await parser.InvokeAsync(new string[] { "mycommand", "--int-option", "54" });
86+
87+
result.Should().Be(54);
88+
89+
result = await parser.InvokeAsync(new string[] { "myothercommand", "--int-option", "54" });
90+
91+
result.Should().Be(100);
92+
}
93+
94+
[Fact]
95+
public static async Task Can_bind_to_arguments_via_injection()
96+
{
97+
var service = new MyService();
98+
var cmd = new RootCommand();
99+
cmd.AddCommand(new MyOtherCommand());
100+
var parser = new CommandLineBuilder(cmd)
101+
.UseHost(host =>
102+
{
103+
host.ConfigureServices(services =>
104+
{
105+
services.AddSingleton<MyService>(service);
106+
})
107+
.UseCommandHandler<MyOtherCommand, MyOtherCommand.MyHandler>();
108+
})
109+
.Build();
110+
111+
var result = await parser.InvokeAsync(new string[] { "myothercommand", "TEST" });
112+
113+
service.StringValue.Should().Be("TEST");
114+
}
115+
116+
public class MyCommand : Command
117+
{
118+
public MyCommand() : base(name: "mycommand")
119+
{
120+
AddOption(new Option<int>("--int-option")); // or nameof(Handler.IntOption).ToKebabCase() if you don't like the string literal
121+
}
122+
123+
public class MyHandler : ICommandHandler
124+
{
125+
private readonly MyService service;
126+
127+
public MyHandler(MyService service)
128+
{
129+
this.service = service;
130+
}
131+
132+
public int IntOption { get; set; } // bound from option
133+
public IConsole Console { get; set; } // bound from DI
134+
135+
public Task<int> InvokeAsync(InvocationContext context)
136+
{
137+
service.Value = IntOption;
138+
return Task.FromResult(IntOption);
139+
}
140+
}
141+
}
142+
143+
public class MyOtherCommand : Command
144+
{
145+
public MyOtherCommand() : base(name: "myothercommand")
146+
{
147+
AddOption(new Option<int>("--int-option")); // or nameof(Handler.IntOption).ToKebabCase() if you don't like the string literal
148+
AddArgument(new Argument<string>("One"));
149+
}
150+
151+
public class MyHandler : ICommandHandler
152+
{
153+
private readonly MyService service;
154+
155+
public MyHandler(MyService service)
156+
{
157+
this.service = service;
158+
}
159+
160+
public int IntOption { get; set; } // bound from option
161+
public IConsole Console { get; set; } // bound from DI
162+
163+
public string One { get; set; }
164+
165+
public Task<int> InvokeAsync(InvocationContext context)
166+
{
167+
service.Value = IntOption;
168+
service.StringValue = One;
169+
return Task.FromResult(service.Action?.Invoke() ?? 0);
170+
}
171+
}
172+
}
173+
174+
public class MyService
175+
{
176+
public Func<int> Action { get; set; }
177+
178+
public int Value { get; set; }
179+
180+
public string StringValue { get; set; }
181+
}
182+
}
183+
}

src/System.CommandLine.Hosting.Tests/HostingTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,5 +232,30 @@ private class MyOptions
232232
{
233233
public int MyArgument { get; set; }
234234
}
235+
236+
private class MyService
237+
{
238+
public int SomeValue { get; set; }
239+
}
240+
241+
private class CommandExecuter
242+
{
243+
public CommandExecuter(MyService service)
244+
{
245+
Service = service;
246+
}
247+
248+
public MyService Service { get; }
249+
250+
public void Execute(int myArgument)
251+
{
252+
Service.SomeValue = myArgument;
253+
}
254+
255+
public void SubCommand(int myArgument)
256+
{
257+
Service.SomeValue = myArgument;
258+
}
259+
}
235260
}
236261
}

src/System.CommandLine.Hosting/HostingExtensions.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,40 @@ public static OptionsBuilder<TOptions> BindCommandLine<TOptions>(
8181
modelBinder.UpdateInstance(opts, bindingContext);
8282
});
8383
}
84+
85+
public static IHostBuilder UseCommandHandler<TCommand, THandler>(this IHostBuilder builder)
86+
where TCommand : Command
87+
where THandler : ICommandHandler
88+
{
89+
return builder.UseCommandHandler(typeof(TCommand), typeof(THandler));
90+
}
91+
92+
public static IHostBuilder UseCommandHandler(this IHostBuilder builder, Type commandType, Type handlerType)
93+
{
94+
if (!typeof(Command).IsAssignableFrom(commandType))
95+
{
96+
throw new ArgumentException($"{nameof(commandType)} must be a type of {nameof(Command)}", nameof(handlerType));
97+
}
98+
99+
if (!typeof(ICommandHandler).IsAssignableFrom(handlerType))
100+
{
101+
throw new ArgumentException($"{nameof(handlerType)} must implement {nameof(ICommandHandler)}", nameof(handlerType));
102+
}
103+
104+
if (builder.Properties[typeof(InvocationContext)] is InvocationContext invocation
105+
&& invocation.ParseResult.CommandResult.Command is Command command
106+
&& command.GetType() == commandType)
107+
{
108+
invocation.BindingContext.AddService(handlerType, c => c.GetService<IHost>().Services.GetService(handlerType));
109+
builder.ConfigureServices(services =>
110+
{
111+
services.AddTransient(handlerType);
112+
});
113+
114+
command.Handler = CommandHandler.Create(handlerType.GetMethod(nameof(ICommandHandler.InvokeAsync)));
115+
}
116+
117+
return builder;
118+
}
84119
}
85120
}

src/System.CommandLine/Invocation/ModelBindingCommandHandler.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,14 @@ public async Task<int> InvokeAsync(InvocationContext context)
6565
object result;
6666
if (_handlerDelegate is null)
6767
{
68-
var invocationTarget = _invocationTarget ??
69-
_invocationTargetBinder?.CreateInstance(bindingContext);
68+
var invocationTarget = _invocationTarget ??
69+
bindingContext.ServiceProvider.GetService(_handlerMethodInfo!.DeclaringType);
70+
if(invocationTarget is { })
71+
{
72+
_invocationTargetBinder?.UpdateInstance(invocationTarget, bindingContext);
73+
}
74+
75+
invocationTarget ??= _invocationTargetBinder?.CreateInstance(bindingContext);
7076
result = _handlerMethodInfo!.Invoke(invocationTarget, invocationArguments);
7177
}
7278
else

0 commit comments

Comments
 (0)