Returning Auto-Unregistering Objects From An Instance-Counting Factory

TL;DR: Automatically unregister a disposed instance at the factory by using a wrapper around the instance that executes a factory callback on being disposed.

Recently in my current project we were faced with the following requirements:

  1. There could only be one instance of a Disposable client instance from a proprietary DLL (let’s call it SingletonClient)
  2. Our own client type using the proprietary client (let’s call that MyClient) had to be instantiated multiple times

Up to here, the standard solution would be a factory owning the instance of SingletonClient and creating instances of MyClient on request. But there were more requirements calling for a more elaborate approach.

  1. When all instances of MyClient are not needed anymore, SingletonClient should be disposed, too. Then, when another instance of MyClient is requested, a new instance of SingletonClient should be created.

This is still pretty standard and can be solved with a good ol’ reference counter. The factory can count the created instances and the using library has to notify the factory of instances it no longer needs. The factory would upon this notification decrease the counter. The fact that the class disposing of the MyClient instance needs access to the factory as well is inconsequential since it should be the owner of an instance that disposes it, the owner usually also being the one having created it, thus having a reference to the factory anyway.

Up to this point, it could look a bit like this:

class SingletonClient : IDisposable
{
    public void DoStuff() { // do something }
    public void Dispose() { // clean up owned unmanaged resources }
}

public interface IMyClient : IDisposable
{
    void DoMyStuff();
}

sealed class MyClient : IMyClient
{
    private readonly SingletonClient singletonClient;

    MyClient(SingletonClient singletonClient)
    {
        this.singletonClient = singletonClient;
    }

    public void DoMyStuff() => singletonClient.DoStuff();
    public void Dispose() => this.singletonClient.Dispose();
}

public sealed class MyClientFactory : IDisposable
{
    SingletonClient singletonClient;
    readonly HashSet<MyClient> myClients = new HashSet<MyClient>();

    public IMyClient CreateClient()
    {
        singletonClient ??= new SingletonClient();
        var myClient = new MyClient(singletonClient);
        myClients.Add(myClient);
        return myClient;
    }

    public void ReturnClient(IMyClient myClient)
    {
        if (myClient is MyClient c)
        {
            c.Dispose();
            myClients.Remove(c);
            if (myClients.Count == 0)
            {
                singletonClient.Dispose();
                singletonClient = null;
            }
        }
    }

    public void Dispose()
    {
        singletonClient?.Dispose();
        singletonClient = null;
    }
}

It just so happened that the interface of MyClient (let’s call it IMyClient) was disposable for unrelated reasons. This caused ambiguity on whether the using code should dispose the IMyClient instance before returning it the factory or not. It also opened up the possibility to remove that ambiguity and make using the IMyClient instances more convenient. Instead of requiring the using code to explicitly return the IMyClient instance to the factory we can make the Dispose method of the returned IMyClient instance unregister itself on disposal.

For this to work without any changes to the existing IMyClient interface and MyClient implementation we wrapped MyClient in a decorator implementing IMyClient. The decorator Dispose method would call the Dispose method of the decoratee and then execute a callback it received from the factory at construction time to notify the factory of its disposal. The factory would keep track of all the instances it created and on the last existing instance unregistering, dispose its SingletonClient instance. The factory itself would also be disposable because of its semantics. The factory needed to be thread-safe, but not high-performing, so we could get away with a simple lock.

The end-result then looked like this:

public sealed class MyClientAutoReturningFactory : IDisposable
{
    SingletonClient singletonClient;
    readonly HashSet<MyClientAutoReturningDecorator> myClients = new HashSet<MyClientAutoReturningDecorator>();

    public IMyClient CreateClient()
    {
        singletonClient ??= new SingletonClient();
        var myClient = new MyClientAutoReturningDecorator(
            new MyClient(singletonClient),
            c => ReturnClient(c));
        myClients.Add(myClient);
        return myClient;
    }

    void ReturnClient(MyClientAutoReturningDecorator myClient)
    {
        myClients.Remove(myClient);
        if (myClients.Count == 0)
        {
            singletonClient.Dispose();
            singletonClient = null;
        }
    }

    public void Dispose()
    {
        singletonClient?.Dispose();
        singletonClient = null;
    }

    sealed class MyClientAutoReturningDecorator : IMyClient
    {
        readonly MyClient decoratee;
        readonly Action<MyClientAutoReturningDecorator> unregister;

        public MyClientAutoReturningDecorator(
            MyClient decoratee,
            Action<MyClientAutoReturningDecorator> unregister)
        {
            this.decoratee = decoratee;
            this.unregister = unregister;
        }

        [DebuggerHidden]
        public void Dispose()
        {
            decoratee.Dispose();
            unregister(this);
        }

        [DebuggerHidden]
        public void DoMyStuff()
        {
            decoratee.DoMyStuff();
        }
    }
}

Note that the ReturnClient method is now private. This removes the ambiguity between IMyClient.Dispose and ReturnClient for the caller. The caller can now dispose of the IMyClient instance as usual.

To make the convenience wrapping as transparent and unintrusive as possible it is a private nested class and the delegating methods are marked with DebuggerHidden.