Click here to Skip to main content
Click here to Skip to main content
Go to top

Dependency Inversion Principle, IoC Container, and Dependency Injection (Part - 3)

, 21 Apr 2013
Rate this:
Please Sign up or sign in to vote.
This is the third part of article on Dependency Injection. It explains how to develop a simple IoC container.

Introduction  

This the third part of my article on Dependency Inversion Principle, IoC Container & Dependency Injection. In the previous part of this article I have tried to explain what IoC container is. If you have gone through the previous parts of the article please go through the below links to have better understanding of the requirement of DIP, IoC and Dependency Injection concepts.

In This article I will be explaining how to create custom IoC container, and how it can be used to adhere  Dependency Inversion Principle.

Background     

There are many way to implement IoC container. In this article I will explaining the development of custom IoC container which will work in similar way as Microsoft Unity Container works. In this part of the article I will explain the basics implementation, and in the later parts of the article more features will be added to it, so that you can have an idea about working of Microsoft Unity Container.

You should have working knowledge of reflection to understand the code written in this article.

Working of Custom IoC Container  

The following figure shows the working of custom IoC Container. Each sections are separated using horizontal line. The scenario described in the following figure is having three sections: 

High-Level Module  

If you remember the Copy example which I have given in the part-1 of this article, which acts as high-level module. And DIP says that high-level module should not depend on the implementation of low-level module rather it should expose an abstraction which should be followed by low-level modules. 

Whenever there is a requirement of low-level module instance, it takes the help of container. The following statement in the above diagram returns an instance of low-level module.

IReader object = customContainer.Resolve<IReader>();  

High-Level module doesn't bother about who are the classes those implements IReader interface. And also we don't need to add a reference of project containing low-level modules, in the project containing high-level modules. It is the responsibility of container which will create the instance of low-level module (dependency) and return it to high-level module.

Custom IoC Container

There are two main methods of Custom IoC container which will be exposed to outside. Those are :

  • Register<TypeToResolve, ResolvedType>( )
  • Resolve<TypeToResolve> ( )

It maintains a dictionary where the combination of TypeToResolve and ResolvedType will be stored in the form of KeyValuePair using the Register( ) method.

And Resolve method first verifies whether the TypeToResolve is registered in the dictionary, if so then it tries to create an instance of its corresponding ResolvedType using reflection.

Note: Custom IoC container can be implemented in various ways. This implementation is one of the way.

Consumer of High-Level module

Basically this is where high-level module is used for performing the action. It can be a Windows/Web/Console Application. This application should know about low-level implementations.

For our example of IReader abstraction provided by High-Level module there is an implementation class called KeyBoardReader. The consumer's project should have a reference to project which contains low-level module implementation. So whenever there is an addition to low-level implementations the consumer may change (depending on the implementation of consumer) but not the high-level module as it does have reference of low-level module and it doesn't know who are the implementers of IReader abstraction.

Coding Custom IoC Container

Step 1: Create a blank solution in Visual Studio and create the following projects 

  • DIP.Abstractions  (Class Library Project)
    • IReader.cs 
    • IWriter.cs
  • DIP.HighLevelModule  (Class Library Project)
    • Copy.cs  
  • DIP.MyIoCContainer  (Class Library Project)
    • Container.cs  
  • DIP.Implementation  (Class Library Project)  
    • KeyboardReader.cs
    • PrinterWriter.cs
  • DIP.Consumer  (Console Application)
    • Program.cs

  

Here the important things to notice are projects references (Dependencies between project to project). 

  • DIP.Abstractions project doesn't have referecens to any project as it is independent of everything. 
  • DIP.HighLevelModule project does have two references 
    • Reference to DIP.Abstractions : As it uses Abstractions and doesn't depend on Implementations
    • Reference to DIP.MyIoCContainer : As it will use container to resolve the dependencies. 
  • DIP.Implementations is having references to DIP.Abstractions as it will implement the abstractions
  • DIP.MyIoCContainer doesn't have any references to any project, as it is a library and doesn't depend on any project.
  • DIP.Consumer have the references to following projects
    • DIP.Abstractions & DIP.Implementation : As it is required to do DI Registration.
    • DIP.MyIocContainer : It will be used as container which will be passed to DIP.HighLevelModule for resolving dependencies.
    • DIP.HighLevelModule : It will use DIP.HighLevelModule to perform action.
