I've been familiarizing myself with some of the things (that are planned to be) added in C# 8 & .NET Core 3.0, and am unsure on the correct way to implement IAsyncDisposable (at time of writing, this link has literally no guidance).
In particular, it is unclear to me what to do in the case when an instance isn't explicitly disposed - that is, it isn't wrapped in an async using(...)
and .DisposeAsync()
isn't explicitly called.
My first thought was to do that same thing I'd do when implementing IDisposable:
DisposeAsync()
implementation calls a DisposeAsync(bool disposing)
with disposing: true
~MyType()
) that calls DisposeAsync(disposing: false)
DisposeAsync(bool disposing)
actually frees and/or disposes everything, and suppresses finalizing if disposing == true
.My concerns are that there's nothing to await the results of DisposeAsync(bool)
in the finalizer, and explicitly waiting in a finalizer seems really really dangerous.
Of course "just leak" also seems less than ideal.
To make this concrete, here's a (simplified) example class that does have a finalizer:
internal sealed class TestAsyncReader: IAsyncDisposable
{
private bool IsDisposed => Inner == null;
private TextReader Inner;
internal TestAsyncReader(TextReader inner)
{
Inner = inner;
}
// the question is, should this exist?
~TestAsyncReader()
{
DisposeAsync(disposing: false);
}
private ValueTask DisposeAsync(bool disposing)
{
// double dispose is legal, but try and do nothing anyway
if (IsDisposed)
{
return default;
}
// should a finalizer even exist?
if (disposing)
{
GC.SuppressFinalize(this);
}
// in real code some resources explicitly implement IAsyncDisposable,
// but for illustration purposes this code has an interface test
if (Inner is IAsyncDisposable supportsAsync)
{
var ret = supportsAsync.DisposeAsync();
Inner = null;
return ret;
}
// dispose synchronously, which is uninteresting
Inner.Dispose();
Inner = null;
return default;
}
public ValueTask DisposeAsync()
=> DisposeAsync(disposing: true);
}
So, is there any guidance around proper handling of leaked IAsyncDisposable
instances?
Basing on examples of how it's implemented inside .NET Core classes (like here) and some recommendations from there, I'd say that when you need to implement IAsyncDisposable
, the good practice would be to implement both IAsyncDisposable
and IDisposable
. In this case IAsyncDisposable
will be only responsible for explicit scenarios when asyncronus disposal is needed, while IDisposable
is supposed to be implemented as usual according to disposable pattern practices and it's going to serve all fallback scenarios including those when things come to finalization. Thus you don't need to have anything like DisposeAsync(bool disposing)
- the asynchronous disposal cannot and shouldn't happen in a finalizer. The only bad news that you'll have to support both paths for resources reclaiming (synchronous and asynchronous).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With