Durante el cambio a los nuevos .NET Core 3 IAsynsDisposable, me encontré con el siguiente problema.
El núcleo del problema: si DisposeAsyncarroja 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 AsyncDisposeexcepción-si se lanza, y la excepción desde adentro await usingsolo si AsyncDisposeno se lanza.
Sin embargo, preferiría lo contrario: obtener la excepción del await usingbloque si es posible, y DisposeAsync-excepción solo si el await usingbloque terminó con éxito.
Justificación: Imagine que mi clase Dtrabaja con algunos recursos de red y se suscribe a algunas notificaciones remotas. El código interno await usingpuede 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 DisposeAsynces la relevante. Esto significa que suprimir todas las excepciones en el interior DisposeAsyncno debería ser una buena idea.
Sé que existe el mismo problema con el caso no asíncrono: la excepción en finallyanula 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 usingsi es posible? Mi búsqueda en internet no encontró ni siquiera discutir este problema.
CloseAsyncmedio 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.
Disposesiempre 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é AsyncDisposedebería ser diferente.
DisposeAsynclo 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.
Closemétodo separado por esta misma razón. Probablemente sea aconsejable hacer lo mismo:CloseAsyncintenta cerrar las cosas muy bien y arroja el fracaso.DisposeAsyncsimplemente hace lo mejor y falla en silencio.