-
Notifications
You must be signed in to change notification settings - Fork 383
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
When using UseCommandHandler with subcomand, Help overrides RootCommand #1344
Comments
Because you haven't assigned the |
Hey @jonsequitur ! appreciate the answer! The thing is, I wanna instantiate my handler using DI, that's why I use the As I understand it, using EDIT: BTW, since I have your attention if you happen to click the links and see something being used incorrectly please do let me know.. Thank you for your time! |
Oh, sorry I missed that. Because the @fredrikhr, thoughts on this? |
Hmm, looking at the source for command-line-api/src/System.CommandLine.Hosting/HostingExtensions.cs Lines 85 to 118 in a2ea4b1
Instead, I'd create an var myCommand = new MyCommand()
{
Handler = CommandHandler.Create(
(InvocationContext context, IHost host) =>
{
var commandHandler = host.Services.GetRequiredService<MyCommandHandler>();
return commandHandler.Invoke(context);
}
}; that should enable you to use your Handler from DI, while still maintaining the Command for Parsing outside of Hosting. The problem is that configuring the Command line argument parser happens outside the Generic Host, it's the parser that ultimately creates the host, and that happens after the command-line has already been parsed. |
I think it works by registering it against the internal's service provider here:
I would assume that handler's are discovered from that service provider, and instantiated if they are not there?
Anyways, I think this is a very interesting topic. My goal was to find a way to inject services into command, and be able to override them for testing purposes. So I have this "builder" that allows me to ConfigureServices like one would do one the IHostBuilder: Then I can do something among these lines:
So my commands can look like this:
I think I don't need the In order to make it work with the RootCommand, I had to hard-code the handler on the command's definition I feel like this should be a common requirement. Ideally, and thinking out loud, I think it would be nice if the BindingContext would use a normal IServiceProvider that we can customize so that we can add and override services for the entire application. |
It seems like the Unfortunately for this senario the To work around the limitation what @fredrikhr suggested should work. |
Hi @Gronex ! I understand. It's basically not gonna work for the RootCommand as it is right now. Do you guys think we need to figure a way to improve how the lib deals with DI? I'm not really talking about generic host integration. I think that adding/overriding services should be an easy task to do. |
Hmm, I think we should redesign |
Does it even have to be connected to the Hosted environment? The idea is to easily register the handler so a DI framework could inject the appropriate services into it, that does not strictly need to rely on the host. If it does not it would definately feel better to streamline the DI so there is only one in play even when using the GenericHost |
@Gronex I definitively agree with everything you just said. I believe we should focus on improving DI support internally, independently of the Hosting package. This should in turn simplify stuff for generic host integration. We should aim at using Regarding the Hosting integration, TBH I would have expected to see the opposite: CommandLine integrating into the GenericHost, not the other way around. What I'm saying is, I would have expected this: HostBulder.Create()
.ConfigureAppConfiguration...
.ConfigureServices...
.ConfigureCommandLineDefaults // Instead of calling ConfigureWebHostDefaults
.Build() I would have expected the CLI to be a feature of the Host, not the other way around. I would be happy to work on a proof of concept if you guys think this could be something we need to explore. |
I agree, however the problem is that we have two separate DI-systems here (the one from the Therefore, the late-bound DI-from-Generic-host injected CommandHandler needs to be wrapped as I showed #1344 (comment)
I do understand that sentiment. Personally, I prefer it the way it is now, but in my opinion we should have it both ways. I see this as two perpendicular lines that cross each other at an intersection. Currently we only have a small bypass ramp that allows going from one line to the other in one direction. Ideally we'd like a cloverleaf-intersection of these two features. So when I originally designed the Sys.CommandLine.Hosting glue, I admit it was easier to make System.CommandLine wrap around the Generic Host than the other way round. However consider the following snippet from ASP.NET (Program.cs) public static class Program
{
public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureServices(services => { /* add services here */ });
webBuilder.Configure((context, app) => { /* configure HTTP pipeline here */ });
});
} You see here that the code in Now using public static class Program
{
public static void Main(string[] args)
{
var rootCommand = new RootCommand { Handler = new MyRootCommandHandler() };
var parser = new CommandLineBuilder(rootCommand)
.UseDefaults()
.UseHost(CreateHostBuilder)
.Build();
parser.Invoke(args);
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureServices(services => { /* add services here */ });
webBuilder.Configure((context, app) => { /* configure HTTP pipeline here */ });
});
}
public class MyRootCommandHandler : ICommandHandler
{
public Task<int> InvokeAsync(InvocationContext context)
{
var host = context.GetHost();
/* Do something ... */
return Task.FromResult(0);
}
} So generally these two code-snippets are really similar. Now your suggestion of having |
Hey! Appreciate you guys taking the time to analyze this!
So..Is there a way we can MSDI for everything? I know that we are now binding CommandLine Options to the handler's properties. Perhaps we can use inject IOptions for that? Does this make sense? I'm hoping the learning curve for building a CLI can be lowered if we integrate more with the .NET ecosystem. (MSDI, IOptions) I think doing that will leave us closer for having a deeper integration with generic host.
Sure thing, both ways can't hurt. That being said, I think we should have a stronger integration with GenericHost. Being able to share services between the two by default seems like what most people would expect. That being said, I was thinking on something like this: public static class Program
{
public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices(...)
.ConfigureAppConfiguration(...)
.ConfigureCommandLineDefaults<MyRootCommand>(args)
// or
.ConfigureCommandLineDefaults<MyRootCommand>(args, builder=> ... )
}
I think we can require them as an argument on the HostBuilder extension method and register a singleton or factory service which contains them. public IHostBuilder ConfigureCommandLineDefaults<TRootCommand>(
this IHostBuilder hostBuilder,
args: string[], Action<CommandLineBuilder>? fn = null) {
hostBuilder.ConfigureServices(services=> {
services.AddSingleton<CommandLineArguments>(()=> new CommandLineArguments(args));
});
// configure the builder to use MSDI
var builder = new CommandLineBuilder<TRootCommand>();
fn?.Invoke(builder)
return hostBuilder;
}
I didn't know about the That being said,
This is completely true. You can configure some of the Host stuff on the HostBuilder extension method, but once the host is running and you are in the context of a HostedService, yes you are done, no more changing the host. I know this is not easy and getting closer to this would require huge changes on how instantiation is being done. Thank you all! |
I feel my two code snippets clearly show how similar the code is compared between ASP.NET and System.CommandLine. Could you elaborate on what you think is so dissimilar there that it confuses people? I also see ModelBinding in commandline as completely different from ctor injection in Ms.Ext.DI. I personally prefer these to be separate. And given the alomost identical code as shown in my sample, I feel Sys.CommandLine.Hosting does what you are asking for: A tight integration between CommandLine and Generic Host. Yes, the Host.CreateDefaultBuilder does accept an array of strings (taken from the command-line arguments), but the only supported arguments are --config-key=value. If you ever supply sub-commands or option values it throws. You also only get the value as a string since it's put into IConfiguration which is purely string key-value-based. That is also the reason why the extension methods to the HostBuilder are problematic: Host.CreateDefaultBuilder fully consumes the passed arguments, there is no way to somehow pass them along the way during the subsequent configure-extensions. The |
I agree with you. They are very similar. However, if CommandLine would follow the same approach as ASP.NET and other frameworks, which configure the HostBuilder, I think it would be more in line with what the rest of the ecosystem is doing. That's all I'm saying.
I can only speak for my self, but personally, my confusion was not is in the HostBuilder configuration, I mean that's just reading the docs and configuring it properly. That being said, since I know the HostBuilder API, I was expecting to add the CommandLine to the HostBuilder, not the other way around. But reading the docs quickly clarifies this. It was just my first instinct, having built stuff that integrates with HostBuilder before. Personally, it took me a while to figure out the DI situation, specially when working with the GenericHost. So, as a conclusion for the DI perspective:
I think that's useful for all CLIs, like the dotnet-affected tool I've written, where we wanna mock some of the stuff we inject in handlers. Regarding GenericHost:
I think this is more useful for enterprises which have built infrastructure on-top of GenericHost which is (partially) independent from ASP.NET All this being said, I think that these would be some cool features!
Personally, I'm more interested in 1 and 2, cause I can probably write something that works (for my particular use case) to resolve 3, but I would need to have 1 and 2 supported by CommandLine if I'm even gonna think about it. I just wanted to start the conversation.. I think you have very valid points, and I think I have expressed mines as clearly as possible, that being said, I'm happy to help and to provide more information about my use case if required. |
@leonardochaia Now I am really confused. You describe it as if the Host in ASP.NET and the Generic Host were two separate things, whereas they are (they used to be, but now they are 100% identical). ASP.NET simply registers some services (e.g. Kestrel) and that's it, there is nothing being done differently in ASP.NET compared to any other app using the Generic Host. Your statement about something built on Generic Host that is independant from ASP.NET does not make sense. There is no way to be dependant on ASP.NET itself, unless you mean implementing an MVC controller (or something similar that somehow uses As I showed in my example snippets both the ASP.NET and System.CommandLine.Hosting code has a prelude in which you handle command-line arguments. In ASP.NET that's a one-liner in ASP.NET uses the Granted, your use case where you materialise the command-handler from DI is a scenario I never envisioned, but as I showed in my earlier comment that is easily doable without breaking things. But I am working on an extension method that allows to do that as a one-liner, I'll tag you once I make the PR. (edit: Opened PR #1356) |
Okey. Appreciate it. Thank you so much for your time dude. I was just trying to provide my first impressions. So you can now bind IOptions to CommandLine Options, and services registered in generic host can be injected into handlers by default right? Considering the PR closes this issue, do you think I should open a separate issue for the dependency injection stuff without generic host? |
You could always do that.
No, default behaviour has not changed, nor is that desirable (IMO). The PR simply makes it easier to use a Command handler provided by the DI system of the .NET Generic Host. But such a command handler is nothing special. It has to be registered with DI just as you woudl register any other service in DI. And it's constructed by calling
I still don't understand what you are referring to here. But sure, if something is blocking you, create an issue.
This has always been possible, but yeah, the PR would make some nuances of that scenario easier. |
So for the CLI I'm building, there are no hosted services, so no generic host. In my handlers, I inject domain interfaces that have an infrastructure implementation (i.e do git diff). Is there a way to do just that without generichost? EDIT: Just wanted to add that none of these is blocking me. I'm just trying to help out other people that may face my same questions in the future |
var rootCommand = new RootCommand
{
Handler = CommandHandler.Create(
(IMyCustomServiceIUseInMyHandler service) =>
{
/* Do something with service */
});
}; And then just make sure you add
|
So I think that's the tricky part. Would you point me in the right direction for doing that using the CommandLineBuilder? Do I need to write a custom middleware to register the implementation? Say I have this Command and its Handler: internal class MyRootCommand : RootCommand
{
public MyRootCommand()
: base("changes")
{
}
public class CommandHandler : ICommandHandler
{
private readonly IMyCustomServiceIUseInMyHandler _instance;
private readonly IConsole _console;
public CommandHandler(IMyCustomServiceIUseInMyHandler instance, IConsole console)
{
_instance = instance;
_console = console;
}
public Task<int> InvokeAsync(InvocationContext ic)
{
return Task.FromResult(0);
}
}
} How do I provide the RootCommand and it's handler? var builder = new CommandLineBuilder(...) Thank you so much for your detailed answers!! |
var parser = new CommandLineBuilder(rootCommand)
.UseMiddleware((context, next) =>
{
context.BindingContext.AddService(serviceProvider =>
{
/* code that returns a IMyCustomServiceIUseInMyHandler */
});
})
.Build(); You can register the But if you want more than ctor-injection (like filling properties from Command-line args) you'd want to new up a |
Okay, I admit this could also be simplified. This has nothing to do with the generic host, though. Hmm, I'll think about it and maybe open another PR tomorrow |
Awesome!! I would love to have something like this, with an IServiceCollection to register services. var parser = new CommandLineBuilder(rootCommand)
.ConfigureServices((IServiceCollection services)=> {
services.AddTransient<IMyHandlerDependency, MyHandlerDependencyImpl>()
})
.Build(); |
@leonardochaia opened #1358 look at this test case for how your simplified bindingcontext-resolved command handler would look like: command-line-api/src/System.CommandLine.Tests/Invocation/CommandHandlerTests.cs Lines 511 to 526 in d556325
Note the new It makes it look very similar to |
Hey! I like it!! I think it would be nice to add an extension method That will simplify testing handlers for sure. Once that's merged I can ditch my custom implementation in dotnet-affected and give that a shot! |
Hi all,
I love this project. We are using it to build dotnet-affected!
I'm not sure if this is a known issue, but I'm migrating to use the Hosting setup and I'm seeing different behavior between declaring the RootCommand with a Handler, and binding a handler through the HostBuilder.
It only happens when using a subcommand. If I remove the subcommad, it works.
For example:
EDIT: Changing the order of
UseDefaults
does not seem to make any difference.The text was updated successfully, but these errors were encountered: