Durante a mudança para o novo .NET Core 3 IAsynsDisposable
, deparei-me com o seguinte problema.
O núcleo do problema: se DisposeAsync
lanç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 AsyncDispose
exceção-se lançada, e a exceção de dentro await using
somente se AsyncDispose
não for lançada .
No entanto, eu preferiria o contrário: obter a exceção do await using
bloco, se possível, e DisposeAsync
-exception somente se o await using
bloco for concluído com êxito.
Justificativa: imagine que minha classe D
funcione com alguns recursos de rede e assine algumas notificações remotas. O código interno await using
pode 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 DisposeAsync
não deve ser uma boa ideia.
Eu sei que existe o mesmo problema com o caso não assíncrono: a exceção finally
substitui 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 using
se possível? Minha pesquisa na internet não encontrou nem discutir esse problema.
CloseAsync
meio separado significa que preciso tomar precauções extras para fazê-lo funcionar. Se eu apenas colocá-lo no final do using
bloco, 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.
Dispose
sempre 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 AsyncDispose
deveria ser diferente.
DisposeAsync
fazer 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.
Close
método separado por esse motivo. Provavelmente, é aconselhável fazer o mesmo:CloseAsync
tentativas de fechar bem as coisas e fracassar.DisposeAsync
apenas faz o seu melhor e falha silenciosamente.