Lors du passage aux nouveaux .NET Core 3 IAsynsDisposable
, je suis tombé sur le problème suivant.
Le cœur du problème: si DisposeAsync
lève une exception, cette exception cache toutes les exceptions await using
levées à l' intérieur de -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");
}
}
Ce qui se fait attraper, c'est l' AsyncDispose
exception-si elle est lancée, et l'exception de l'intérieur await using
uniquement si AsyncDispose
elle ne lance pas.
Je préfère cependant l'inverse: obtenir l'exception du await using
bloc si possible, et DisposeAsync
-exception uniquement si le await using
bloc s'est terminé avec succès.
Justification: Imaginez que ma classe D
fonctionne avec certaines ressources réseau et s'abonne à certaines notifications à distance. Le code à l'intérieur await using
peut faire quelque chose de mal et échouer le canal de communication, après quoi le code dans Dispose qui essaie de fermer la communication avec élégance (par exemple, se désinscrire des notifications) échouera également. Mais la première exception me donne les vraies informations sur le problème, et la seconde n'est qu'un problème secondaire.
Dans l'autre cas, lorsque la partie principale a traversé et que l'élimination a échoué, le vrai problème est à l'intérieur DisposeAsync
, donc l'exception de DisposeAsync
est pertinente. Cela signifie que la suppression de toutes les exceptions à l'intérieur DisposeAsync
ne devrait pas être une bonne idée.
Je sais qu'il y a le même problème avec le cas non asynchrone: l'exception dans finally
remplace l'exception dans try
, c'est pourquoi il n'est pas recommandé de le faire Dispose()
. Mais avec les classes d'accès au réseau, la suppression des exceptions dans les méthodes de fermeture ne semble pas du tout bonne.
Il est possible de contourner le problème avec l'aide suivante:
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 { }
}
}
}
et l'utiliser comme
await new D().UsingAsync(d =>
{
throw new ArgumentException("I'm inside using");
});
ce qui est un peu moche (et interdit les choses comme les premiers retours à l'intérieur du bloc using).
Existe-t-il une bonne solution canonique, avec await using
si possible? Ma recherche sur Internet n'a même pas permis de discuter de ce problème.
CloseAsync
moyen séparé , je dois prendre des précautions supplémentaires pour le faire fonctionner. Si je le mets juste à la fin de using
-block, il sera ignoré lors des premiers retours, etc. Mais l'idée semble prometteuse.
Dispose
a toujours été "Les choses ont peut-être mal tourné: faites simplement de votre mieux pour améliorer la situation, mais ne faites pas qu'aggraver", et je ne vois pas pourquoi cela AsyncDispose
devrait être différent.
DisposeAsync
faire de son mieux pour ranger mais pas jeter est la bonne chose à faire. Vous parliez de retours anticipés intentionnels , où un retour anticipé intentionnel pourrait par erreur contourner un appel à CloseAsync
: ce sont ceux qui sont interdits par de nombreuses normes de codage.
Close
méthode distincte pour cette raison. Il est probablement sage de faire de même:CloseAsync
tente de bien fermer les choses et jette l'échec.DisposeAsync
fait de son mieux et échoue silencieusement.