Note : In the above project dependencies you can notice that there is no dependencies between DIP.HighLevelModule and the  DIP.Implementation. Hence any addition or removal of classes in DIP.implementation doesn't change anything in DIP.HighLevelModule.

Step 2: Add the following code to Container.cs (DIP.MyIoCContainer project)

    public class Container
    {
        private Dictionary<Type,Type> iocMap = new Dictionary<Type,Type>();
 
        public void Register<TypeToResolve,ResolvedType>()
        {
            if (iocMap.ContainsKey(typeof(TypeToResolve)))
            {
                throw new Exception(string.Format("Type {0} already registered.", typeof(TypeToResolve).FullName));
            }
            iocMap.Add(typeof(TypeToResolve), typeof(ResolvedType));
        }
 
        public T Resolve<T>()
        {
            return (T)Resolve(typeof(T));
        }
 
        public object Resolve(Type typeToResolve)
        {
            // Find the registered type for typeToResolve
            if (!iocMap.ContainsKey(typeToResolve))
                throw new Exception(string.Format("Can't resolve {0}. Type is not registed.", typeToResolve.FullName));
 
            Type resolvedType = iocMap[typeToResolve];
 
            // Try to construct the object
            // Step-1: find the constructor (ideally first constructor if multiple constructos present for the type)
            ConstructorInfo ctorInfo = resolvedType.GetConstructors().First();
 
            // Step-2: find the parameters for the constructor and try to resolve those
            List<parameterinfo> paramsInfo = ctorInfo.GetParameters().ToList();
            List<object> resolvedParams = new List<object>();
            foreach (ParameterInfo param in paramsInfo)
            {
                Type t = param.ParameterType;
                object res = Resolve(t);
                resolvedParams.Add(res);
            }
 
            // Step-3: using reflection invoke constructor to create the object
            object retObject = ctorInfo.Invoke(resolvedParams.ToArray());
 
            return retObject;
        }
    }

  • iocMap   

A Dictionary<Type,Type> member which will keep the list of registered TypeToResolve and it's corresponding ResolvedType type.   

  • Register 

Method used to get the instance of a TypeToResolve Type
Syntax : void Register<TypeToResolve, ResolvedType>();

  • Resolve 

Method used to get the instance of a TypeToResolve Type
Syntax : <TypeToResolve> Resolve<TypeToResolve>(); 

Step 3: Construct the DIP.Abstractions Project

This project contains the abstractions (interfaces) which will be used by HighLevelModule to perform actions. In our case we have two abstractions:

IReader.cs

    public interface IReader
    {
        string Read();
    }

IWriter.cs

    public interface IWriter
    {
        void Write(string data);
    }

Step 4: Develop DIP.HighLevelModule

