1
\$\begingroup\$

I have a following task: execute multiple asynchronous methods synchronously and always execute them all, even when one of them throws. Rethrow thrown exceptions if any occured only after all tasks have finished. Here is my code:

public static async Task ContinueWithoutThrowing(this IEnumerable<Func<Task>> tasks)
    {
        var thrownExceptions = new List<Exception>();

        foreach (var task in tasks)
        {
            try
            {
                await task().ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                thrownExceptions.Add(ex);
            }
        }

        if (thrownExceptions.Any())
        {
            throw new AggregateException(thrownExceptions);
        }
    }

Usage of this code is following:

Func<Task>[] tasks = { First, Second };
await tasks.ContinueWithoutThrowing().ConfigureAwait(false);

I am not sure whether what I am doing is accomplishable using already implemented methods on Task class like WhenAll. But using WhenAll with await throws on first exception. Also couldn't figure out some better name than ContinueWithoutThrowing when it actually throws if used with await (but its wanted behavior). I know this code doesn't handle cancellation but its not my main concern right now.

\$\endgroup\$
  • \$\begingroup\$ What you have works. I was going to mention cancellation tokens but you already hinted at it in your post. \$\endgroup\$ – Nkosi Feb 22 '18 at 15:51
2
\$\begingroup\$

After my initial answer and based on your subsequent comments, I would say that your implementation is OK.

As for naming I would keep it simple like WhenAllCompleted

/// <summary>
/// Creates a task that will complete when all of the supplied tasks have completed synchronously.
/// </summary>
/// <param name="tasks">The tasks to wait on for completion.</param>
/// <returns>A task that represents the completion of all of the supplied tasks.</returns>
public static async Task WhenAllCompleted(this IEnumerable<Func<Task>> tasks) {
    if (tasks == null) throw new ArgumentNullException("tasks");
    var exceptions = new List<Exception>();
    using (var e = tasks.GetEnumerator()) {
        while (e.MoveNext()) {
            var task = e.Current;
            try {
                await task();
            } catch (Exception ex) {
                exceptions.Add(ex);
            }
        }
    }
    if (exceptions.Any()) {
        throw new AggregateException(exceptions);
    }
}

I've basically only added the null check on the provided argument. I considered doing null checks on the function and task returned like

var function = e.Current;
if (function == null) {
    exceptions.Add(new ArgumentException("The tasks collection contained a null task.", "tasks"));
    continue;
}
//...

but figured that would all be caught by your simpler implementation, so decided against it.

Though you have not added cancellation as yet the following should be of some help with an optional cancellation token

/// <summary>
/// Creates a task that will complete when all of the supplied tasks have completed synchronously.
/// </summary>
/// <param name="tasks">The tasks to wait on for completion.</param>
/// <returns>A task that represents the completion of all of the supplied tasks.</returns>
public static async Task WhenAllCompleted(this IEnumerable<Func<Task>> tasks, CancellationToken token = default(CancellationToken)) {
    if (tasks == null) throw new ArgumentNullException("tasks");
    var exceptions = new List<Exception>();
    using (var e = tasks.GetEnumerator()) {
        while (e.MoveNext()) {
            if (token.IsCancellationRequested) {
                exceptions.Add(new OperationCanceledException("tasks collection was cancelled", token));
                break;
            }
            try {
                await e.Current();
            } catch (Exception ex) {
                exceptions.Add(ex);
            }
        }
    }
    if (exceptions.Any()) {
        throw new AggregateException(exceptions);
    }
}
\$\endgroup\$
  • \$\begingroup\$ There was a reason I used Func<Task> instead of just Task. AFAIK your implementation can cause concurrency problems when used for example with a single instance of DbContext. Thats why I used Func<T> and actually start each task only when previous task is completed. \$\endgroup\$ – LadislavBohm Feb 22 '18 at 21:11
  • \$\begingroup\$ Ok I see your point with the delayed execution with the Func<Task>. That makes sense. Did not realize that was your reason for using it. \$\endgroup\$ – Nkosi Feb 22 '18 at 23:05
  • \$\begingroup\$ I find the foreach loop was a better choice. There is no need to use the full enumerator here which overcomplicates the entire thing. \$\endgroup\$ – t3chb0t Feb 23 '18 at 6:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.