Durante a mudança para o novo .NET Core 3 IAsynsDisposable, deparei-me com o seguinte problema.
O núcleo do problema: se DisposeAsynclança uma exceção, essa exceção oculta todas as exceções lançadas 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");
}
}
O que está sendo pego é a AsyncDisposeexceção-se lançada, e a exceção de dentro await usingsomente se AsyncDisposenão for lançada .
No entanto, eu preferiria o contrário: obter a exceção do await usingbloco, se possível, e DisposeAsync-exception somente se o await usingbloco for concluído com êxito.
Justificativa: imagine que minha classe Dfuncione com alguns recursos de rede e assine algumas notificações remotas. O código interno await usingpode fazer algo errado e falhar no canal de comunicação; depois disso, o código em Dispose, que tenta fechar normalmente a comunicação (por exemplo, cancelar a assinatura das notificações) também falharia. Mas a primeira exceção fornece informações reais sobre o problema, e a segunda é apenas um problema secundário.
No outro caso, quando a parte principal foi executada e a eliminação falhou, o problema real está dentro DisposeAsync, portanto a exceção DisposeAsyncé a relevante. Isso significa que apenas suprimir todas as exceções internas DisposeAsyncnão deve ser uma boa ideia.
Eu sei que existe o mesmo problema com o caso não assíncrono: a exceção finallysubstitui a exceção try, é por isso que não é recomendável lançar Dispose(). Mas, com as classes de acesso à rede, suprimir exceções nos métodos de fechamento não parece bom.
É possível solucionar o problema com o seguinte auxiliar:
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 { }
}
}
}
e use-o como
await new D().UsingAsync(d =>
{
throw new ArgumentException("I'm inside using");
});
que é meio feio (e não permite coisas como retornos antecipados dentro do bloco using).
Existe uma solução boa e canônica, await usingse possível? Minha pesquisa na internet não encontrou nem discutir esse problema.
CloseAsyncmeio separado significa que preciso tomar precauções extras para fazê-lo funcionar. Se eu apenas colocá-lo no final do usingbloco, ele será ignorado nos retornos antecipados etc. (é isso que gostaríamos que acontecesse) e exceções (é isso que gostaríamos que acontecesse). Mas a ideia parece promissora.
Disposesempre foi "As coisas podem ter dado errado: basta fazer o seu melhor para melhorar a situação, mas não piorar", e não vejo por que AsyncDisposedeveria ser diferente.
DisposeAsyncfazer o melhor para arrumar, mas não jogar é a coisa certa a fazer. Você estava falando sobre retornos antecipados intencionais , nos quais um retorno antecipado intencional pode ignorar erroneamente uma chamada para CloseAsync: esses são os proibidos por muitos padrões de codificação.
Closemétodo separado por esse motivo. Provavelmente, é aconselhável fazer o mesmo:CloseAsynctentativas de fechar bem as coisas e fracassar.DisposeAsyncapenas faz o seu melhor e falha silenciosamente.