Introduction
I’ve been working with the IOC container pattern since 2013 and it is now baked into .Net Core. Obviously, the IOC container is the standard going forward (assuming something more fantastic doesn’t supersede it). I have a selfish reason for liking the IOC container pattern: I am a proponent of unit tests.
Though I still see code written that is based on the IOC container pattern, without a unit test in sight, at least we are half-way there. If a project is built around the IOC pattern, I (and any other developer) can add unit tests with little difficulty. Breaking dependencies are the most difficult part of unit testing and the IOC container performs that task.
In this post, I’m going to talk a little bit about the change in memory profile that occurs when using the IOC container pattern. I believe that developers are generally unaware of what is happening and how to reduce the memory problems that can occur.
Large Classes
I’m sure you’re reading the title to this block of text and thinking that I’m running the train right off the rails. I can assure you, that this will all come together.
One issue I see continuing is the practice of creating a class and then adding more and more methods to it. Each class should have a distinct purpose and perform that purpose well. What happens is that the purpose starts out generic like a “controller.” This occurs when creating an API. It starts out small. One controller that performs a purpose, then endpoints are added and over time there are hundreds of endpoints. Each endpoint is categorized as part of that controller, but the whole controller class becomes large.
This phenomenon occurs mostly on controllers because it’s difficult to break up a controller using refactoring techniques. Nobody wants to rename all the endpoints to move them to another controller. It’s risky to rename an endpoint in use. The developer must account for any other APIs or code that is using that endpoint and change the URL. Therefore, the controller class grows.
As the controller grows, so does the number of injected classes and interfaces. It’s easy to keep adding to the parameters of the constructor, and then adding an interface and class name pair to the IOC container list in the Setup.cs file. Eventually, the constructor has injections for twenty or more classes.
Beware of Object Creation Time
Here’s where the issue starts. Every time an object is created by the IOC container, all objects that are injected are created at the same time. For each injected class, those objects that are created may also have injections into their constructor. Those are created as well.
If you have a controller and each endpoint uses a different object that is injected (just a thought experiment here), then an IOC container patterned project will create all objects whether they are used or not. For a non-IOC container project, objects are created one at a time and only created when they are used. I’m not advocating the creation of tightly coupled code here. I’m just pointing out the fact that, in an IOC container pattern, all objects are created for the controller, even if only one of them is used by the endpoint.
That means that there is a penalty to pay for using the IOC container. If a web request comes in to access an endpoint, each object that is used by the controller of that endpoint must be created when the controller is created. Then the endpoint is executed and all the objects go out of scope and are torn down (one instance per request).
There are possible efficiencies as well. These do not necessarily balance out, but it should be recognized that an object created per scope can be more efficient in an IOC container situation. This occurs when an object is used by more than one class during the scope of the request. The IOC container will re-use the same object for the lifetime of the scope. Transients are never reused and there are no economies with this scoping method.
What’s the Problem?
Let’s get down to the brass tacks of this problem. If you build a system and keep adding injections to classes that have already been in use, now you’re adding more objects to take up memory for the class that you are modifying. You need to be aware of this issue. But, hey! Memory is cheap. That’s true, but I’ve already experienced one disaster caused by this problem.
There was a legacy system that I worked on and the decision (not mine) was made to refactor the entire DTO section to use an IOC container. The goal was a good one, except the entire system was already running on hardware that was maxed out. Contractors were hired to perform the labor required to convert the code to use the repository pattern in a system using Linq2SQL. The intent was to get the IOC container in place and then replace Linq2SQL with something newer (all at once).
Now here’s what actually happened. The website that ran this DTO section would chew up memory as soon as it started. The entire memory profile of the application had radically changed and it was impossible to make it work on the existing hardware. The amount of memory used by the system made it too expensive for that company to afford and the decision was made to abandon that entire branch of work that had been done. This was several months of contract labor that was thrown out the window because an awareness of the memory usage of an IOC container was not known. I have to admit some guilt in not thinking about this issue as well. I had known of the “issue” from previous work with WebApi’s but never ran into something so disastrous.
Solution?
First, keep your classes small and try to reduce the number of injected classes. If you have a lot of methods that each use a different set of injected objects, then memory is being wasted. Second, if you are refactoring from an older system to add an IOC container, then measure the amount of memory used by the application before starting. Perform measurements as you add injections to classes to see if your memory is averaging higher or not. If it’s noticeably higher (like 1/3 more memory usage), then you might have an issue. It’s better to head this off early when you can shut down expensive refactoring and save money. It’s also good to be aware of what you might run into when you try to deploy your new application. If you’re working on a small API and you have the memory capacity to handle it, then you might not be concerned at all. However, if you have a large monolithic application and you want to perform a large-scale refactoring, be prepared for some pain.