Example to those who still hesitate to implement both:
internal class Program
{
static void Main(string[] args)
{
foreach (var a in new B()){}
//IAsyncDisposable is not called - you leaking resources.
//No deadlocks in UI, no warning in compilation, nothing.
//So it is better to be on safe side and implement both
//because you never know how one will manage lifetime of your class.
}
public class B : IEnumerable, IAsyncEnumerable<object>
{
public IEnumerator GetEnumerator() => new A();
public IAsyncEnumerator<object> GetAsyncEnumerator(CancellationToken ct) => new A();
}
public class A : IAsyncEnumerator<object>, IEnumerator
{
public ValueTask DisposeAsync()
{
Console.WriteLine("Async Disposed");
return ValueTask.CompletedTask;
}
public bool MoveNext() => false;
public void Reset(){}
public ValueTask<bool> MoveNextAsync() => ValueTask.FromResult(false);
public object Current => null;
}
}
Conclusion
You can freely add support for async version only, but beware: some wraps, like foreach
or older versions of DI containers (Ninject, StructureMap, etc), code generators like RestSharp, or proxy generators like Castle.Proxy might not support IAsyncDisposable
. Failing to cast object to IDisposable
will present hard to catch bugs in your app. Whereas if you do implement it, the worst thing that could happen is deadlock in finally block (if you do it through sync-over-async).
In general, it is better to support both operations if you plan to make it public API or you don't have control over your class lifetime (like in DI containers or other widely known wrappers).
How to
There is full Microsoft example on how to implement both of them in inheritable class (non-sealed, like in your example) - https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-disposeasync#implement-both-dispose-and-async-dispose-patterns
class ExampleConjunctiveDisposableusing : IDisposable, IAsyncDisposable
{
IDisposable? _disposableResource = new MemoryStream();
IAsyncDisposable? _asyncDisposableResource = new MemoryStream();
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
Dispose(disposing: false);
#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize
GC.SuppressFinalize(this);
#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_disposableResource?.Dispose();
(_asyncDisposableResource as IDisposable)?.Dispose();
_disposableResource = null;
_asyncDisposableResource = null;
}
}
protected virtual async ValueTask DisposeAsyncCore()
{
if (_asyncDisposableResource is not null)
{
await _asyncDisposableResource.DisposeAsync().ConfigureAwait(false);
}
if (_disposableResource is IAsyncDisposable disposable)
{
await disposable.DisposeAsync().ConfigureAwait(false);
}
else
{
_disposableResource?.Dispose();
}
_asyncDisposableResource = null;
_disposableResource = null;
}
}