ltjax
ltjax

Reputation: 16007

Transforming async lambda only throwing an OperationCanceledException to synchronous variant

I have this small test in .NET 6, which works fine (it's green).

Func<CancellationToken, Task> taskFactory = async token => throw new OperationCanceledException();
Assert.True(taskFactory(CancellationToken.None).IsCanceled);

However, the compiler (rightfully?) complains: warning CS1998: This async method lacks 'await' operators and will run synchronously. I could not figure out a way to transform this into a synchronous variant. I tried these 2 options for the lambda

  1. without async: token => throw new OperationCanceledException(). It's pretty clear to me that this will just throw the exception directly on the stack instead of wrapping it in the task, but this is what the IDE suggested.
  2. token => Task.FromException(new OperationCanceledException()). This goes to IsFaulted instead of IsCanceled.

What is the correct way to transfrom this to a synchronous variant?

Edit: The point of this snippet was to test whether some CUT deals correctly with the OperationCanceledException being emitted from a taskFactory. So the exception still needs to emitted, possibly thrown, in the solution. I am not simply looking for a way to set a task to canceled.

Upvotes: 1

Views: 139

Answers (2)

Peter Csala
Peter Csala

Reputation: 22829

You could use the low-level APIs as well to achieve the desired outcome in a synchronous way.

TaskCompletionSource tcs = new();
Func<CancellationToken, Task> taskFactory = token => tcs.Task;

With this approach your delegate remains sync (don't need to use the async keyword)

CancellationTokenSource cts = new(0);
cts.Token.Register(() => tcs.TrySetCanceled());

Here we connect the Task and Cancellation primitives via a simple callback.
We cancel the Task right away.

And that's it :) A working example can be found here.

Upvotes: 0

Matthew Watson
Matthew Watson

Reputation: 109762

You could await a completed task:

Func<CancellationToken, Task> taskFactory = async token =>
{
    await Task.CompletedTask;
    throw new OperationCanceledException();
};
        
Assert.True(taskFactory(CancellationToken.None).IsCanceled);

However your test does have a race condition, although it's likely to usually succeed. But if you change it to this:

Func<CancellationToken, Task> taskFactory = async token =>
{
    await Task.Delay(1000);
    throw new OperationCanceledException();
};

var task = taskFactory(CancellationToken.None);
Console.WriteLine(task.IsCanceled);

it will print false. To fix that, you have to wait for the task to complete:

Func<CancellationToken, Task> taskFactory = async token =>
{
    await Task.Delay(1000);
    throw new OperationCanceledException();
};

var task = taskFactory(CancellationToken.None);
Task.WaitAny(task);
Console.WriteLine(task.IsCanceled);

Note that I'm using Task.WaitAny() to wait for the task to complete without throwing a TaskCanceledException. If you simply await task; it will of course throw that exception.

Upvotes: 2

Related Questions