This article contains a small IoC container implementation just created for educational purpose.
The other day I was having a discussion with one of my friends on how service locator and IoC containers are related and the Dependency injection best practices. Which let to another discussion with some junior level developers on the internal workings of IoC containers. And this gave me an idea that writing a small IoC container might be a good exercise to explain these guys how IoC containers works internally.
So this article talks briefly about a small IoC container that I created in an hours time for teaching the basics of IoC containers and how to implement one to some developers. I am putting this online so that someone might get benefited from this.
To start the discussion, lets start with understanding what an IoC container is. An IoC container is a component that lets us register our Concrete class dependencies with our contracts i.e. interfaces so that for any given interface, the registered concrete class will be instantiated. This let the higher level modules specify their own concrete classes, register them and get them injected in the application for any given interface.
The above explanation is only the Dependency injection part of the IoC containers. An IoC container could also manage the life time of an object too. There are many full fledged IoC containers exists that provides a comprehensive solution for all the inversion of controls and object lifetime management needs. In no way the code in this article should be used for production applications. It is just a simple exercise to understand and take a sneak peek the inner workings of IoC containers.
Lets start by looking at the set of functions that are being exposed from our container library i.e. what has been implemented in the container
- RegisterInstanceType: Register interfaces with concrete types where for each Resolve request, a new instance will be returned.
- RegisterSingletonType: Register interfaces with concrete types where for all Resolve request, a singleton instance will be returned.
- Resolve: Resolve the interfaces and retrieve the configured Concrete type for a given interface.
Apart from this the container also exposes an attribute called TinyDependencyAttributeto handle the nested dependency injection.
To handle nested dependencies, this container supports constructor injection. The custom attribute TinyDependencyAttributeshould be used to decorate the constructors that require other dependencies to be injected. This attribute will be used by our container to inject the registered dependencies in the constructor of the given type i.e. the constructor decorated with our custom attribute.
Now let us briefly look at the various components in the code.
- Container- Concrete class that implements the IContainerinterface and encapsulates the inner working of registration and instance resolution.
- RegistrationModel- A simple model that keep the information about the type being registered and its life time.
- InstanceCreationService- A service that takes care of instance creation from a given type. This class also takes care of nested dependencies and their injection.
- SingletonCreationService- A service that keeps track of singleton instances and creates singleton instances on Resolve requests.
Now with this explanation,we are ready to see how the IoC container functionalities have been implemented.
Lets start by looking at the IContainer interface.
So these are the 3 methods that we will be exposing from our container. The user of our application will be able to use RegisterInstanceTypeto register a normal instance type dependency for an interface and RegisterSingletonTypefor registering a singleton object dependency.
Now let us look at the implementation of the Containerclass which encapsulates these functionalities.
What this class does is that it keep a track of all the interface type and their concrete implementation types in a dictionary. The Register and resolve methods will register a dependency and return an instance of the registered type respectively. The RegistrationModelobject is used to keep track of the concrete object type and requested lifetime. This model looks like following.
The second thing to notice is the resolve method. The resolve method looks at the concrete type registered for an interface and then check if our custom attribute TinyDependencyAttributeis present on any constructor. If this attribute is present then this is the case of nested dependency and thus, we need to create and pass the dependent objects in the constructor. If no constructor contains this attribute, we will simply use the default constructor to create the instance of registered concrete type.
The resolve method used two other classes for actual object instantiation from a give type. The SingletonCreationServicewill manage the singleton objects and return the registered instance to the caller. If the instance already exists for a given object, it will return the same. If not, it will create an instance and then return. Also, it will keep that instance saved for next resolve call for this singleton object.
And the InstanceCreationServicealways returns a new object for each resolve call.
Now that we have seen all the classes that are involved in the Container library lets see how they will coordinate and work.
- The caller will call the Register function on the container.
- The container will store the interface and the concrete class type as a dependency based on type of register method i.e. instance or singleton.
- The the caller called resolve, the container will use InstanceCreationService to create an object of registered instance type and return to the user.
- The the caller called resolve, and if the registered type is of singleton, the container will use SingeltonCreationServiceto create an object or return an already existing object of registered type.
Now that we have seen the internals of the IoC container library, lets see how the container can be tested.
To test the container, let us create some dummy interfaces and some concrete classes. Lets start with simple dependencies and test our register methods using them.
lets test our Register and Resolve functionality for above interfaces and classes/
To test the nested dependencies, let us create some classes that expects other interface dependencies to be injected in them.
Notice the use of TinyDependencyAttributein the above classes’ constructors. Now to test these lets register them and try to resolve them.
And now when we run the application, we can see that all the dependencies have been resolved to their registered classes.
Before we end the discssion, here are few important that could be helpful before looking at the source code.
- All the registrations are being done using code only. This code can be enhanced to read the dependencies from a config file but was not a part of this application scope.
- This container is able to inject the nested dependencies provided all the dependencies have been registered before the Resolve call. I have tested up to 3 levels of nested dependencies but theoretically it should work up to N levels.
- The test application for this is a console application that contains a lot of interfaces and classes with all the dependencies being registered and resolved in the Main function.
This small application is a result of an hour of code. The main idea of this application was to demonstrate how IoC containers work. This has been written as a teaching/learning exercise and thus the coding standards and best practices are not up to the mark. The code has been put in form of an article just to make it available to others(beginners mainly) so that they can also get a sneak peak on how IoC containers must be working.
Download the sample code from here: yaTinyIoCContainer