C# asynchronous tasks training (turn-based simulation)
The Wait call is tricky.

It throws by default AggregateException not OperationCanceledException.
If you provide the cancellationToken to the Wait then it will throw an OperationCanceledException.
The biggest problem with this approach is that it cancels only the Wait not the underlying task.

Please read this article for more details.



So, if we switch to async main then the application will not stop after user cancellation:
static async Task Main()
{
    var gameTurnsController = new GameTurnsController();
    var userRequestedCancellation = new CancellationTokenSource();

    try
    {
        _ = Task.Run(() =&gt;
        {
            Console.WriteLine(&quot;Please press any key to cancel&quot;);
            Console.ReadKey(true);
            userRequestedCancellation.Cancel();
        });

        await gameTurnsController.PassTurns(TimeSpan.FromSeconds(1), userRequestedCancellation.Token);
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine(Environment.NewLine + &quot;Cancelled.&quot;);
        Environment.ExitCode = 0;
    }

    Console.WriteLine(&quot;Execution finished.&quot;);
}


Because the underlying LongRunningOperation will not terminate.

(Let me use Thread.Sleep(5000) to simulate complex CPU bound operation)



private async Task&lt;long&gt; LongRunningOperation(CancellationToken cancellationToken)
{
    return await Task.Run(() =&gt;
    {
        long result;
        Thread.Sleep(5000);
        return result;
    }, cancellationToken);
}


If I run this app and I press any key then it will not cancel the LongRunningOperation method that's why the output will be the Execution finished..

How to fix it?
By passing all the way long the cancellationToken:
private async Task&lt;long&gt; LongRunningOperation(CancellationToken cancellationToken)
{
    var job = Task.Run(() =&gt; LongRunningOperationInternal(cancellationToken), cancellationToken);
    return await job;
}

private long LongRunningOperationInternal(CancellationToken cancellationToken)
{
    long result = 1;
    for (int i = 0; i &lt; 5; i++)
    {
        cancellationToken.ThrowIfCancellationRequested();
        Thread.Sleep(1000); 
    }

    return result;
}

If need to do graceful cancellation then consider to use the following pattern:
if(cancellationToken.IsCancellationRequested)
{
  //TODO: clean up whatever is needed

  cancellationToken.ThrowIfCancellationRequested();
}

TL;DR: You need to check the cancellationToken at milestones/checkpoints to be able to truly cancel the whole application.

Might be an interesting read: How to can non-cancelable async operations