Lors du passage aux nouveaux .NET Core 3 IAsynsDisposable, je suis tombé sur le problème suivant.
Le cœur du problème: si DisposeAsynclève une exception, cette exception cache toutes les exceptions await usinglevé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' AsyncDisposeexception-si elle est lancée, et l'exception de l'intérieur await usinguniquement si AsyncDisposeelle ne lance pas.
Je préfère cependant l'inverse: obtenir l'exception du await usingbloc si possible, et DisposeAsync-exception uniquement si le await usingbloc s'est terminé avec succès.
Justification: Imaginez que ma classe Dfonctionne avec certaines ressources réseau et s'abonne à certaines notifications à distance. Le code à l'intérieur await usingpeut 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 DisposeAsyncest pertinente. Cela signifie que la suppression de toutes les exceptions à l'intérieur DisposeAsyncne 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 finallyremplace 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 usingsi possible? Ma recherche sur Internet n'a même pas permis de discuter de ce problème.
CloseAsyncmoyen 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.
Disposea 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 AsyncDisposedevrait être différent.
DisposeAsyncfaire 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.
Closeméthode distincte pour cette raison. Il est probablement sage de faire de même:CloseAsynctente de bien fermer les choses et jette l'échec.DisposeAsyncfait de son mieux et échoue silencieusement.