Chaining Tasks with Continuation Tasks - C#

Traditionally, when we wanted to invoke an asynchronous operation shortly after the previous one had finished, we would have used callbacks.

Here is an example of a callback being used:

static void Main()
{
    CallbackExample();
}

public static void CallbackExample()
{
    GetItem((result) => {
        Console.WriteLine($"My {result} is the best {result}.");
    });
}

private static void GetItem(Action<string> callback)
{
    var item = "hat";
    callback(item);
}
My hat is the best hat.

Callback limitations

This works well, in simple scenarios, however there are a few caveats:

  • We don't have a great deal of control over the callback. What if we wished to cancel it?
  • If GetItem could potentially fail, we'd need to wrap try/catch around the contents, and CallbackExample would need to be able to expect a failed result.
  • When we have multiple tasks that need to be run concurrently, we can quickly create spaghetti code.

Thanks to the Task Parallel Library (TPL), we can achieve the same functionality using continuation tasks.

Creating a continuation task

Here's the same example, using tasks:

public static void SingleContinue()
{
    var task = Task.Run(() =>
    {
        return "hat";
    }).ContinueWith(t =>
    {
        return $"My {t.Result} is the best {t.Result}.";
    });

    Console.WriteLine(task.Result);
}
My hat is the best hat.

In order to create a continuation, the ContinueWith method is called on the previous task, effectively chaining the two methods together. In the above example, the first task executes, but passes the whole task object (containing the result) onto the proceeding task.

Specifying task continuation options

What if the task throws an exception? What if we want to cancel the task? In those cases, we can also specify TaskContinuationOptions to ContinueWith:

public static void MultipleScheduledContinuations()
{
    var task = Task.Run(() =>
    {
        return "hat";
    });

    task.ContinueWith(t =>
    {
        // When the task has been explicitly cancelled
        Console.WriteLine("Task was cancelled");
    }, TaskContinuationOptions.OnlyOnCanceled);

    task.ContinueWith(t =>
    {
        // If the task fails, and/or hits an exception
        Console.WriteLine("Task was faulted.");
    }, TaskContinuationOptions.OnlyOnFaulted);

    var completedTask = task.ContinueWith(t =>
    {
        // If the task runs successfully
        Console.WriteLine($"My {t.Result} is the best {t.Result}.");
    }, TaskContinuationOptions.OnlyOnRanToCompletion);

    completedTask.Wait();
}

And, because the task ran successfully:

My hat is the best hat.

Try for yourself!

You can have a look at the above examples in this sandbox: