How to return AggregateException from async method

后端 未结 3 1387
野的像风
野的像风 2021-01-24 17:06

I got an async method working like an enhanced Task.WhenAll. It takes a bunch of tasks and returns when all are completed.

public async Task MyWhenA         


        
3条回答
  •  野性不改
    2021-01-24 17:37

    This answer combines ideas from Servy's and canton7's solutions. As an example is used an actually useful method named Retry, that attempts to run a task multiple times before reporting failure. In case that the maximum number of attempts has been reached, the returned task transitions to a faulted state, containing all the exceptions bundled inside an AggregateException. So this method behaves very similar to the built-in Task.WhenAll method. Here is the Retry method:

    public static Task Retry(Func> taskFactory,
        int maxAttempts)
    {
        if (taskFactory == null) throw new ArgumentNullException(nameof(taskFactory));
        if (maxAttempts < 1) throw new ArgumentOutOfRangeException(nameof(maxAttempts));
        return FlattenTopAggregateException(Implementation());
    
        async Task Implementation()
        {
            var exceptions = new List();
            while (true)
            {
                var task = taskFactory();
                try
                {
                    return await task.ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    exceptions.Add(ex);
                    if (exceptions.Count >= maxAttempts)
                        throw new AggregateException(exceptions);
                }
            }
        }
    }
    

    The method is separated into two parts: 1) argument validation and 2) implementation. The implementation is an asynchronous local function, that throws an AggregateException. The resulting task of the local function is not well behaved because on failure it contains a nested AggregateException. To fix this problem the task is flattened before returned by the outer Retry method. It is flattened only at top level, meaning that a possibly deep AggregateException hierarchy will become shortened only by one level. The purpose of flattening is just to eliminate the top-level nesting that is caused by throwing an AggregateException from an async method. Here is the flattening method:

    private static Task FlattenTopAggregateException(Task task)
    {
        var tcs = new TaskCompletionSource();
        HandleTaskCompletion();
        return tcs.Task;
    
        async void HandleTaskCompletion()
        {
            try
            {
                var result = await task.ConfigureAwait(false);
                tcs.SetResult(result);
            }
            catch (OperationCanceledException ex) when (task.IsCanceled)
            {
                // Unfortunately the API SetCanceled(CancellationToken) is missing
                if (!tcs.TrySetCanceled(ex.CancellationToken)) tcs.SetCanceled();
            }
            catch (Exception ex)
            {
                var taskException = task.Exception;
                if (taskException == null || taskException.InnerExceptions.Count == 0)
                {
                    // Handle abnormal case
                    tcs.SetException(ex);
                }
                else if (taskException.InnerExceptions.Count == 1
                    && taskException.InnerException is AggregateException aex
                    && aex.InnerExceptions.Count > 0)
                {
                    // Fix nested AggregateException
                    tcs.SetException(aex.InnerExceptions);
                }
                else
                {
                    // Keep it as is
                    tcs.SetException(taskException.InnerExceptions);
                }
            }
        }
    }
    

    This method contains an async void local function. The reason for this is for ensuring that any bugs that may lurk in the implementation will be surfaced. In case async void methods are undesirable, it can be trivially converted to a fire-and-forget task.

    Here is a usage example of the Retry method. The returned task is awaited inside a try-catch block. The Exception caught by the block is ignored, and the Exception property of the task is observed instead:

    var task = Retry(async () =>
    {
        Console.WriteLine("Try to do something");
        await Task.Delay(100, new CancellationToken(true));
        return "OK";
    }, maxAttempts: 3);
    
    try
    {
        await task;
    }
    catch
    {
        if (task.IsFaulted)
        {
            Console.WriteLine($"Errors: {task.Exception.InnerExceptions.Count}");
            Console.WriteLine($"{task.Exception.Message}");
        }
        else
        {
            Console.WriteLine("Not faulted");
        }
    }
    

    Output:

    Try to do something
    Try to do something
    Try to do something
    Errors: 3
    One or more errors occurred. (A task was canceled.) (A task was canceled.) (A task was canceled.)

提交回复
热议问题