Muitas respostas boas aqui, mas eu ainda gostaria de postar meu discurso retórico, pois acabei de encontrar o mesmo problema e conduzi algumas pesquisas. Ou pule para a versão TLDR abaixo.
O problema
Aguardar o taskretornado por Task.WhenAllapenas lança a primeira exceção do AggregateExceptionarmazenado em task.Exception, mesmo quando várias tarefas falharam.
Os documentos atuais paraTask.WhenAll dizer:
Se qualquer uma das tarefas fornecidas for concluída em um estado de falha, a tarefa retornada também será concluída em um estado de Falha, onde suas exceções conterão a agregação do conjunto de exceções não agrupadas de cada uma das tarefas fornecidas.
O que está correto, mas não diz nada sobre o comportamento de "desembrulhar" mencionado acima de quando a tarefa retornada é aguardada.
Suponho que os documentos não mencionem isso porque esse comportamento não é específico paraTask.WhenAll .
É simplesmente que Task.Exceptioné do tipo AggregateExceptione para awaitcontinuações sempre é desembrulhado como sua primeira exceção interna, por design. Isso é ótimo para a maioria dos casos, porque geralmente Task.Exceptionconsiste em apenas uma exceção interna. Mas considere este código:
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]);
}
Aqui, uma instância de AggregateExceptioné desempacotada em sua primeira exceção interna InvalidOperationExceptionexatamente da mesma maneira que poderíamos ter feito Task.WhenAll. Poderíamos ter falhado em observar DivideByZeroExceptionse não tivéssemos passado task.Exception.InnerExceptionsdiretamente.
Stephen Toub, da Microsoft, explica a razão por trás desse comportamento no problema relacionado do GitHub :
O que eu estava tentando enfatizar é que isso foi discutido em profundidade, anos atrás, quando estes foram originalmente adicionados. Originalmente, fizemos o que você está sugerindo, com a Tarefa retornada de WhenAll contendo uma única AggregateException que continha todas as exceções, ou seja, task.Exception retornaria um wrapper AggregateException que continha outra AggregateException que continha as exceções reais; então, quando fosse aguardado, o AggregateException interno seria propagado. O forte feedback que recebemos que nos levou a mudar o design foi que a) a grande maioria desses casos tinha exceções razoavelmente homogêneas, de modo que a propagação de tudo em um agregado não era tão importante, b) a propagação do agregado quebrou as expectativas em torno das capturas para os tipos de exceção específicos, ec) para os casos em que alguém deseja o agregado, pode fazê-lo explicitamente com as duas linhas, como escrevi. Também tivemos extensas discussões sobre qual deveria ser o comportamento de await em relação a tarefas que continham múltiplas exceções, e foi aí que pousamos.
Outra coisa importante a se notar, esse comportamento de desembrulhar é superficial. Ou seja, ele apenas desembrulhará a primeira exceção AggregateException.InnerExceptionse a deixará lá, mesmo se acontecer de ser uma instância de outra AggregateException. Isso pode adicionar mais uma camada de confusão. Por exemplo, vamos mudar WhenAllWrongassim:
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));
}
Uma solução (TLDR)
Então, voltando ao await Task.WhenAll(...), o que eu pessoalmente queria é ser capaz de:
- Obtenha uma única exceção se apenas uma tiver sido lançada;
- Obtenha um
AggregateExceptionse mais de uma exceção tiver sido lançada coletivamente por uma ou mais tarefas;
- Evite ter que salvar
Taskapenas para verificar seu Task.Exception;
- Propagar o status de cancelamento adequadamente (
Task.IsCanceled), como algo como isso não faria isso: Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }.
Eu juntei a seguinte extensão para isso:
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();
}
}
Agora, o seguinte funciona da maneira que eu quero:
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. Se você usou emTask.Waitvez deawaitem seu exemplo, você pegariaAggregateException