Beaucoup de bonnes réponses ici, mais je voudrais quand même poster mon coup de gueule car je viens de rencontrer le même problème et j'ai fait quelques recherches. Ou passez à la version TLDR ci-dessous.
Le problème
En attendant le task
retourné par Task.WhenAll
ne lance que la première exception de la AggregateException
stocké dans task.Exception
même lorsque plusieurs tâches sont en panne.
Le site documents actuels pour Task.WhenAll
dites :
Si l'une des tâches fournies s'achève dans un état de défaillance, la tâche retournée se terminera également dans un état défaillant, où ses exceptions contiendra l'agrégation de l'ensemble des exceptions non enveloppées de chacune des tâches fournies.
Ce qui est correct, mais ne dit rien sur le comportement de "déballage" mentionné plus haut lorsque la tâche retournée est attendue.
Je suppose que les docs ne le mentionnent pas. car ce comportement n'est pas spécifique à Task.WhenAll
.
C'est simplement que Task.Exception
est de type AggregateException
et pour await
il est toujours déballé comme sa première exception interne, à dessein. C'est très bien dans la plupart des cas, car généralement Task.Exception
consiste en une seule exception interne. Mais considérez ce code :
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]);
}
Ici, une instance de AggregateException
se déroule jusqu'à sa première exception interne InvalidOperationException
exactement de la même manière que nous aurions pu l'avoir avec Task.WhenAll
. Nous aurions pu ne pas observer DivideByZeroException
si on ne passait pas par là task.Exception.InnerExceptions
directement.
Microsoft Stephen Toub explique la raison de ce comportement dans la question connexe sur GitHub :
Ce que je voulais dire, c'est que le sujet a été discuté en profondeur, il y a des années, quand ils ont été ajoutés à l'origine. A l'origine, nous avons fait ce que ce que vous suggérez, avec la tâche retournée par WhenAll contenant une une seule AggregateException qui contenait toutes les exceptions, à savoir task.Exception renvoie une enveloppe AggregateException qui contient une autre AggregateException qui contiendrait une autre AggregateException contenant les véritables exceptions. exceptions ; ensuite, lorsqu'elle est attendue, l'exception interne AggregateException interne se propageait. Le feedback important que nous avons reçu et qui nous a amené à modifier la conception était que a) la grande majorité de ces cas avaient des exceptions exceptions assez homogènes, de sorte que la propagation de toutes dans un dans un agrégat n'était pas si important, b) la propagation de l'agrégat rompait alors les attentes concernant les captures pour l'exception spécifique. brisait les attentes autour des captures pour les types d'exceptions spécifiques, et c) pour les cas où quelqu'un voulait l'agrégat, il pouvait le faire le faire explicitement avec les deux lignes que j'ai écrites. Nous avons également eu de longues discussions approfondies sur ce que le comportement de await devrait être par rapport aux tâches contenant des exceptions multiples, et c'est là que nous avons atterri.
Une autre chose importante à noter, ce comportement de déballage est peu profond. C'est-à-dire qu'il ne déballera que la première exception de l'élément AggregateException.InnerExceptions
et le laisser là, même s'il s'agit d'une instance d'une autre AggregateException
. Cela peut ajouter une nouvelle couche de confusion. Par exemple, changeons WhenAllWrong
comme ça :
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));
}
Une solution (TLDR)
Donc, retour à await Task.WhenAll(...)
Ce que je voulais personnellement, c'était de pouvoir.. :
- Obtenir une seule exception si une seule a été lancée ;
- Obtenir un
AggregateException
si plus d'une exception a été lancée collectivement par une ou plusieurs tâches ;
- Évitez de devoir sauvegarder le
Task
uniquement pour vérifier son Task.Exception
;
- Propager correctement l'état d'annulation (
Task.IsCanceled
), car quelque chose comme ça ne le ferait pas : Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }
.
J'ai créé l'extension suivante pour cela :
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();
}
}
Maintenant, ce qui suit fonctionne comme je le veux :
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}");
}