Tyrrrz/CliFx

Add support for running under Microsoft.Extensions.Hosting.Host

Closed this issue · 7 comments

Add support for configuring and launching the application under the .NET Generic Host.

Currently, this can be achieved in a sort of hacky way:

public static Task<int> Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();
    return new CliApplicationBuilder()
        .AddCommandsFromThisAssembly()
        .UseCommandFactory(schema => (ICommand)host.Services.GetRequiredService(schema.Type))
        .Build()
        .RunAsync(args);
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        services.AddSingleton<IConsole, SystemConsole>();
        var typesThatImplementICommand = typeof(Program).Assembly.GetTypes().Where(x => typeof(ICommand).IsAssignableFrom(x));
        foreach (var t in typesThatImplementICommand)
            services.AddTransient(t);
    })
    .UseConsoleLifetime();

I envision this as something like

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
    .ConfigureAsCliApplication()
    .ConfigureServices((hostContext, services) =>
    {
        // Add custom services here
    })
    .UseConsoleLifetime();

Then running it by either calling the existing Build().RunAsync() or .RunConsoleAsync() on IHostBuilderor adding a new.RunAsCliApplicationAsync()on eitherIHostBuilderorIHost` (or similar).

Can you also give an example use case where generic host could be helpful?

Closing as I don't see much value in this. The only benefit of the generic host is the DI, which is trivial to add as it is. Other features like logging, configuration, etc are not really relevant to CliFx.

I think I'd like to second this idea. Although it's feasible to set DI up, I guess the question people will always have is whether the example from @thegreatco is the best approach.

I wouldn't worry so much about logging, configuration and others being the goal here, so much as just having a nice official way to add support for command line arguments to worker projects.

There is an easier way to set up DI without IHost, it's covered in the readme. But if you want to use IHost then something like the example above will work. Although I'm not entirely sure what's the benefit.

Well, the benefit is that your library makes it really nice to parse command line arguments, and people authoring worker projects would like to use that.

It's less about what clifx would get in this scenario, so much as just having an official way/some guidance to use this library in worker projects.

Unfortunately I haven't done that myself, so can't advise on what's the best solution.

I would like to use BackgroundService to integrate CliFx and I think this approach is flexible. Here is an example and I'm writing a library Modulight.Modules.CommandLine based on CliFx in this way to support hosting.

public class DefaultCommand : ICommand
{
    public async ValueTask ExecuteAsync(IConsole console)
    {
        await console.Output.WriteLineAsync("Hello world!");
    }
}

static class Program
{
    public static async Task Main(string[] args) => await CreateHostBuilder(args).Build().RunAsync();

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                services.AddSingleton<DefaultCommand>();
                services.AddHostedService<Worker>();
            })
            .UseConsoleLifetime();
}

class Worker : BackgroundService
{
    public Worker(IServiceProvider services, IHostApplicationLifetime lifetime)
    {
        Services = services;
        Lifetime = lifetime;
    }

    IServiceProvider Services { get; }

    IHostApplicationLifetime Lifetime { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var exitCode = await new CliApplicationBuilder()
            .AddCommandsFromThisAssembly()
            .UseTypeActivator(Services.GetService)
            .Build()
            .RunAsync();

        Environment.ExitCode = exitCode;

        Lifetime.StopApplication();
    }
}