Durante el cambio a los nuevos .NET Core 3 IAsynsDisposable
, me encontré con el siguiente problema.
El núcleo del problema: si DisposeAsync
arroja una excepción, esta excepción oculta cualquier excepción lanzada dentro de await using
-block.
class Program
{
static async Task Main()
{
try
{
await using (var d = new D())
{
throw new ArgumentException("I'm inside using");
}
}
catch (Exception e)
{
Console.WriteLine(e.Message); // prints I'm inside dispose
}
}
}
class D : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await Task.Delay(1);
throw new Exception("I'm inside dispose");
}
}
Lo que queda atrapado es la AsyncDispose
excepción-si se lanza, y la excepción desde adentro await using
solo si AsyncDispose
no se lanza.
Sin embargo, preferiría lo contrario: obtener la excepción del await using
bloque si es posible, y DisposeAsync
-excepción solo si el await using
bloque terminó con éxito.
Justificación: Imagine que mi clase D
trabaja con algunos recursos de red y se suscribe a algunas notificaciones remotas. El código interno await using
puede hacer algo mal y fallar el canal de comunicación, luego el código en Dispose que intenta cerrar la comunicación con gracia (por ejemplo, cancelar la suscripción a las notificaciones) también fallará. Pero la primera excepción me da la información real sobre el problema, y la segunda es solo un problema secundario.
En el otro caso, cuando la parte principal se ejecutó y la eliminación falló, el verdadero problema está dentro DisposeAsync
, por lo que la excepción DisposeAsync
es la relevante. Esto significa que suprimir todas las excepciones en el interior DisposeAsync
no debería ser una buena idea.
Sé que existe el mismo problema con el caso no asíncrono: la excepción en finally
anula la excepción en try
, por eso no se recomienda incluir Dispose()
. Pero con las clases de acceso a la red que suprimen las excepciones en los métodos de cierre no se ve nada bien.
Es posible solucionar el problema con el siguiente ayudante:
static class AsyncTools
{
public static async Task UsingAsync<T>(this T disposable, Func<T, Task> task)
where T : IAsyncDisposable
{
bool trySucceeded = false;
try
{
await task(disposable);
trySucceeded = true;
}
finally
{
if (trySucceeded)
await disposable.DisposeAsync();
else // must suppress exceptions
try { await disposable.DisposeAsync(); } catch { }
}
}
}
y úsalo como
await new D().UsingAsync(d =>
{
throw new ArgumentException("I'm inside using");
});
lo cual es un poco feo (y no permite cosas como retornos tempranos dentro del bloque de uso).
¿Existe una buena solución canónica, await using
si es posible? Mi búsqueda en internet no encontró ni siquiera discutir este problema.
CloseAsync
medio separado significa que debo tomar precauciones adicionales para que funcione. Si lo pongo al final de using
-block, se omitirá en los primeros retornos, etc. (esto es lo que queremos que suceda) y excepciones (esto es lo que queremos que suceda). Pero la idea parece prometedora.
Dispose
siempre ha sido "Las cosas podrían haber salido mal: solo haz tu mejor esfuerzo para mejorar la situación, pero no la empeores", y no veo por qué AsyncDispose
debería ser diferente.
DisposeAsync
lo mejor para ordenar pero no tirar es lo correcto. Estaba hablando de devoluciones anticipadas intencionales , donde una devolución anticipada intencional podría omitir por error una llamada a CloseAsync
: esas son las prohibidas por muchos estándares de codificación.
Close
método separado por esta misma razón. Probablemente sea aconsejable hacer lo mismo:CloseAsync
intenta cerrar las cosas muy bien y arroja el fracaso.DisposeAsync
simplemente hace lo mejor y falla en silencio.