Viele gute Antworten hier, aber ich möchte trotzdem meine Beschimpfungen veröffentlichen, da ich gerade auf dasselbe Problem gestoßen bin und einige Nachforschungen angestellt habe. Oder springen Sie zur folgenden TLDR-Version.
Das Problem
Das Warten auf die taskRückgabe durch löst Task.WhenAllnur die erste Ausnahme der AggregateExceptiongespeicherten task.ExceptionDaten aus, selbst wenn mehrere Aufgaben fehlerhaft sind.
Die aktuellen Dokumente zum BeispielTask.WhenAll :
Wenn eine der bereitgestellten Aufgaben in einem fehlerhaften Zustand ausgeführt wird, wird die zurückgegebene Aufgabe auch in einem fehlerhaften Zustand abgeschlossen, in dem ihre Ausnahmen die Aggregation der nicht ausgepackten Ausnahmen aus jeder der bereitgestellten Aufgaben enthalten.
Das ist richtig, sagt aber nichts über das oben erwähnte "Auspacken" -Verhalten aus, wenn die zurückgegebene Aufgabe erwartet wird.
Ich nehme an, die Dokumente erwähnen es nicht, weil dieses Verhalten nicht spezifisch istTask.WhenAll .
Es ist einfach Task.Exceptionvom Typ AggregateExceptionund für awaitFortsetzungen wird es immer als erste innere Ausnahme von Natur aus ausgepackt. Dies ist in den meisten Fällen großartig, da es normalerweise Task.Exceptionnur eine innere Ausnahme gibt. Beachten Sie jedoch diesen 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]);
}
Hier wird eine Instanz von genau so AggregateExceptionauf ihre erste innere Ausnahme entpackt, InvalidOperationExceptionwie wir es vielleicht hatten Task.WhenAll. Wir hätten es nicht beobachten können, DivideByZeroExceptionwenn wir nicht task.Exception.InnerExceptionsdirekt durchgegangen wären .
Stephen Toub von Microsoft erklärt den Grund für dieses Verhalten im zugehörigen GitHub-Problem :
Der Punkt, den ich ansprechen wollte, ist, dass er vor Jahren ausführlich besprochen wurde, als diese ursprünglich hinzugefügt wurden. Wir haben ursprünglich das getan, was Sie vorschlagen, wobei die von WhenAll zurückgegebene Aufgabe eine einzelne AggregateException enthielt, die alle Ausnahmen enthielt, dh task.Exception einen AggregateException-Wrapper zurückgab, der eine andere AggregateException enthielt, die dann die tatsächlichen Ausnahmen enthielt. Wenn es dann erwartet wurde, wurde die innere AggregateException weitergegeben. Das starke Feedback, das uns veranlasste, das Design zu ändern, war, dass a) die überwiegende Mehrheit dieser Fälle ziemlich homogene Ausnahmen aufwies, so dass die Vermehrung aller Aggregate nicht so wichtig war, b) die Vermehrung des Aggregats die Erwartungen an die Fänge brach für die spezifischen Ausnahmetypen und c) für Fälle, in denen jemand das Aggregat haben wollte, konnte er dies explizit mit den beiden Zeilen tun, wie ich es geschrieben habe. Wir hatten auch ausführliche Diskussionen darüber, wie sich das Warten auf Aufgaben mit mehreren Ausnahmen verhalten sollte, und hier sind wir gelandet.
Eine andere wichtige Sache zu beachten, ist dieses Auspackverhalten flach. Das heißt, es wird nur die erste Ausnahme auspacken AggregateException.InnerExceptionsund dort belassen , selbst wenn es sich zufällig um eine Instanz einer anderen handelt AggregateException. Dies kann noch eine weitere Verwirrungsebene hinzufügen. Ändern wir zum Beispiel Folgendes 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));
}
Eine Lösung (TLDR)
Zurück zu await Task.WhenAll(...), was ich persönlich wollte, ist in der Lage zu sein:
- Holen Sie sich eine einzelne Ausnahme, wenn nur eine ausgelöst wurde.
- Erhalten Sie eine,
AggregateExceptionwenn mehr als eine Ausnahme von einer oder mehreren Aufgaben gemeinsam ausgelöst wurde.
- Vermeiden Sie es, das
Taskeinzige zu speichern, um es zu überprüfen Task.Exception.
- Verbreiten Sie den Stornierungsstatus ordnungsgemäß (
Task.IsCanceled), da so etwas das nicht tun würde : Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }.
Dafür habe ich folgende Erweiterung zusammengestellt:
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();
}
}
Nun funktioniert Folgendes so, wie ich es möchte:
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. Wenn SieTask.Waitanstelle vonawaitin Ihrem Beispiel verwendet, würden Sie fangenAggregateException