ここでは多くの良い答えがありますが、同じ問題に遭遇していくつかの調査を行ったばかりなので、私はまだ怒りを投稿したいと思います。または、以下のTLDRバージョンにスキップしてください。
問題
複数のタスクが失敗した場合でも、task
によって返されるのを待つTask.WhenAll
と、にAggregateException
格納されている最初の例外のみがスローされtask.Exception
ます。
言うためTask.WhenAll
の現在のドキュメント:
提供されたタスクのいずれかがフォルト状態で完了すると、返されたタスクもフォルト状態で完了します。その例外には、提供された各タスクからのラップされていない例外のセットの集約が例外に含まれます。
これは正しいですが、返されたタスクが待機するときの前述の「アンラップ」動作については何も述べていません。
私は、その動作はに固有ではないためTask.WhenAll
、ドキュメントには記載されていないと思います。
それは単にTask.Exception
タイプでAggregateException
あり、await
継続のために、最初の内部例外として、設計により常にアンラップされます。通常Task.Exception
は1つの内部例外のみで構成されるため、これはほとんどの場合に最適です。しかし、このコードを考えてみましょう:
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のStephen Toubが、関連するGitHubの問題でこの動作の背後にある理由を説明しています。
私が作ろうとしたのは、何年も前にこれらが最初に追加されたときに、それが詳細に議論されたことです。私たちは当初、すべての例外を含む単一のAggregateExceptionを含むWhenAllから返されたTaskで提案したことを行いました。つまり、task.Exceptionは、実際の例外を含む別のAggregateExceptionを含むAggregateExceptionラッパーを返します。その後、それが待機されると、内部のAggregateExceptionが伝搬されます。デザインを変更するきっかけとなった強いフィードバックは、a)そのようなケースの大部分がかなり均一な例外を抱えていたため、すべてを集約して伝播することはそれほど重要ではなかった、b)集約を伝播してから、漁獲に関する期待を裏切った特定の例外タイプについては、c)誰かが集計を必要としていた場合、私が書いたように、2行で明示的に集計を行うことができます。また、複数の例外を含むタスクに関して、awaitの動作がどうなるかについて、広範な議論がありました。
注意すべきもう1つの重要な点として、このアンラップ動作は浅いです。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(...)
、私が個人的に欲しかったのは、次のことができるようになることです。
- 1つだけスローされた場合、1つの例外を取得します。
AggregateException
1つ以上のタスクによって複数の例外がまとめてスローされた場合は、を取得します。
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