I'm currently learning how to properly expose asynchronous parts of our library API with Task
s so they can be easier and nicer to use for customers. I decided to go with the TaskCompletionSource
approach of wrapping a Task
around it that doesn't get scheduled on the thread pool (in the instance here unneeded anyway since it's basically just a timer). This works fine, but cancellation is a bit of a headache right now.
The example shows the basic usage, registering a delegate on the token, but is a tiny bit more complicated than in my case, more to the point, I'm not really sure what to do with the TaskCanceledException
. The documentation says either just returning and having the task status switch to RanToCompletion
, or throwing an OperationCanceledException
(which results in the task's result being Canceled
) is fine. However, the examples seem to only pertain to, or at least mention, tasks that are started via a delegate passed to TaskFactory.StartNew
.
My code currently is (roughly) as follows:
public Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);
// Cancellation
token.Register(() => {
tcs.TrySetCanceled();
CancelAndCleanupFoo(foo);
});
RunFoo(foo, callback);
return tcs.Task;
}
(There is no result and no possible exceptions during execution; one reason why I chose to start here, and not at more complicated places in the library.)
In the current form, when I call TrySetCanceled
on the TaskCompletionSource
, I always get a TaskCanceledException
if I await the task returned. My guess would be that this is normal behaviour (I hope it is) and that I'm expected to wrap a try
/catch
around the call when I want to use cancellation.
If I don't use TrySetCanceled
, then I'll eventually run in the finish callback and the task looks like it finished normally. But I guess if a user wants to distinguish between a task that finished normally and one that was canceled, the TaskCanceledException
is pretty much a side-effect of ensuring that, right?
Another point I didn't quite understand: Documentation suggests that any exceptions, even those that pertain to cancellation, are wrapped in an AggregateException
by the TPL. However, in my tests I'd always get the TaskCanceledException
directly, without any wrapper. Am I missing something here, or is that just poorly-documented?
TL;DR:
- For a task to transition into the
Canceled
state, there's always a corresponding exception needed for that and users have to wrap atry
/catch
around the async call to be able to detect that, right? - The
TaskCanceledException
being thrown unwrapped is also expected and normal and I'm not doing anything wrong here?