spectre.console icon indicating copy to clipboard operation
spectre.console copied to clipboard

AddDelegateAsync or pass original arguments

Open WojciechNagorski opened this issue 2 years ago • 1 comments

Is your feature request related to a problem? Please describe. I would like to run ASP Core application in one of the command:

public static async Task Main(string[] args)
{
    var app = new CommandApp<RunCommand>();
    
    app.Configure(config =>
        {
            config.AddCommand<RunCommand>("db-migrate")
                .WithDescription("Only starts database migrations and exits the application.");
            config.AddCommand<RunCommand>("run")
                .WithDescription("Runs ASP core application");
        });
    await app.RunAsync(args);
}

But in this way, I'm not able to pass original args to my host:

public class RunCommand : AsyncCommand
{
    public override async Task<int> ExecuteAsync(CommandContext context)
    {
        var host = Host.CreateDefaultBuilder(args) // <-- here I need the program arguments, but context contains only parsed list
                .ConfigureWebHostDefaults(webBuilder =>
                    {
                        webBuilder.UseStartup<Startup>();
                    }).Build();

        await host.RunAsync();

        return 0;
    }
}

So I tried use the AddDelegate method instead of AddCommand, but this method can't be async.

public static async Task Main(string[] args)
{
    var app = new CommandApp<RunCommand>();

    app.Configure(config =>
        {
            config.AddCommand<RunCommand>("db-migrate")
                .WithDescription("Only starts database migrations and exits the application.");
            config.AddDelegate("run", context => RunApplication(args)) //<-- the RunApplication method can'd be async
                .WithDescription("Runs ASP core application");
        });

    await app.RunAsync(args);
}

public static async Task RunApplication(string[] args)
{
    var host = Host.CreateDefaultBuilder(args) // <-- here I need the program arguments, but context contains only parsed list
                .ConfigureWebHostDefaults(webBuilder =>
                    {
                        webBuilder.UseStartup<Startup>();
                    }).Build();

    await host.RunAsync();
}

Describe the solution you'd like This problem can be solved in two ways:

  1. Add original parameters to the command context:
public class RunCommand : AsyncCommand
{
    public override async Task<int> ExecuteAsync(CommandContext context)
    {
        var args = context.OriginalArguments //<-- contains args from the main method

        return 0;
    }
}
  1. Support async deleget AddDelegateAsync:
public static async Task Main(string[] args)
{
    var app = new CommandApp<RunCommand>();

    app.Configure(config =>
        {
            config.AddDelegateAsync("run", async context => await RunApplication(args)) //<-- the new AddDelegateAsync method
        });

    await app.RunAsync(args);
}

WojciechNagorski avatar Mar 09 '22 08:03 WojciechNagorski

I can prepare PR with the solution, but first I need to know which solution do you prefer.

WojciechNagorski avatar Mar 09 '22 08:03 WojciechNagorski

I think that adding an async delegate is more generic. I came here because I need that as a way to constructing an async command by myself (fiddling with the typeregistrar to build it your way is much more complicated).

I would say, though, that ending the method name with Async can be confusing since the method itself is not async, I'd rather name it AddAsyncDelegate (we are not adding a delegate asynchronously, but adding an asynchronous delegate).

icalvo avatar Apr 14 '23 09:04 icalvo

Hello @WojciechNagorski, I'm going to merge @icalvo's linked PR shortly which will provide you with solution 2.

However, I thought you might be interested to see the following example which passes data to an executing command through the CommandContext:

using Spectre.Console;
using Spectre.Console.Cli;

public class Program
{
    public static async Task Main(string[] args)
    {
        var app = new CommandApp();

        app.SetDefaultCommand<AsynchronousCommand>()
            .WithData(new string[] { "a", "b" });

        app.Configure(config =>
        {
            config.PropagateExceptions();
        });

        await app.RunAsync(args);
    }
}

public sealed class AsynchronousCommand : AsyncCommand
{
    private readonly IAnsiConsole _console;

    public AsynchronousCommand(IAnsiConsole console)
    {
        _console = console;
    }

    public async override Task<int> ExecuteAsync(CommandContext context)
    {
        _console.WriteLine($"AsynchronousCommand.ExecuteAsync");
        _console.WriteLine($"- context.Data: {string.Join(", ", ((string[])context.Data))}");

        return 0;
    }
}

Result: image

FrankRay78 avatar May 24 '23 11:05 FrankRay78