Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#

Lets write a Tiny IoC Container to learn(and for fun)

Rate me:
Please Sign up or sign in to vote.
4.89/5 (17 votes)
31 Jan 2016CPOL7 min read 45.1K   348   23   13
This article contains a small IoC container implementation just created for educational purpose.

Introduction

This article contains a small IoC container implementation just created for educational purpose.

Background

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.

What is an IoC Container

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.

Using the Code

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

  1. RegisterInstanceType: Register interfaces with concrete types where for each Resolve request, a new instance will be returned.
  2. RegisterSingletonType: Register interfaces with concrete types where for all Resolve request, a singleton instance will be returned.
  3. Resolve: Resolve the interfaces and retrieve the configured Concrete type for a given interface.

Apart from this the container also exposes an attribute called TinyDependencyAttribute to handle the nested dependency injection.

To handle nested dependencies, this container supports constructor injection. The custom attribute TinyDependencyAttribute should 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.

  • IContainer - Interface that contains all the methods for our Container.
  • Container - Concrete class that implements the IContainer interface 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.

C#
public interface IContainer
{
    void RegisterInstanceType< I, C >()
        where I : class
        where C : class;

    void RegisterSingletonType< I, C >()
        where I : class
        where C : class;

    T Resolve< T >();
}

So these are the 3 methods that we will be exposing from our container. The user of our application will be able to use RegisterInstanceType to register a normal instance type dependency for an interface and RegisterSingletonType for registering a singleton object dependency.

Now let us look at the implementation of the Container class which encapsulates these functionalities.

C#
public class Container : IContainer
{
    Dictionary< type, registrationmodel >  instanceRegistry = new Dictionary< type, registrationmodel >();
        
    public void RegisterInstanceType< I, C >()
        where I : class
        where C : class
    {
        RegisterType< I, C >(REG_TYPE.INSTANCE);
    }

    public void RegisterSingletonType< I, C >()
        where I : class
        where C : class
    {
        RegisterType< I, C >(REG_TYPE.SINGLETON);
    }

    private void RegisterType< I, C >(REG_TYPE type)
    {
        if (instanceRegistry.ContainsKey(typeof(I)) == true)
        {
            instanceRegistry.Remove(typeof(I));
        }

        instanceRegistry.Add(
            typeof(I),
                new RegistrationModel
                {
                    RegType = type,
                    ObjectType = typeof(C)
                }
            );
    }

    public I Resolve< I >()
    {
        return (I)Resolve(typeof(I));            
    }

    private object Resolve(Type t)
    {
        object obj = null;

        if (instanceRegistry.ContainsKey(t) == true)
        {
            RegistrationModel model = instanceRegistry[t];

            if (model != null)
            {
                Type typeToCreate = model.ObjectType;
                ConstructorInfo[] consInfo = typeToCreate.GetConstructors();

                var dependentCtor = consInfo.FirstOrDefault(item => item.CustomAttributes.FirstOrDefault(att => att.AttributeType == typeof(TinyDependencyAttribute)) != null);
                
                if(dependentCtor == null)
                {
                    // use the default constructor to create
                    obj = CreateInstance(model);
                }
                else
                {
                    // We found a constructor with dependency attribute
                    ParameterInfo[] parameters = dependentCtor.GetParameters();

                    if (parameters.Count() == 0)
                    {
                        // Futile dependency attribute, use the default constructor only
                        obj = CreateInstance(model);
                    }
                    else
                    {
                        // valid dependency attribute, lets create the dependencies first and pass them in constructor
                        List< object > arguments = new List< object >();

                        foreach (var param in parameters)
                        {
                            Type type = param.ParameterType;
                            arguments.Add(this.Resolve(type));
                        }

                        obj = CreateInstance(model, arguments.ToArray());
                    }
                }
            }
        }

        return obj;
    }

    private object CreateInstance(RegistrationModel model, object[] arguments = null)
    {
        object returnedObj = null;
        Type typeToCreate = model.ObjectType;

