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 task
Rückgabe durch löst Task.WhenAll
nur die erste Ausnahme der AggregateException
gespeicherten task.Exception
Daten 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.Exception
vom Typ AggregateException
und für await
Fortsetzungen wird es immer als erste innere Ausnahme von Natur aus ausgepackt. Dies ist in den meisten Fällen großartig, da es normalerweise Task.Exception
nur 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 AggregateException
auf ihre erste innere Ausnahme entpackt, InvalidOperationException
wie wir es vielleicht hatten Task.WhenAll
. Wir hätten es nicht beobachten können, DivideByZeroException
wenn wir nicht task.Exception.InnerExceptions
direkt 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.InnerExceptions
und 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,
AggregateException
wenn mehr als eine Ausnahme von einer oder mehreren Aufgaben gemeinsam ausgelöst wurde.
- Vermeiden Sie es, das
Task
einzige 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.Wait
anstelle vonawait
in Ihrem Beispiel verwendet, würden Sie fangenAggregateException