Muchas buenas respuestas aquí, pero aún me gustaría publicar mi perorata, ya que me encontré con el mismo problema y realicé algunas investigaciones. O pase a la versión TLDR a continuación.
El problema
Esperar la task
devolución Task.WhenAll
solo arroja la primera excepción de la AggregateException
almacenada task.Exception
, incluso cuando varias tareas han fallado.
Los documentos actuales paraTask.WhenAll
decir:
Si alguna de las tareas proporcionadas se completa en un estado de falla, la tarea devuelta también se completará en un estado de falla, donde sus excepciones contendrán la agregación del conjunto de excepciones sin empaquetar de cada una de las tareas proporcionadas.
Lo cual es correcto, pero no dice nada sobre el comportamiento de "desenvolvimiento" antes mencionado de cuándo se espera la tarea devuelta.
Supongo que los documentos no lo mencionan porque ese comportamiento no es específico deTask.WhenAll
.
Es simplemente que Task.Exception
es de tipo AggregateException
y para las await
continuaciones siempre se desenvuelve como su primera excepción interna, por diseño. Esto es excelente para la mayoría de los casos, porque generalmente Task.Exception
consta de una sola excepción interna. Pero 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]);
}
Aquí, una instancia de AggregateException
se desenvuelve en su primera excepción interna InvalidOperationException
exactamente de la misma manera que podríamos haberla tenido Task.WhenAll
. Podríamos haber dejado de observar DivideByZeroException
si no hubiéramos pasado task.Exception.InnerExceptions
directamente.
Stephen Toub de Microsoft explica la razón detrás de este comportamiento en el problema relacionado de GitHub :
El punto que estaba tratando de hacer es que se discutió en profundidad, hace años, cuando se agregaron originalmente. Originalmente hicimos lo que está sugiriendo, con la Task devuelta de WhenAll que contiene una única AggregateException que contiene todas las excepciones, es decir, task.Exception devolvería un contenedor AggregateException que contenía otra AggregateException que luego contenía las excepciones reales; luego, cuando se esperaba, se propagaría la AggregateException interna. La fuerte retroalimentación que recibimos que nos llevó a cambiar el diseño fue que a) la gran mayoría de estos casos tenían excepciones bastante homogéneas, de modo que propagar todo en un agregado no era tan importante, b) propagar el agregado luego rompió las expectativas en torno a las capturas para los tipos de excepción específicos, yc) para los casos en los que alguien quería el agregado, podía hacerlo explícitamente con las dos líneas como escribí. También tuvimos extensas discusiones sobre cuál debería ser el comportamiento de await con respecto a las tareas que contienen múltiples excepciones, y aquí es donde aterrizamos.
Otra cosa importante a tener en cuenta, este comportamiento de desenvolver es superficial. Es decir, solo desenvolverá la primera excepción de AggregateException.InnerExceptions
y la dejará allí, incluso si resulta ser una instancia de otra AggregateException
. Esto puede agregar otra capa de confusión. Por ejemplo, cambiemos WhenAllWrong
así:
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));
}
Una solución (TLDR)
Entonces, volviendo a await Task.WhenAll(...)
, lo que personalmente quería es poder:
- Obtenga una única excepción si solo se ha lanzado una;
- Obtenga un
AggregateException
si se ha lanzado más de una excepción de forma colectiva por una o más tareas;
- Evite tener que guardar el
Task
único para comprobarlo Task.Exception
;
- Propagar el estado de cancelación correctamente (
Task.IsCanceled
), ya que algo como esto no haría eso: Task t = Task.WhenAll(...); try { await t; } catch { throw t.Exception; }
.
He reunido la siguiente extensión para eso:
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();
}
}
Ahora, lo siguiente funciona de la manera que quiero:
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
. Si usara enTask.Wait
lugar deawait
en su ejemplo, lo atraparíaAggregateException