diff --git a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt index cf7c930638..d6319ed1be 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt +++ b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt @@ -54,6 +54,7 @@ public System.Void SetAction(System.Action action) public System.Void SetAction(System.Func action) public System.Void SetAction(System.Func action) + public System.Void SetAction(System.Func action) public System.Void SetAction(System.Func> action) public class CommandLineConfiguration .ctor(Command rootCommand) diff --git a/src/System.CommandLine.Tests/Invocation/InvocationTests.cs b/src/System.CommandLine.Tests/Invocation/InvocationTests.cs index b855b4b905..b9cc930bef 100644 --- a/src/System.CommandLine.Tests/Invocation/InvocationTests.cs +++ b/src/System.CommandLine.Tests/Invocation/InvocationTests.cs @@ -188,7 +188,27 @@ public async Task Anonymous_RootCommand_int_returning_Action_can_set_custom_resu (await rootCommand.Parse("").InvokeAsync()).Should().Be(123); } - + + [Fact] // https://github.com/dotnet/command-line-api/issues/2562 + public void Anonymous_async_action_is_not_mapped_into_sync_void_with_fire_and_forget() + { + RootCommand rootCommand = new(); + using CancellationTokenSource cts = new(); + Task delay = Task.Delay(TimeSpan.FromHours(1), cts.Token); + + rootCommand.SetAction(async parseResult => + { + await delay; + }); + + Task started = rootCommand.Parse("").InvokeAsync(); + + // The action is supposed to wait for an hour, so it should not complete immediately. + started.IsCompleted.Should().BeFalse(); + + cts.Cancel(); + } + [Fact] public void Terminating_option_action_short_circuits_command_action() { diff --git a/src/System.CommandLine/Command.cs b/src/System.CommandLine/Command.cs index e61b771b1a..7a1bc15941 100644 --- a/src/System.CommandLine/Command.cs +++ b/src/System.CommandLine/Command.cs @@ -149,6 +149,30 @@ public void SetAction(Func action) }); } + /// + /// Sets an asynchronous action to be run when the command is invoked. + /// + /// + /// When possible, prefer using the overload + /// and passing the parameter to the async method(s) called by the action. + /// + // Hide from intellisense, it's public to avoid the compiler choosing a sync overload + // for an async action (and fire and forget issue described in https://github.com/dotnet/command-line-api/issues/2562). + [EditorBrowsable(EditorBrowsableState.Never)] + public void SetAction(Func action) + { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + + Action = new AnonymousAsynchronousCommandLineAction(async (context, cancellationToken) => + { + await action(context); + return 0; + }); + } + /// /// Sets an asynchronous action when the command is invoked. ///