Following on from Parts 1 and 2, in this final part of the series, I move on to dealing with types that you do not have source control for and therefore cannot change directly to hide the Dispose method using the techniques I have described in the previous posts.
In the previous two parts of this series, I have made the assumption that you are able to amend the source code for types that implement IDisposable.
But what happens, if you don't have the source code? This is where some well known design patterns come in useful.
When trying to hide disposability from container consumers, the general principle (as shown in the previous parts of this series), is to use an interface that excludes the dispose method from its definition so that the consumer only receives the instance via the interface and not as the concrete class, thus hiding the Dispose method.
If we do not have the source code, we need to create an intermediary between the interface that we want to expose and the type that we want to use.
There are four classic design patterns , each a variation of the intermediary theme, that we can use together to achieve our goal.
I will not go into an in depth description of the patterns here as there are plenty of resources that can be a much better job, however, here is a brief overview.
You may already have another interface, either your own or some other third party that achieves the goal, but for which you do not have the source code or cannot change as it would break other dependencies. In this case, the adapter pattern is used to fill the gaps to make the two interfaces work with each other.
If the desired interface does not already exist, this is becomes the bridge pattern which does the same thing, but the interface design is within your control (whereas adapter uses an interface outside your control)
The core aim of our intermediary is to remove the Dispose method and in effect simplify the interface. If the third party has a number of members that you are not interested in or that need to be brought together into a single method, the Façade pattern can be used.
The proxy pattern is used to prevent direct access to an object. It usually has an identical interface to the class that it represents.
The decorator pattern is similar to the proxy pattern, but it can may additional functionality to enhance behaviour.
For our purposes, you are likely to use an adapter pattern if you do not have control over either interface, but more likely, you will be designing the interface to be used by consumers and therefore will write a custom class that brings together elements of the other three patterns, namely
- Bridge - we will be creating one between the desired interface to receive calls and the target interface
- Façade - we will be simplifying the interface to remove any members not needed
- Proxy - passing through calls to members of our class on to the instance that we are hiding
- Decorator - we may be taking the opportunity to do some additional work such as logging calls
In the example below we have a class SomeDisposable that we do not have the source code for. The class implements IDisposable and has multiple methods, but only one of interest, namely - DoSomething().
Rather than register the class directly, we want to wrap it with an intermediary that will
- Create an instance of DoSomething inside the constructor (as we do not want to register the DoSomething class with the container)
- Implement a simplified interface (façade) of just the one DoSomething() method
- Have a Dispose method (that is not exposed via the interface) that the container will call to proxy to the inner object's Dispose() method
- Decorate the inner DoSomething method with a call to the logger to log that the method has been called and when it has completed
Once we have the intermediary class, we can register this with the container in the StartUp class with the façade interface as the service type.
With this in place, we have managed to avoid consumers being able to dispose of the DoSomething singleton instance directly as it is hidden inside the intermediary class but the intermediary class (and in turn the inner instance) is still disposable by the container, but having put a façade in place, the consumer cannot call Dispose() directly.
That is the end of this series on preventing consumers causing problems by disposing of objects that may have a longer lifetime than the consumer when under the control of the .NET DI container.
I hope it has been of use.