stv
stv

Reputation: 1

How do you correctly implement IDispose and IAsyncDispose in a class that contains multiple disposable objects

I need to implement a class that manages the lifetime of multiple resources. Those resources may have different implementations of the dipose pattern:

  1. managed resources with both IDispose and IAsyncDispose
  2. managed resources with only IDispose
  3. managed resources with only IAsyncDispose
  4. managed resources without IDispose or IAsyncDispose
  5. unmanaged resources

The task now is to dispose all resources under control of that class correctly, no matter, if IDispose or IAsyncDispose (or even none of them) is called.

How do I dispose resources with only IAsyncDisposable when IDispose gets called? How do I dispose resource with only IDispose when IAsyncDispose gets called?

How can that behavior be implemented in an abstract base class (e.g. DisposableObject) where child classes only "register" their resources and the base class takes care of disposing?

Here is my first implementation attempt:

public abstract class DisposableObject : IDisposableObject, IAsyncDisposableObject
{
    private readonly DisposableCollection<IDisposable?> disposables = new();

    private readonly AsyncDisposableCollection<IAsyncDisposable?> asyncDisposables = new();

    public async ValueTask DisposeAsync()
    {
        await this.DisposeAsyncCore().ConfigureAwait(false);
        GC.SuppressFinalize(this);
    }

    public void Dispose()
    {
        this.DisposeCore();
        GC.SuppressFinalize(this);
    }

    public bool IsDisposed { get; private set; }

    bool IAsyncDisposableObject.IsDisposed => this.IsDisposed;

    bool IDisposableObject.IsDisposed => this.IsDisposed;

    protected void Manage(IDisposable? disposable)
    {
        this.disposables.Add(disposable);
    }

    protected void Manage(IAsyncDisposable? asyncDisposable)
    {
        this.asyncDisposables.Add(asyncDisposable);
    }

    private void DisposeCore()
    {
        if (this.IsDisposed) return;

        this.IsDisposed = true;

        this.disposables.Dispose();

        this.asyncDisposables.ForEach(asyncDisposable =>
        {
            // dispose hybrid resources sync
            if (asyncDisposable is IDisposable disposable) disposable.Dispose();
            // ???
            asyncDisposable?.DisposeAsync().AwaitResult();
        });
    }

    private async ValueTask DisposeAsyncCore()
    {
        if (this.IsDisposed) return;

        this.IsDisposed = true;

        await this.asyncDisposables.TryDisposeAsync().ConfigureAwait(false);

        await this.disposables.ForEachAsync(async disposable =>
        {
            // dispose hybrid resources async
            if (disposable is IAsyncDisposable asyncDisposable) await asyncDisposable.DisposeAsync().ConfigureAwait(false);
            // ???
            disposable?.Dispose();
        }).ConfigureAwait(false);
    }
}

Upvotes: 0

Views: 251

Answers (1)

Stephen Cleary
Stephen Cleary

Reputation: 457147

managed resources with both IDispose and IAsyncDispose

This is quite rare, but I'd recommend only calling DisposeAsync in that case.

managed resources without IDispose or IAsyncDispose

You don't do anything in this case.

unmanaged resources

The proper solution in this case is to create a wrapper class just for the unmanaged resource that implements IDisposable. Attempting to support both unmanaged resources and managed disposable resources is a fool's errand, adding considerable complexity for no benefit.

The task now is to dispose all resources under control of that class correctly, no matter, if IDispose or IAsyncDispose (or even none of them) is called.

If the DisposeAsync implementations support disposal from arbitrary threads, then this may be possible. Of course, ensuring disposal even if no dispose is called is (again) a fool's errand and not possible. The wrapper classes handling the unmanaged resource will take care of that scenario in their finalizers.

How do I dispose resources with only IAsyncDisposable when IDispose gets called?

This is the tricky one. If the implementations allow calling DisposeAsync from any thread, then you can toss it on the thread pool and block on it.

How do I dispose resource with only IDispose when IAsyncDispose gets called?

Just call Dispose?

How can that behavior be implemented in an abstract base class (e.g. DisposableObject) where child classes only "register" their resources and the base class takes care of disposing?

Why would you ever want to do this? Composition wins over inheritance an all but a handful of design scenarios. That's definitely the case here.

I have a Disposables library that mostly does what you want. It just doesn't do the funky IDisposable.Dispose => IAsyncDisposable.DisposeAsync thing, so you'd have to do that yourself:

public sealed class MyDisposableCollection : IDisposable, IAsyncDisposable
{
    private readonly CollectionAsyncDisposable _disposables = new();
    public bool TryAdd(IAsyncDisposable disposable) => _disposables.TryAdd(disposable);
    public bool TryAdd(IDisposable disposable) => _disposables.TryAdd(disposable.ToAsyncDisposable());

    public ValueTask DisposeAsync() => _disposables.DisposeAsync();
    public void Dispose()
    {
      Task.Run(async () => await _disposables.DisposeAsync())
          .GetAwaiter().GetResult(); 
    }
}

Upvotes: 0

Related Questions