Implementing IDisposable (the Disposable Pattern) as a service (class member)
Asked Answered
W

1

1

The Disposable pattern is one that is re-implemented on a per class basis. So, I was looking for a way to generalize it. The problem I ran into a few years ago is that, even if you implement it as class itself, you can't have an object derive from both your Disposable implementation and from another class (C# doesn't support multi-inheritance).

The question is, how do you make a generic way to have the Disposable pattern implemented so you don't need to write it explicitly per class that implements IDisposable?

Here is the standard Disposable pattern that is generated for you by Visual Studio (VS 2015).

public class TestClass : IDisposable {
    #region IDisposable Support

    private bool disposedValue = false; // To detect redundant calls

    protected virtual void Dispose(bool disposing) {
        if (!disposedValue) {
            if (disposing) {
                // TODO: dispose managed state (managed objects).
            }

            // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
            // TODO: set large fields to null.

            disposedValue = true;
        }
    }

    // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
    // ~DisposeTest() {
    //   // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
    //   Dispose(false);
    // }

    // This code added to correctly implement the disposable pattern.
    public void Dispose() {
        // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
        Dispose(true);
        // TODO: uncomment the following line if the finalizer is overridden above.
        // GC.SuppressFinalize(this);
    }

    #endregion
}
Wendeline answered 4/9, 2015 at 21:5 Comment(2)
Feels opinion based - if you favor containment over inheritance and never mix native and manged classes (create separate managed wrappers for native resources like SafeHandle) the pattern get reduced to single method public void Dispose()... So I'd say - if you need full IDisposable pattern you may be building to deep hierarchy of classes.Niu
@AlexeiLevenkov Microsoft made that pattern many years ago. If you'd like the full explanation for it, you can read that here: joeduffyblog.com/2005/04/08/… I don't tend to use many unmanaged resources or classes that have unmanaged resources. But when I do, I just want it to be as simple and straightforward as possible.Wendeline
W
3

My implementation

So, here is the solution I came up with.

public class DisposeService<T> where T : IDisposable {
    private readonly T _disposee;
    public Action<T> ManagedAction { get; set; }
    public Action<T> UnmanagedAction { get; set; }

    public DisposeService(T disposee, Action<T> managedAction = null, Action<T> unmanagedAction = null) {
        _disposee = disposee;
        ManagedAction = managedAction;
        UnmanagedAction = unmanagedAction;
    }

    private bool _isDisposed;

    public void Dispose(bool isDisposing) {
        if (_isDisposed) return;
        if (isDisposing && ManagedAction != null) {
            ManagedAction(_disposee);
        }
        var hasUnmanagedAction = UnmanagedAction != null;
        if (hasUnmanagedAction) {
            UnmanagedAction(_disposee);
        }
        _isDisposed = true;
        if (isDisposing && hasUnmanagedAction) {
            GC.SuppressFinalize(_disposee);
        }
    }
}

This class allows you to create a DisposableService<> member for your class that implements IDisposable. Here is an example on how to use it when you only have managed resources.

public class TestClass : IDisposable {
    protected readonly DisposeService<TestClass> DisposeService;
    private readonly SafeHandle _handle;

    public TestClass() {
        DisposeService = new DisposeService<TestClass>(this, ps => { if (_handle != null) _handle.Dispose(); });
        _handle = new SafeFileHandle(IntPtr.Zero, true);
    }

    public void Dispose() {
        DisposeService.Dispose(true);
    }
}

How it works

  • The DisposeService will run it's Dispose on your object's Dispose.
  • The DisposeService's dispose will run your Managed and Unmanaged Action that you provide on initialization (or update in derived classes).
  • The GC.SuppressFinalize will run automatically if an UnmanagedAction is provided.
  • Always make sure to create the DisposableService<> as the first action of your constructor.

So, here is an example of using this service with unmanaged resources.

public class TestClass : IDisposable {
    protected readonly DisposeService<TestClass> DisposeService;
    private readonly SafeHandle _handle;

    public TestClass() {
        DisposeService = new DisposeService<TestClass>(this,
            ps => { if (_handle != null) _handle.Dispose(); },
            ps => { /* Free unmanaged resources here */ });
        _handle = new SafeFileHandle(IntPtr.Zero, true);
    }

    public void Dispose() {
        DisposeService.Dispose(true);
    }

    ~TestClass() {
        DisposeService.Dispose(false);
    }
}

And, an example of making a derived class from the class above.

public class TestClassDerived : TestClass, IDisposable {
    private readonly SafeHandle _derivedHandle;

    public TestClassDerived() {
        // Copy the delegate for the base's managed dispose action.
        var baseAction = DisposeService.ManagedAction;
        // Update the managed action with new disposes, while still calling the base's disposes.
        DisposeService.ManagedAction = ps => {
            if (_derivedHandle != null) {
                _derivedHandle.Dispose();
            }
            baseAction(ps);
        };
        _derivedHandle = new SafeFileHandle(IntPtr.Zero, true);
    }
}

Easy peasy lemon squeezy. You keep the reference to the base's delegate and call it as part of the derived class's delegate.

Overall, should be cleaner then managing that procedural region of blarg that Microsoft has been providing since 2005...

Edit: I thought the 'this' being passed in the constructor might be a concern. But, it doesn't seem to be: Is it a bad practice to pass "this" as an argument? Just remember to put the null checks in your actions so you don't try to Dispose something that is null. :-)

Wendeline answered 4/9, 2015 at 21:5 Comment(3)
This looks like a very interesting solution and thank you for sharing. Doesn't really seem like you had a question for stackoverflow though :P.Buchheim
@dustmouse I did, and I searched, and searched for an implementation, but was unable to find one. I realized how I could implement it, so instead of asking an waiting for a response, I just made it myself, but posted my implementation so others could use it. :-PWendeline
Fair enough. I upvoted it anyway because I think it's a nice solution.Buchheim

© 2022 - 2024 — McMap. All rights reserved.