Async Commands and Cancellation

How to create asynchronous commands and handle cancellation in Spectre.Console.Cli

When your command performs I/O-bound operations like HTTP requests, database queries, or file operations, use AsyncCommand<TSettings> instead of Command<TSettings>. This lets you use async/await and enables graceful shutdown when users press Ctrl+C.

Wire Up Console Cancellation

To enable Ctrl+C handling, create a CancellationTokenSource and pass its token to RunAsync. The framework automatically propagates this token to your command's ExecuteAsync method.

// Create a cancellation token source to handle Ctrl+C
var cancellationTokenSource = new CancellationTokenSource();
  
// Wire up Console.CancelKeyPress to trigger cancellation
System.Console.CancelKeyPress += (_, e) =>
{
    e.Cancel = true; // Prevent immediate process termination
    cancellationTokenSource.Cancel();
    System.Console.WriteLine("Cancellation requested...");
};
  
var app = new CommandApp<FetchCommand>();
  
// Pass the cancellation token to RunAsync
return await app.RunAsync(args, cancellationTokenSource.Token);

Create an Async Command

Inherit from AsyncCommand<TSettings> and override ExecuteAsync. The CancellationToken is passed automatically—forward it to any async operations so they can respond to cancellation requests.

internal class FetchCommand : AsyncCommand<FetchCommand.Settings>
{
    public class Settings : CommandSettings
    {
        [CommandArgument(0, "<url>")]
        [Description("The URL to fetch")]
        public string Url { get; init; } = string.Empty;
    }
  
    protected override async Task<int> ExecuteAsync(
        CommandContext context,
        Settings settings,
        CancellationToken cancellationToken)
    {
        System.Console.WriteLine($"Fetching {settings.Url}...");
  
        try
        {
            using var httpClient = new HttpClient();
            // Pass the cancellation token to async operations
            var response = await httpClient.GetStringAsync(settings.Url, cancellationToken);
            System.Console.WriteLine($"Fetched {response.Length} characters");
            return 0;
        }
        catch (OperationCanceledException)
        {
            System.Console.WriteLine("Request was cancelled.");
            return 1;
        }
    }
}

See Also