Тут є багато хороших відповідей, але я все одно хотів би опублікувати свою заяву, оскільки я щойно стикався з тією ж проблемою та проводив деякі дослідження. Або перейдіть до версії TLDR нижче.
Проблема
Очікування taskповерненого Task.WhenAllлише викидає перше виняток із AggregateExceptionзбереженого в task.Exception, навіть коли кілька завдань мають збій.
В даний час документи дляTask.WhenAll кажуть:
Якщо якесь із поставлених завдань виконується у стані несправності, повернене завдання також виконується у стані несправності, де його винятки містять сукупність набору нерозгорнутих винятків з кожного із поставлених завдань.
Що правильно, але це нічого не говорить про згадану поведінку "розгортання", коли очікується повернене завдання.
Я припускаю, що документи не згадують про це, оскільки така поведінка не є специфічною дляTask.WhenAll .
Це просто Task.Exceptionтип, AggregateExceptionі для awaitпродовжень він завжди розгортається як перший внутрішній виняток, за задумом. Це чудово для більшості випадків, оскільки зазвичай Task.Exceptionскладається лише з одного внутрішнього винятку. Але враховуйте цей код:
Task WhenAllWrong()
{
var tcs = new TaskCompletionSource<DBNull>();
tcs.TrySetException(new Exception[]
{
new InvalidOperationException(),
new DivideByZeroException()
});
return tcs.Task;
}
var task = WhenAllWrong();
try
{
await task;
}
catch (Exception exception)
{
// task.Exception is an AggregateException with 2 inner exception
Assert.IsTrue(task.Exception.InnerExceptions.Count == 2);
Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(InvalidOperationException));
Assert.IsInstanceOfType(task.Exception.InnerExceptions[1], typeof(DivideByZeroException));
// However, the exception that we caught here is
// the first exception from the above InnerExceptions list:
Assert.IsInstanceOfType(exception, typeof(InvalidOperationException));
Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
}
Тут екземпляр AggregateExceptionрозгортається до свого першого внутрішнього винятку InvalidOperationExceptionточно так само, як і у нас Task.WhenAll. Ми могли б не спостерігати, DivideByZeroExceptionякби не пройшли task.Exception.InnerExceptionsбезпосередньо.
Стівен Туб від Microsoft пояснює причину такої поведінки у відповідній проблемі GitHub :
Я намагався сказати, що це було глибоко обговорено багато років тому, коли вони спочатку були додані. Спочатку ми зробили те, що ви пропонуєте, із завданням, повернутим із WhenAll, що містить один AggregateException, який містив усі винятки, тобто task.Exception повертає оболонку AggregateException, яка містить інший AggregateException, який потім містить фактичні винятки; тоді, коли його очікували, внутрішній AggregateException буде розповсюджений. Вагомий відгук, який ми отримали, що змусив нас змінити дизайн, полягав у тому, що а) переважна більшість таких випадків мала досить однорідні винятки, наприклад, поширення всього в сукупності було не так важливо, б) розповсюдження сукупності потім порушило очікування щодо вилову для конкретних типів винятків, і в) для випадків, коли хтось хотів сукупність, вони могли зробити це явно за допомогою двох рядків, як я писав. Ми також вели обговорення щодо того, якою може бути поведінка await щодо завдань, що містять численні винятки, і саме тут ми потрапили.
Ще одна важлива річ, на яку слід звернути увагу, - така поведінка розгортання є поверхневою. Тобто, він розгорне лише перший виняток AggregateException.InnerExceptionsі залишить його там, навіть якщо це випадково інший екземпляр AggregateException. Це може внести ще один шар плутанини. Наприклад, давайте змінимось WhenAllWrongтак:
async Task WhenAllWrong()
{
await Task.FromException(new AggregateException(
new InvalidOperationException(),
new DivideByZeroException()));
}
var task = WhenAllWrong();
try
{
await task;
}
catch (Exception exception)
{
// now, task.Exception is an AggregateException with 1 inner exception,
// which is itself an instance of AggregateException
Assert.IsTrue(task.Exception.InnerExceptions.Count == 1);
Assert.IsInstanceOfType(task.Exception.InnerExceptions[0], typeof(AggregateException));
// And now the exception that we caught here is that inner AggregateException,
// which is also the same object we have thrown from WhenAllWrong:
var aggregate = exception as AggregateException;
Assert.IsNotNull(aggregate);
Assert.AreSame(exception, task.Exception.InnerExceptions[0]);
Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException));
Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException));
}
Рішення (TLDR)
Отже, повертаючись до того await Task.WhenAll(...), що я особисто хотів, це мати можливість:
- Отримайте єдиний виняток, якщо було лише одне;
- Отримайте,
AggregateExceptionякщо декілька винятків були спільно викликані одним або кількома завданнями;
- Уникайте збереження
Taskєдиного для перевірки його Task.Exception;
- Розмножуються статус скасування правильно (
Task.IsCanceled), а що - щось на зразок цього не робитиме , що: Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }.
Для цього я зібрав таке розширення:
public static class TaskExt
{
/// <summary>
/// A workaround for getting all of AggregateException.InnerExceptions with try/await/catch
/// </summary>
public static Task WithAggregatedExceptions(this Task @this)
{
// using AggregateException.Flatten as a bonus
return @this.ContinueWith(
continuationFunction: anteTask =>
anteTask.IsFaulted &&
anteTask.Exception is AggregateException ex &&
(ex.InnerExceptions.Count > 1 || ex.InnerException is AggregateException) ?
Task.FromException(ex.Flatten()) : anteTask,
cancellationToken: CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
scheduler: TaskScheduler.Default).Unwrap();
}
}
Тепер наступне працює так, як я хочу:
try
{
await Task.WhenAll(
Task.FromException(new InvalidOperationException()),
Task.FromException(new DivideByZeroException()))
.WithAggregatedExceptions();
}
catch (OperationCanceledException)
{
Trace.WriteLine("Canceled");
}
catch (AggregateException exception)
{
Trace.WriteLine("2 or more exceptions");
// Now the exception that we caught here is an AggregateException,
// with two inner exceptions:
var aggregate = exception as AggregateException;
Assert.IsNotNull(aggregate);
Assert.IsInstanceOfType(aggregate.InnerExceptions[0], typeof(InvalidOperationException));
Assert.IsInstanceOfType(aggregate.InnerExceptions[1], typeof(DivideByZeroException));
}
catch (Exception exception)
{
Trace.WriteLine($"Just a single exception: ${exception.Message}");
}
AggregateException. Якби ви використалиTask.Waitзамість того, щобawaitу вашому прикладі, ви впіймали бAggregateException