        if (model.RegType == REG_TYPE.INSTANCE)
        {
            returnedObj = InstanceCreationService.GetInstance().GetNewObject(typeToCreate, arguments);
        }
        else if (model.RegType == REG_TYPE.SINGLETON)
        {
            returnedObj = SingletonCreationService.GetInstance().GetSingleton(typeToCreate, arguments);
        }

        return returnedObj;
    }
}

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 RegistrationModel object is used to keep track of the concrete object type and requested lifetime. This model looks like following.

C#
internal enum REG_TYPE
{
    INSTANCE,
    SINGLETON
};

internal class RegistrationModel
{
    internal Type ObjectType { get; set; }
    internal REG_TYPE RegType { get; set; }
}

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 TinyDependencyAttribute is 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 SingletonCreationService will 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.

C#
internal class SingletonCreationService
{
    static SingletonCreationService instance = null;
    static Dictionary< string, object > objectPool = new Dictionary< string, object >());

    static SingletonCreationService()
    {
        instance = new SingletonCreationService();
    }

    private SingletonCreationService()
    { }

    public static SingletonCreationService GetInstance()
    {
        return instance;
    }

    public object GetSingleton(Type t, object[] arguments = null)
    {
        object obj = null;

        try
        {
            if (objectPool.ContainsKey(t.Name) == false)
            {
                obj = InstanceCreationService.GetInstance().GetNewObject(t, arguments);
                objectPool.Add(t.Name, obj);
            }
            else
            {
                obj = objectPool[t.Name];
            }
        }
        catch
        {
            // log it maybe
        }

        return obj;
    }
}

And the InstanceCreationService always returns a new object for each resolve call.

C#
internal class InstanceCreationService
{
    static InstanceCreationService instance = null;

    static InstanceCreationService()
    {
        instance = new InstanceCreationService();
    }

    private InstanceCreationService()
    { }

    public static InstanceCreationService GetInstance()
    {
        return instance;
    }

    public object GetNewObject(Type t, object[] arguments = null)
    {
        object obj = null;

        try
        {
            obj = Activator.CreateInstance(t, arguments);
        }
        catch
        {
            // log it maybe
        }

        return obj;
    }
}

Now that we have seen all the classes that are involved in the Container library lets see how they will coordinate and work.

  1. The caller will call the Register function on the container.
  2. 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.
  3. The the caller called resolve, the container will use InstanceCreationService to create an object of registered instance type and return to the user.
  4. The the caller called resolve, and if the registered type is of singleton, the container will use SingeltonCreationService to 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.

Lets test the Container

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.

C#
interface ITest1
{
    void Print();
}

class ClassTest1 : ITest1
{
    public void Print()
    {
        Console.WriteLine("ClassName: {0}, HashCode: {1}", this.GetType().Name, this.GetHashCode());
    }
}

interface ITest2
{
    void Print();
}

class ClassTest2 : ITest2
{
    public void Print()
    {
        Console.WriteLine("ClassName: {0}, HashCode: {1}", this.GetType().Name, this.GetHashCode());
    }
}

lets test our Register and Resolve functionality for above interfaces and classes/

C#
IContainer container = new yaTinyIoCContainer.Container();

// testing instance type resigtration for class
container.RegisterInstanceType< itest1, classtest1 >();
ITest1 obj1 = container.Resolve< itest1 >();
obj1.Print();


// testing singleton registration for class
container.RegisterSingletonType< itest2, classtest2 >();
ITest2 obj5 = container.Resolve< itest2 >();
obj5.Print();

To test the nested dependencies, let us create some classes that expects other interface dependencies to be injected in them.

C#
interface One
{
    void FunctionOne();
}

interface Two
{
    void FunctionTwo();
}

class ClassOne : One
{
    ITest1 m_Itest1 = null;
    
    [TinyDependency]
    public ClassOne(ITest1 test1)
    {
        m_Itest1 = test1;
    }
    
    public void FunctionOne()
    {
        Console.WriteLine("ClassName: {0}, HashCode: {1}", this.GetType().Name, this.GetHashCode());
        m_Itest1.Print();
    }
}

class ClassTwo : Two
{
    One m_One = null;
    ITest1 m_Itest1 = null;
        
    [TinyDependency]
    public ClassTwo(ITest1 test1, One one)
    {
        m_Itest1 = test1;
        m_One = one;
    }
    
