Question

For this question, I'm examining the behavior of a task t1 and a continuation function f2, where f2 executes after t1 completes. The target framework is .NET 4.0 or .NET 3.5 with the Task Parallel Library for .NET 3.5, so the use of async and await is not allowed. The simplest way of executing this is something like the following:

// if f2 doesn't return a Task
Task result = t1.ContinueWith(f2);

// if f2 returns a Task
Task result = t1.ContinueWith(f2).Unwrap();

While the above code handles the success case well, it's suboptimal for cases of cancellation and faults. To address the situation, I'm interested in the cleanest approach which meets all of the following:

  1. If t1 is cancelled, then f2 is not executed and the status of result is TaskStatus.Canceled.

  2. If t1 is faulted and f2 does not handle faulted antecedent tasks, then f2 is not executed and the exception returned by result.Exception is the same as the value returned by t1.Exception (i.e. the exception is not wrapped in another AggregateException).

  3. If t1 completes successfully, or if t1 is faulted and f2 does handle faulted antecedent tasks, then the result behaves identically to the ContinueWith code listed above.

To address this situation, I created two extension methods Chain (which handles the case where f2 doesn't return a Task), and ChainAsync (which handles the case where f2 returns a Task).

/// <summary>
/// Execute a continuation when a task completes. The <paramref name="supportsErrors"/>
/// parameter specifies whether the continuation is executed if the antecedent task is faulted.
/// </summary>
/// <remarks>
/// <para>If the antecedent task is cancelled, or faulted with <paramref name="supportsErrors"/>
/// set to <see langword="false"/>, the status of the antecedent is directly applied to the task
/// returned by this method; it is not wrapped in an additional <see cref="AggregateException"/>.
/// </para>
/// </remarks>
/// <typeparam name="TSource">The type of the result produced by the antecedent <see cref="Task{TResult}"/>.</typeparam>
/// <typeparam name="TResult">The type of the result produced by the continuation <see cref="Task{TResult}"/>.</typeparam>
/// <param name="task">The antecedent task.</param>
/// <param name="continuationFunction">The continuation function to execute when <paramref name="task"/> completes successfully.</param>
/// <param name="supportsErrors"><see langword="true"/> if the <paramref name="continuationFunction"/> properly handles a faulted antecedent task; otherwise, <see langword="false"/>.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation. When the task completes successfully,
/// the <see cref="Task{TResult}.Result"/> property will contain the result returned from the <paramref name="continuationFunction"/>.</returns>
/// <exception cref="ArgumentNullException">
/// If <paramref name="task"/> is <see langword="null"/>.
/// <para>-or-</para>
/// <para>If <paramref name="continuationFunction"/> is <see langword="null"/>.</para>
/// </exception>
public static Task<TResult> Chain<TSource, TResult>(this Task<TSource> task, Func<Task<TSource>, TResult> continuationFunction, bool supportsErrors)
{
    if (task == null)
        throw new ArgumentNullException("task");
    if (continuationFunction == null)
        throw new ArgumentNullException("continuationFunction");

    TaskCompletionSource<TResult> completionSource = new TaskCompletionSource<TResult>();

    TaskContinuationOptions successContinuationOptions = supportsErrors ? TaskContinuationOptions.NotOnCanceled : TaskContinuationOptions.OnlyOnRanToCompletion;
    task
        .ContinueWith(continuationFunction, successContinuationOptions)
        .ContinueWith(
            t =>
            {
                if (task.Status == TaskStatus.RanToCompletion || supportsErrors && task.Status == TaskStatus.Faulted)
                    completionSource.SetFromTask(t);
            }, TaskContinuationOptions.ExecuteSynchronously);

    TaskContinuationOptions failedContinuationOptions = supportsErrors ? TaskContinuationOptions.OnlyOnCanceled : TaskContinuationOptions.NotOnRanToCompletion;
    task
        .ContinueWith(t => completionSource.SetFromTask(t), TaskContinuationOptions.ExecuteSynchronously | failedContinuationOptions);

    return completionSource.Task;
}

/// <summary>
/// Execute a continuation task when a task completes. The continuation
/// task is created by a continuation function, and then unwrapped to form the result
/// of this method. The <paramref name="supportsErrors"/> parameter specifies whether
/// the continuation is executed if the antecedent task is faulted.
/// </summary>
/// <remarks>
/// <para>If the antecedent <paramref name="task"/> is cancelled, or faulted with
/// <paramref name="supportsErrors"/> set to <see langword="false"/>, the status
/// of the antecedent is directly applied to the task returned by this method; it is
/// not wrapped in an additional <see cref="AggregateException"/>.
/// </para>
/// </remarks>
/// <typeparam name="TSource">The type of the result produced by the antecedent <see cref="Task{TResult}"/>.</typeparam>
/// <typeparam name="TResult">The type of the result produced by the continuation <see cref="Task{TResult}"/>.</typeparam>
/// <param name="task">The antecedent task.</param>
/// <param name="continuationFunction">The continuation function to execute when <paramref name="task"/> completes successfully. The continuation function returns a <see cref="Task{TResult}"/> which provides the final result of the continuation.</param>
/// <param name="supportsErrors"><see langword="true"/> if the <paramref name="continuationFunction"/> properly handles a faulted antecedent task; otherwise, <see langword="false"/>.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation. When the task completes successfully,
/// the <see cref="Task{TResult}.Result"/> property will contain the result provided by the
/// <see cref="Task{TResult}.Result"/> property of the task returned from <paramref name="continuationFunction"/>.</returns>
/// <exception cref="ArgumentNullException">
/// If <paramref name="task"/> is <see langword="null"/>.
/// <para>-or-</para>
/// <para>If <paramref name="continuationFunction"/> is <see langword="null"/>.</para>
/// </exception>
public static Task<TResult> ChainAsync<TSource, TResult>(this Task<TSource> task, Func<Task<TSource>, Task<TResult>> continuationFunction, bool supportsErrors)
{
    if (task == null)
        throw new ArgumentNullException("task");
    if (continuationFunction == null)
        throw new ArgumentNullException("continuationFunction");

    TaskCompletionSource<TResult> completionSource = new TaskCompletionSource<TResult>();

    TaskContinuationOptions successContinuationOptions = supportsErrors ? TaskContinuationOptions.NotOnCanceled : TaskContinuationOptions.OnlyOnRanToCompletion;
    task
        .ContinueWith(continuationFunction, successContinuationOptions)
        .Unwrap()
        .ContinueWith(
            t =>
            {
                if (task.Status == TaskStatus.RanToCompletion || supportsErrors && task.Status == TaskStatus.Faulted)
                    completionSource.SetFromTask(t);
            }, TaskContinuationOptions.ExecuteSynchronously);

    TaskContinuationOptions failedContinuationOptions = supportsErrors ? TaskContinuationOptions.OnlyOnCanceled : TaskContinuationOptions.NotOnRanToCompletion;
    task
        .ContinueWith(t => completionSource.SetFromTask(t), TaskContinuationOptions.ExecuteSynchronously | failedContinuationOptions);

    return completionSource.Task;
}

As you can see, these implementations create result in two additional calls to ContinueWith plus an allocation of TaskCompletionSource<TResult> per continuation, which leaves me with two questions.

  1. Are there any cases where the tasks returned by these extension methods do not behave in the expected manner, according to the list of goals above?

  2. Is there a more efficient way (fewer resources in play) to achieve similar results in both success and error cases?

No correct solution

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top