In this post I will be discussing the traps that can catch you out by potentially creating memory leaks when registering types that implement the IDisposable interface as services with the out-of-the-box .NET Dependency Injection container.
Typically, a type will implement the IDisposable interface when it holds unmanaged resources that need to be released or to free up memory.
More information about cleaning up resource can be found on Microsoft Docs
To keep things simple for the rest of this post, I will be referring to instances of types that implement IDisposable as "Disposable Objects".
Outside of dependency injection, if you create an instance of such a type, it is your responsibly to call the Dispose method on the class to initiate the release of unmanaged resources.
This can be done either
- explicitly by calling the Dispose method (or some alias such as Close), typically in a finally block of a try-catch construct or
- implicitly via the using construct to automatically dispose the object.
The two approaches are documented on the Microsoft Docs site.
As a general rule, if the Dependency Injection container creates an instance of the disposable object, it will clean up when the instance lifetime (transient, scoped or singleton) expires (E.g. for scoped instances in ASP.NET Core, this will be at the end of the request/response lifetime but for singletons, it is when the container itself is disposed).
The following table (based on the table in the Microsoft Docs page) shows which registration methods will trigger the container to automatically dispose of the object.
|Add<>(sp => new )||Yes|
As you can see from the table above, the three most common methods for adding services, where the container itself is responsible for creating the instance, will automatically dispose of the object at the appropriate time.
However, the last two methods do not dispose of the object. Why? It's because in these methods, the objects have been directly instantiated with a new keyword and therefore, the container has not been responsible for creating the object.
Whilst they look similar to the third method, the difference is that the instance in that method has been created within the context of a lambda expression which is within the control of the container and therefore in the container's control.
In the last two methods, the object could be created at the time of registration (by using the new statement) but then again, it may have been created outside these methods (either within the scope of the ConfigureServices method in the StartUp class, or at a class level) and therefore, the container cannot possibly know of where the object has been created, the scope of its reference, and where else it may be used. Without this understanding, it cannot safely dispose of the object as this may throw an ObjectDisposedException if referenced elsewhere in code after the container has disposed of it.
I will come on to dealing with ensuring these objects referenced in these last two methods can be disposed of correctly in Part 2.
The first method in the table above is the simplest way to register a type. Consumers will request an instance of the object and make use of it.
However, if the type implements IDisposable, this means that the Dispose method is available to the consumer to call. This has repercussions depending on the lifetime that the dependency has been registered as.
For transients that have been created specifically to be injected into the consuming class, it is not the end of the world. If dispose is called on a transient, the only place that will suffer is the consuming class (and anything it passes the reference to) as any subsequent references to the object (or to be more specific, members in the type that check the disposed status) are likely to result in an ObjectDisposedException (this will depend on the implementation of the injected class).
For scoped and singleton lifetimes, things become more complicated as the object has a lifetime beyond the consumer class. If the consuming class calls Dispose and another consumer then also makes use of a member on the disposed class, that other consumer is likely to receive an ObjectDisposedException.
Therefore, we want to ensure that the Dispose method on the registered class is somehow hidden from the consumer.
There are several ways of hiding the Dispose method which are considered below
The quick (and dirty) way of hiding the Dispose method that exists on a class is to change the Dispose method's declaration from a public method to an explicit interface declaration (as shown below) so that it can only be called by casting the object to IDisposable.
It should, however, be recognised that this is just obfuscating the availability of the Dispose method. It does not truly hide it as the consumer may be aware that the type implements IDisposable and explicitly cast the object and call Dispose.
This is where extracting out other interfaces comes to our rescue when it comes to dependency injection.
If we define an interface that has all the public members of our class except for the Dispose method and only make the object available by registering it in the DI container with the limited interface as the service, this will make it harder (but not completely impossible) for the consumer of the object to dispose of the object as the concrete type is only known to the container registration (unless the consumer uses GetType() of course, but that is splitting hairs and in many ways negates the whole point of using the container).
Of course, following the Interface Segregation Principle from SOLID, this interface may be broken down into smaller interfaces which the class registered against.
In Part 2 of this series on IDisposable in Dependency Injection, I will move on to dealing with those objects that the container will not dispose of for you.