Tyrrrz / CliFx

Class-first framework for building command-line interfaces
MIT License
1.5k stars 61 forks source link

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

Closed thegreatco closed 4 years ago

thegreatco commented 5 years ago

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() onIHostBuilderor adding a new.RunAsCliApplicationAsync()on eitherIHostBuilderorIHost` (or similar).

Tyrrrz commented 5 years ago

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

Tyrrrz commented 4 years ago

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.

atrauzzi commented 3 years ago

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.

Tyrrrz commented 3 years ago

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.

atrauzzi commented 3 years ago

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.

Tyrrrz commented 3 years ago

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

StardustDL commented 3 years ago

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();
    }
}