-
Notifications
You must be signed in to change notification settings - Fork 382
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
Integration with GenericHost #440
Comments
It's been brought up a couple of times but not looked at closely. The service registration in |
@natemcmaster's fork of CommandLineUtils has integration with generic hosts: https://natemcmaster.github.io/CommandLineUtils/docs/advanced/generic-host.html The highlights are:
|
I concluded in #533 that adding the generic can neatly be done as an Invocation Middleware. This means that command-handlers can accept parameters of type Actually there are some things to be said for separating the command-line parser services from the host. Take a look at #533 where I have done that, but provided the ability to bridge that separation if necessary. The approach is similar to how the generic host separates host configuration and app configuration. I.e. the generic host actually first builds a Configuration for the host, and then injects that into the application DI-container. I create the Host and start it, and then add it to the invocation binding context. If you do the Generic Host as a command-line invocation middleware, you still get the benefits of being able to do |
@couven92 Now you just have to get your branch to pass the CI build. :) |
@couven92 That is basically what I ended up doing as well - just not as well organized. Good job! 😄 |
#533 is now merged, so now you can use |
@couven92 Besides using .NET Core 3, and the latest command line nuget packages, what code changes need to be made to the generic host sample app for this integration to work? I tried to figure it out on my own, but ran out of time. I'm a little confused about what to do. |
@alexdresko checkout https://github.com/couven92/dotnet-command-line-api/tree/hosting-sample/samples/HostedConsole for a small example using .NET Core 3.0 together with the new hosting API. Or checkout using the following git command git clone --depth 1 --branch hosting-sample https://github.com/couven92/dotnet-command-line-api.git -- couven92-dotnet-command-line-api And navigate to the |
Note that |
After going down the rabbit hole, it seems that the Will you tackle this matter as part of #2341? Back story: Once you have real tight integration with Generic hosts System.CommandLine would be the first CLI toolkit to do so. |
I (the original author of System.CommandLine.Hosting) am currently looking into how to best support Host.CreateApplicationBuilder. What you need to do is first acknowledge that command line argument parsing happens before the IHost is created. Therefore, CliCommand creation and parsing cannot be part of the Host DI system. Instead you'll use the parsing result from System.CommandLine to feed into Options instances used by the IHost application like you're used to. But instead of binding from a json file, you bind from the ParseResult. For simple scenarios, there is an extension method BindCommandLine() on the OptionsBuilder. Lastly, you'll want to use CommandHandler.Create in the assignment to your CliCommand.Action property. As an argument to create use a delegate with one argument of type IHost. If you're using hosted services, the IHost should automatically start your service when your command handler is invoked. In that case simply call host.WaitForShutdown in your handler. |
@EugeneKrapivin so the using System.CommandLine;
using System.CommandLine.Hosting;
using System.CommandLine.NamingConventionBinder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
CliRootCommand cliRoot = new()
{
Action = CommandHandler.Create(Run),
};
CliArgument<string> hostedServiceArg = new("myHostedServiceArg");
cliRoot.Arguments.Add(hostedServiceArg);
var cliConfig = new CliConfiguration(cliRoot);
cliConfig.UseHost(Host.CreateDefaultBuilder, host =>
{
host.ConfigureServices((context, services) =>
{
//services.AddOptions<MyHostedServiceOptions>()
// .Configure<ParseResult>((opts, parseResult) =>
// {
// opts.Argument = parseResult.GetValue(hostedServiceArg);
// });
//services.AddHostedService<MyHostedService>();
});
});
return await cliConfig.InvokeAsync(args ?? Array.Empty<string>())
.ConfigureAwait(continueOnCapturedContext: false);
static Task Run(IHost host) => host.WaitForShutdownAsync(); |
@fredrikhr thanks for your swift response :) I saw this example in the Samples folder. // this will register all required components into the host DI container
builder.Services.AddCommandLineCli(root=> {
root.AddCommand<MyCommand>(commandConfig => {...});
// other configs
});
// ...
var host = builder.Build();
host.UseCommandCliService(args); // BackgroundService/HostedService with application lifecycle control in mind (i.e. shutdown host on help handler). On execute this host would parse the args, build the commands from factories feeding from the hosts IServiceProvider...
//...
await host.RunAsync(args); it seems that mainly the restriction is stemming from the btw I'm actually using the v2 beta nugets, I'm guessing those are part of the Powderhouse project? |
@EugeneKrapivin your proposal basically turns the model upside down. I.e. Making System.CommandLine a component of the application, instead of the current model where Hosting is a middleware of System.CommandLine. |
@fredrikhr Well, I do not know all the functionality of the library, I assume I miss a lot of the capabilities =) While having I mean, in my mind the CLI parser and command invoker should be the frontend of the command line process, not the main host. a bit like the minimal APIs/controllers are the frontend into the logic of my services. What functionality you think would not fit well into the "GenericHost first" model? |
@EugeneKrapivin non exhaustive list:
|
I see... seems like making the library work as part of the Generic Host pattern of .NET would be a huge undertaking. Thanks for your time :) |
When it comes to class design, I usually create MyAppCommandDefinition classes (either just one, or one per subcommand) containing the CliCommand, Options and Arguments as fields. Then I create ConfigureHost, ConfigureServies and ConfigureOptions methods. Then I create MyAppCommandRunner classes that are added as services to host DI (and use ctor injection from Host DI). Then, back in the Definition class I add a CommandHandler method, with one IHost argument and then use host.Services.GetRequiredService() to get an instance of the runner for the command and then finally call a Run method on the runner. The definition class is simply a container to store the command and its options and arguments, this is purely useful for the ConfigureOptions method (or if Options are not used to get the option values inside the runner). Then the runner class is purely a Hosting DI service class and its Run method is what actually runs a command. It works as a lightweight inline IHostedService. A hosted service will run in the background until something triggers the app to shut down. The Runner will run and after the run method returns, the host will shutdown. Pick whatever works for you. If using only hosted services you'll use host.WaitForShutdown in the CommandHandler method. If using a Runner class, you'll wait for the runners run method to complete. |
Sounds like what I came up with eventually, I think. :) public interface ICommand
{
string Description { get; }
string Name { get; }
Command CreateCommand();
} The constructors have all the dependencies required to execute the command public AnalyzeCommand(Channel<ComplianceTestRequest> channel, IPackageFactory packageFactory)
{
_channel = channel;
_packageFactory = packageFactory;
} The create method actually creates the In my builder.Services.AddTransient<ICommand, ListCommand>();
builder.Services.AddTransient<ICommand, AnalyzeCommand>(); and of course all the other dependencies. Note that everything is in the Host DI container. I registered a public CliService(IEnumerable<ICommand> commands, IHostApplicationLifetime applicationLifetime)
{
_commands = commands;
_applicationLifetime = applicationLifetime;
} the protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var root = new RootCommand("compliance analyzer");
foreach (var command in _commands)
{
root.AddCommand(command.CreateCommand());
}
var builder =
new CommandLineBuilder(root)
.UseVersionOption()
.UseHelpBuilder((bindingContext) =>
{
// must call application stop, otherwise it hangs
_applicationLifetime.StopApplication();
return new HelpBuilder(LocalizationResources.Instance);
})
.UseEnvironmentVariableDirective()
.UseParseDirective()
.UseSuggestDirective()
.RegisterWithDotnetSuggest()
.UseTypoCorrections()
.UseParseErrorReporting()
.CancelOnProcessTermination()
.UseExceptionHandler((ex, context) =>
{
AnsiConsole.WriteException(ex);
}, 1);
var app = builder.Build();
await app.InvokeAsync(Environment.GetCommandLineArgs()[1..]);
} yes, I'm using Spectre Console for my output, I tried to hack the My setup is not lightweight as I have multiple http clients, a couple of background hosts that process data, some channels, configs, I gather telemetry and write file logs... so I really need to be Host first and CLI later... |
Are there any plans on how to integrate this with the GenericHost from Microsoft.Extensions.Hosting?
I mean all the functionality the GenericHost provides like the hosting environment, logging, DI, configuration - would still be useful when creating a command-line API.
I saw that you can add your own registrations to the BindingContext.ServiceProvider - but I'm not quite sure if that's the right way to integrate the GenericHost with this library.
The text was updated successfully, but these errors were encountered: