Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Dependency Inversion Principle, IoC Container, and Dependency Injection: Part 3

4.88/5 (38 votes)
23 Apr 2020CPOL6 min read 83.5K   945  
How to create a custom IoC container and how it can be used to adhere to DIP
This is the third part of the article on Dependency Injection. It explains how to develop a simple IoC container and how to use it to adhere to the Dependency Inversion Principle.

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 not gone through the previous parts of the article, please go through them using the below links to have a better understanding of the requirement of DIP, IoC and Dependency Injection concepts:

In this article, I will explain how to create a custom IoC container, and how it can be used to adhere to the Dependency Inversion Principle.

Background

There are many ways to implement an IoC container. In this article, I will explain the development of custom IoC container which will work in a similar way to how Microsoft Unity Container works. In this part of the article, I will explain the basic implementation, and in the later parts of the article, more features will be added to it, so that you can have an idea about the 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 section is separated using a horizontal line. The scenario described in the following figure has three sections:

Click to enlarge image

High-Level Module

If you remember, the Copy example which I gave in Part 1 of this article 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.

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

High-Level module doesn't bother about which are the classes that implement 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 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 ways.

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

Image 2

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

  • DIP.Abstractions project doesn't have references 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 has references to the 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)

C++
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 registered.", typeToResolve.FullName));

        Type resolvedType = iocMap[typeToResolve];

        // Try to construct the object
        // Step-1: find the constructor
        // (ideally first constructor if multiple constructors 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

C#
public interface IReader
{
    string Read();
}

IWriter.cs

C#
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.

C#
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 it simple only for demonstrating the usage of IoC Container not how to read from keyboard.

KeyboardReader.cs

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

PrinterWriter.cs

C#
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

C#
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 to implement the 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:

Image 3

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 explain 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 advanced techniques to make custom IoC container more realistic and useful.

History

  • 23rd April, 2020: Second revision (updated link to the final article)
  • 29th March, 2013: First revision

License

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