Zach Posten - 2017 Nov 15
The design and implementation of an Inversion of Control container in C#
An IoC container is an essential part of any MVVM framework. Its purpose is to allow objects to explicitly declare, via the constructor, their dependencies, and have those dependencies automagically supplied to them; this pattern is known as dependency injection. The alternative to dependency injection is to allow each object to create its own dependencies. This does not allow for the sharing of those objects, but more fatally, doing so makes testing very difficult. By injecting interfaces into a class under test (as opposed to concrete objects), the developer can then write tests that inject mock implementations into the class, facilitating thorough testing.
An IoC container is also a very clean way of instating objects. Instead of having run-on lines where you pass a long list of objects into a constructor, you can simply ask the IoC container to give you an instance of the object. Using the objects that it already has stored, or those that it knows how to create, the container automatically calls the constructor, and supplies you with the object.
In designing and implementing this container, I went through a few phases. In the first one here, let's look at the simplest possible implementation. At the very least, our container needs to be able to store objects, and retrieve objects. Let's call those methods
Admittedly, the name
IIocContainer is a little awkward, but I don't feel as though
IocContainer conveys very well that it's an interface. So we'll make due.
When the user asks us to store a type, they give us a type, and an object. When they want to retrieve an object, they give us that same type back. We pretty much have a key value pair relationship here, so a
Dictionary<Type, object> seems like a logical data structure to use for our underlying implementation.
To make the container a bit easier to use, we can declare some extension methods that allow the use of generics. Notice that the extension methods are created for the interface, so that they could be used for any implementation of
Well hey, that was pretty easy! There are definitely some improvements that could be made here though. What if the user were to call
Register again, with a different object? Also, this doesn't do any of the fancy magic object instantiation that we talked about earlier. Let's look at solving these problems and more in phase 2.
The big improvement that we want to make in our second go at this is to allow our container to instantiate objects on command. There are several pieces to this:
When we begin the implementation, one of the first issues that we're going to run into is somehow indicating to the
Resolve method how it should go about fetching the requested object. If I have a type, but no concrete object, that could either indicate that I should make it once and store it, or that I should instantiate it without storing it.
Additionally, you'll run into issues if the same type was used with calls to different methods. If it was first stored as an instance and then per request, unless you made sure (for the per request) to check to see if an instance already existed and remove it, if you were using the existence of a stored object as your flag, you would never create another instance, even though the last thing the user asked for was for you to create the object anew every time it was requested.
To "resolve" these issues, we'll define a
ResolutionInfo class that stores the necessary data to correctly instantiate the objects.
Notice that we don't allow a
ResolutionInfo object to be created externally, instead forcing the use of three static methods that return a
ResolutionInfo instance. Notice the properties of this class. In addition to flags that indicate how and when the object should be created, there is also a field to hold the object instance after creation.
Not let's look at how we use it. To make it more understandable, we'll break the explanation up into several pieces.
The first things that we're going to need are a place to store our objects, and something that knows how to created instances of things on demand.
Notice that just like before we're using a
Dictionary to hold our instances. The only difference this time is that the values are of type
ResolutionInfo. Recall from earlier that
ResolutionInfo has an
Instance property that will be used to hold the object that we're interested in accessing.
IInstantiator is a simple interface that allows for the creation of any type:
Finally, the constructor. It simply creates an instance of our
Instantiator object (we'll look at how this is implemented later) and stores itself so that if some viewmodel wants to explicitly use the IoC container, it is able to do so.
The constructor could take an optional instance of
IInstantiator, itself supporting dependency injection, the pattern that the container is part of. If that instance is not provided, it would default to our local implementation.
Instance method is the simplest of the four. It's purpose is to store an already created object for later retrieval. Taking in a type and the already instantiated object, it creates an instance version of the
ResolutionInfo with this object, and stores it in the dictionary with the type as a key.
Notice that the method returns the container so that method calls can be chained together.
Singleton method's purpose is to create an instance of an object upon request, but only once. And subsequent calls to retrieve that same type will receive the same instance.
Instead of taking a concrete object, it takes two types; one of them being optional. The implementation type is the type of the object that is to be instantiated. It cannot be an interface or an abstract class. I didn't explicitly check for that here, but you could. The service type is the key you want to use to store this object. This will typically be an interface that the implementation type implements.
PerRequest method is rather boring to look at by now, it is nearly identical to the
Singleton method apart from the type of
ResolutionInfo that it stores.
The purpose of the resolve method is to give back the user the object they have stored in the container. This method could logically be called
Resolve is the old standard.
The first thing we do is get the stored
ResolutionInfo. If we don't find one, that means that no object has been stored with the passed service type, so we return null.
Next we need to determine if we should instantiate an instance or not. If this info is not supposed to be created every time it's requested, and it's also not marked to be created this once, then we fetch the type and return it. Otherwise, we ask the
IInstantiator to instantiate an instance of the implementation type for us.
Finally, if the
ResolutionInfo specified that the object was only supposed to be created once, we store the created instance and mark that it has already been created this once, and should not be created again.
If the info is marked as
CreatePerRequest, then this instance will not be stored, and the same thing will happen the next time we call
There is a bit of complexity in how you go about dynamically creating objects, ensuring that their constructor's are passed all the objects that they require, in the proper order. This sounds like a job for System.Reflection! Let's just look at the entire class at once this time, I think you're ready for it.
The first thing to note is that the constructor takes an
IIocContainer, which is what we'll use to get the dependencies of the type we're trying to instantiate. That is a bit circular, but it makes sense when you think about it.
As you can see, we have three overloads of the same method here. The first, public one takes only a type. It makes two calls to the second method, first requiring all parameters, and then if that couldn't be done (was
null), without requiring all the constructor's parameters.
The second method loops through all the types constructors, seeing if we can fulfill any of them. It returns the first successfully created object.
The third method is really where the magic :rabbit::tophat: happens. For the constructor that we are trying to fulfill, we go through each of its parameters. For each parameter we try and fetch it from the
IIocContainer. If the type was not successfully fetched from the IoC (was
null), the we have a decision to make:
nullto be returned from this method.
nullfor this parameter. This could be an issue if a constructor took an integral type, then this would throw an exception because
nullis not a valid value for an integral type. This could be improved to instead use
Those are all the necessary pieces of a robust IoC container! However, passing types to methods can get annoying fast, so similarly to before, we can define some extension methods to make using the IoC easier.
As you can see, each one of these is simply generic, and uses
typeof to pass the
Type parameters of the regular methods.
However, there is one more extension method that I think can be useful here:
This method allows you to immediately create an instance of an object, without having to manually call its constructor, and also have it already stored in your IoC container, neat!
In this, somewhat lengthy, post, we've looked at how to build a robust inversion of control container. The code samples were all in C#, but these same ideas can certainly be applied to any language.
One of the most common use cases for an IoC container is within an MVVM framework, but I think there is a lot of value in it outside of MVVM as well, simply as a way of managing object instances that you want to share/define throughout your application.