    public void FunctionTwo()
    {
        Console.WriteLine("ClassName: {0}, HashCode: {1}", this.GetType().Name, this.GetHashCode());
        m_Itest1.Print();
        m_One.FunctionOne();
    }
}

Notice the use of TinyDependencyAttribute in the above classes' constructors. Now to test these lets register them and try to resolve them.

C#
 // testing nested dependency for 2 levels
container.RegisterInstanceType< one, classone >();
One obj9 = container.Resolve< one >();
obj9.FunctionOne();

// testing nested dependency for 2 levels with 2 arguments
container.RegisterInstanceType< two, classtwo >();
Two obj10 = container.Resolve< two >();
obj10.FunctionTwo();

And now when we run the application, we can see that all the dependencies have been resolved to their registered classes.

Image 1

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.

Point of interest

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. 

History

  • 01 February, 2016 - Introduced the Attribute to better handle the nested dependencies
  • 29 January, 2016 - First version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
India India

I Started my Programming career with C++. Later got a chance to develop Windows Form applications using C#. Currently using C#, ASP.NET & ASP.NET MVC to create Information Systems, e-commerce/e-governance Portals and Data driven websites.

My interests involves Programming, Website development and Learning/Teaching subjects related to Computer Science/Information Systems. IMO, C# is the best programming language and I love working with C# and other Microsoft Technologies.

  • Microsoft Certified Technology Specialist (MCTS): Web Applications Development with Microsoft .NET Framework 4
  • Microsoft Certified Technology Specialist (MCTS): Accessing Data with Microsoft .NET Framework 4
  • Microsoft Certified Technology Specialist (MCTS): Windows Communication Foundation Development with Microsoft .NET Framework 4

If you like my articles, please visit my website for more: www.rahulrajatsingh.com[^]

  • Microsoft MVP 2015

Comments and Discussions

 
QuestionGreat and Thanks Pin
Member 155103071-Apr-22 7:48
Member 155103071-Apr-22 7:48 
QuestionHorrific... Pin
Rick Shaw21-Aug-16 13:57
Rick Shaw21-Aug-16 13:57 
GeneralMy vote of 5 Pin
Daniel Miller11-Feb-16 15:09
professionalDaniel Miller11-Feb-16 15:09 
QuestionA suggestion Pin
George Swan2-Feb-16 7:22
mveGeorge Swan2-Feb-16 7:22 

Thanks for the interesting article. I think your RegisterType method can be shortened to

C#
private void RegisterType<I, C>(REG_TYPE type)
       {
        //This adds the key if not present or replaces the value if the key is present
           instanceRegistry[typeof(I)] = new RegistrationModel { RegType = type, ObjectType = typeof(C) };

       }

QuestionNice Pin
Sacha Barber31-Jan-16 23:27
Sacha Barber31-Jan-16 23:27 
AnswerRe: Nice Pin
Rahul Rajat Singh1-Feb-16 0:18
professionalRahul Rajat Singh1-Feb-16 0:18 
GeneralRe: Nice Pin
Sacha Barber1-Feb-16 1:17
Sacha Barber1-Feb-16 1:17 
GeneralMy vote of 5 Pin
Santhakumar M31-Jan-16 20:33
professionalSanthakumar M31-Jan-16 20:33 
GeneralMy vote of 5 Pin
mayank.saxena8831-Jan-16 19:51
mayank.saxena8831-Jan-16 19:51 
QuestionWhere's the code? Pin
Pete O'Hanlon28-Jan-16 23:42
subeditorPete O'Hanlon28-Jan-16 23:42 
AnswerRe: Where's the code? Pin
Rahul Rajat Singh28-Jan-16 23:45
professionalRahul Rajat Singh28-Jan-16 23:45 
AnswerRe: Where's the code? Pin
Rahul Rajat Singh29-Jan-16 0:38
professionalRahul Rajat Singh29-Jan-16 0:38 
GeneralRe: Where's the code? Pin
Pete O'Hanlon29-Jan-16 0:41
subeditorPete O'Hanlon29-Jan-16 0:41 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.