This module will use the abstractions to perform the action. In our case we have Copy.cs, which will copy from IReader to IWriter.

    public class Copy
    {
        private Container _container;
        private IReader _reader;
        private IWriter _writer;
 
        public Copy(Container container)
        {
            _container = container;
            _reader = _container.Resolve<IReader>();
            _writer = _container.Resolve<IWriter>();
        }
 
        public void DoCopy()
        {
            string stData = _reader.Read();
            _writer.Write(stData);
        }

Step 5: Implement the abstractions (DIP.Implementation)

This project contains the implementations of abstractions which are defined in DIP.Abstractions. There can be any number of implementation of a single abstraction. For example we can have KeyboardReader, FileReader etc. implemented from IReader interface.

To make the classes simple to understand I have defined two classes KeyboardReader (implementing IReader) and PrinterWriter (implementing IWriter).

Note: This doesn't implement the actual reading of Keyboard. I have kept is simple only for demonstrating the usage of IoC Container not how to read from keyboard.

KeyboardReader.cs

    public class KeyboardReader:IReader
    {
        public string Read()
        {
            return "Reading from \"Keyboard\"";
        }
    }
 

PrinterWriter.cs

    public class PrinterWriter:IWriter
    {
        public void Write(string data)
        {
            Console.WriteLine(string.Format("Writing to \"Printer\": [{0}]", data));
        }
    }

Step 6: Finally developing the Consumer class which will actually use HighLevelModule  (DIP.Consumer)

It is a console application which interacts with the user.

Program.cs

    class Program
    {
        static void Main(string[] args)
        {
            Container container = new Container();
            DIRegistration(container);
            Copy copy = new Copy(container);

            copy.DoCopy();
            Console.Read();
        }

        static void DIRegistration(Container container)
        {
            container.Register<IReader,KeyboardReader>();
            container.Register<IWriter,PrinterWriter>();
        }
    }

You can notice that it contains another method called DIRegistration( ). It basically does the registration of dependencies. The statement container.Register<IReader, KeyboardReader>() registers the mapping of IReader and KeyboardReader, so that whenever there is a need of implementation of IReader object, an instance of KeyboardReader will be returned.

Note : There are many ways to register the dependencies. We can also implement DI registration using configuration file.

Running the application 

If you run the developed application, you will get the following output.

Here you can extend the implementations without changing the high-level program. The only thing you need to do is change the registration.  

Summary

In this part of the article I have tried to expain how to develop a simple IoC container. Hope you found this topic good and easy to understand. In the next chapter I will introduce some advance techniques to make custom IoC container more realistic and useful. 

History

First revision : 29-March-2013

License

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

Share

About the Author

Chinmaya_Champatiray
Software Developer (Senior) PAREXEL International
India India
Life is short. So enjoy each and every moment of life.
And necessarily keep smiling. Smile | :)

Comments and Discussions

 
QuestionDictionary<T1,T2> question [modified] PinmemberMember 98220099-Sep-14 23:43 
AnswerRe: Dictionary<T1,T2> question PinmemberChinmaya_Champatiray10-Sep-14 6:42 
QuestionBrilliant Pinmemberstiknorr6-Jun-14 3:01 
AnswerRe: Brilliant PinmemberChinmaya_Champatiray6-Jun-14 4:48 
QuestionVery good article! PinmemberGanesh_d15-Aug-13 23:29 
AnswerRe: Very good article! PinprofessionalChinmaya_Champatiray6-Aug-13 8:18 
Questionfor the running purpose Pinmemberkaushik giri6-May-13 17:30 
AnswerRe: for the running purpose PinprofessionalChinmaya_Champatiray6-May-13 20:54 
GeneralRe: for the running purpose Pinmemberkaushik giri7-May-13 17:46 
GeneralMy vote of 5 PinmemberGregoryW16-Apr-13 2:21 
GeneralRe: My vote of 5 PinmemberChinmaya_Champatiray16-Apr-13 5:12 
Thanks Gregory. Smile | :)
GeneralRe: My vote of 5 PinmemberGregoryW16-Apr-13 5:24 
GeneralMy vote of 5 Pinmemberhulinning11-Apr-13 1:58 
GeneralRe: My vote of 5 PinmemberChinmaya_Champatiray11-Apr-13 2:04 
GeneralMy vote of 5 PinmemberGajendra Medatia5-Apr-13 1:11 
GeneralRe: My vote of 5 PinmemberChinmaya_Champatiray5-Apr-13 1:38 
GeneralRe: My vote of 5 PinmemberGajendra Medatia8-Apr-13 14:28 
GeneralRe: My vote of 5 PinmemberChinmaya_Champatiray8-Apr-13 16:25 
GeneralMy vote of 5 PinmemberLucian-aSterX2-Apr-13 21:09 
GeneralRe: My vote of 5 PinmemberChinmaya_Champatiray2-Apr-13 21:16 

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

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

| Advertise | Privacy | Mobile
Web04 | 2.8.140916.1 | Last Updated 21 Apr 2013
Article Copyright 2013 by Chinmaya_Champatiray
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid