Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / game / Unity

Rolling Our Own IoC

0.00/5 (No votes)
13 Nov 2018CPOL5 min read 2.6K  
What an Inversion of Control (IoC) container is and how it works

Introduction

Previously, we talked about what Dependency Inversion and Dependency Injection are. In this post, we’re going to look at what an Inversion of Control (IoC) container is and how it works. In the future, we will also have a look at what are the common implementations of IoCs available and which are my own personal favorites.

If you only want to see the code (I know I sometimes do), you can find the source code for this project here. (This solution will have several commits so that you can follow along with the changes.)

What is an IoC Container?

An IoC container is an implementation that allows the use of Dependency Injection in our applications by keeping everything neat and organized across the application and allows us to have one configuration spot (or several, depending on the architecture) so that we can swap parts without affecting the code or hard-coding the creation of instances.

Some of the most common (and we will look at those in detail in a future post) IoC container implementations are as follows:

In this post, we’re going to make our own from scratch and see what makes it tick, keep in mind that this implementation is by no means complete or for all scenarios, the IoC implementations enumerated earlier have been used and grown in the industry for quite some time and we are going to look only at the most simple implementation for example purposes.

Rolling Our Own

So in true API development, we will be following the Test Driven Development method to implement just what we need and for this, I will be using NUnit.

First off, we create a class library for our tests and begin writing what we want the API to do.

C#
namespace IoCExample.Tests
{
    using NUnit.Framework;

    [TestFixture]
    class ConcreteClassesDepeneciesTests
    {
        [Test]
        public void WhenRequiringASimpleClass_ShouldReturnClass()
        {
            IoCExampleImplementation sut = new IoCExampleImplementation();

            SimpleClass simpleClass = sut.Create<SimpleClass>();

            Assert.That(simpleClass, Is.Not.Null, "The instance shouldn't be null");
        }
    }

    public class SimpleClass
    {
    }
}

In true TDD fashion, none of those types have been declared yet, so we will create our implementation in a class library and we will add a reference to that library.

C#
namespace IoCExample
{
    public class IoCExampleImplementation
    {
        public T Create<T>()
        {
            throw new System.NotImplementedException();
        }
    }
}

Now we can start the Fail, Pass, Refactor phases of TDD. When we run the above test case, we can be sure that it will fail because the method we are using has not been implemented.

Since our example class (SimpleClass) isn’t, and for an IoC container it shouldn’t, be declared in our implementation library, we will need to do more than just return a new instance of that type. Time to make our test pass.

C#
namespace IoCExample
{
    using System;

    public class IoCExampleImplementation
    {
        public T Create<T>()
        {
            T instance = Activator.CreateInstance<T>();
            return instance;
        }
    }
}

Now we have a very rudimentary IoC, we give it a type and it returns an instance of that type. But what if we have a type that requires additional classes as dependencies? Well, let’s write the test and we will see.

C#
[Test]
public void WhenRequiringAClassWithDepencies_ShouldCascadeAndCreateClass()
{
    IoCExampleImplementation sut = new IoCExampleImplementation();

    SimpleClass2 simpleClass2 = sut.Create<SimpleClass2>();

    Assert.That(simpleClass2, Is.Not.Null, "The instance shouldn't be null");
    Assert.That(simpleClass2.Dependency, Is.Not.Null, 
                "The dependency should be injected via constructor and shouldn't be null");
}

public class SimpleClass2
{
    public SimpleClass Dependency { get; }
}

public class SimpleClass
{

}

If we run the tests now, we will see that the first one passes and also the first assert in the second test, from this we know that our previous functionality still works and that we can continue on developing without fear. But also notice that we are not fulfilling the requirement of the API. If we make the property to be injected via constructor, it will look like this:

C#
public class SimpleClass2
{
    public SimpleClass2(SimpleClass dependency)
    {
        Dependency = dependency;
    }

    public SimpleClass Dependency { get; }
}

Now if we run the tests, we will notice that only the first one will pass and the second one will throw an exception because our implementation of the container only works with default or parameterless constructors. So we confirm the functionality that we’re looking for. Now let’s make this test pass as well.

C#
namespace IoCExample
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;

    public class IoCExampleImplementation
    {
        public T Create<T>()
        {
            return (T)this.Create(typeof(T));
        }

        private object Create(Type type)
        {
            ConstructorInfo constructorInfo = type.GetConstructors().Single();

            List<object> parameters = null;
            ParameterInfo[] constructorParameters = constructorInfo.GetParameters();
            foreach (ParameterInfo parameterInfo in constructorParameters)
            {
                parameters = parameters ?? new List<object>();
                parameters.Add(this.Create(parameterInfo.ParameterType));
            }

            object instance;
            if (parameters != null)
            {
                instance = constructorInfo.Invoke(parameters.ToArray());
            }
            else
            {
                instance = Activator.CreateInstance(type);
            }

            return instance;
        }
    }
}

With this implementation, the test pass. Though do note that the limitation of only having one constructor.

Now let’s throw a curve ball at it and see if it will hold with a more complex scenario.

C#
[Test]
public void WhenRequiringAClassWithMultipleAndNestedDepencies_ShouldCascadeAndCreateClass()
{
    IoCExampleImplementation sut = new IoCExampleImplementation();

    SimpleClass3 simpleClass3 = sut.Create<SimpleClass3>();

    Assert.That(simpleClass3, Is.Not.Null, "The instance shouldn't be null");
    Assert.That(simpleClass3.Dependency1, Is.Not.Null, 
                "The dependency should be injected via constructor and shouldn't be null");
    Assert.That(simpleClass3.Dependency2, Is.Not.Null, 
                "The dependency should be injected via constructor and shouldn't be null");

    Assert.That(simpleClass3.Dependency1.Dependency, Is.Not.Null, 
                "The nested dependency should be fulfilled as well");
}

public class SimpleClass3
{
    public SimpleClass2 Dependency1 { get; }

    public SimpleClass Dependency2 { get; }

    public SimpleClass3(SimpleClass2 simpleClass2, SimpleClass simpleClass)
    {
        this.Dependency1 = simpleClass2;
        this.Dependency2 = simpleClass;
    }
}

public class SimpleClass2
{
    public SimpleClass2(SimpleClass dependency)
    {
        Dependency = dependency;
    }

    public SimpleClass Dependency { get; }
}

public class SimpleClass
{
}

And guess what? Without any further changes to our implementation, all the tests pass, we now have a class that can create instances with no matter how many concrete class parameters and in depth.

But this isn’t really much of a Dependency Inversion implementation since we are using concrete classes. What we really need is to be allowed to map out classes to interfaces or to other abstract classes. So what we are going to do next is make another test class so that we can play around with creating classes with interfaces and map them out.

This is how our new test class will look:

C#
namespace IoCExample.Tests
{
    using NUnit.Framework;

    [TestFixture]
    class InterfaceDependenciesTests
    {
        [Test]
        public void WhenMappingInterfaceDepencies_ShouldCreateProperClass()
        {
            IoCExampleImplementation sut = new IoCExampleImplementation();
            sut.Mapping.Add(typeof(INotification), typeof(EmailNotification));

            OrderProcessor orderProcessor = sut.Create<OrderProcessor>();

            Assert.That(orderProcessor, Is.Not.Null, "The instance of the test object wasn't created");
            Assert.That(orderProcessor.Notification, Is.Not.Null, 
                        "The dependency has not been fulfilled");
            Assert.That(orderProcessor.Notification, Is.TypeOf<EmailNotification>(), 
                        "The dependency is not of mapped type");

            sut.Mapping[typeof(INotification)] = typeof(SmsNotification);
            orderProcessor = sut.Create<OrderProcessor>();

            Assert.That(orderProcessor, Is.Not.Null, 
                        "The instance of the test object wasn't created");
            Assert.That(orderProcessor.Notification, Is.Not.Null, 
                        "The dependency has not been fulfilled");
            Assert.That(orderProcessor.Notification, Is.TypeOf<SmsNotification>(), 
                        "The dependency is not of mapped type");
        }
    }

    internal interface INotification
    {
    }

    internal class EmailNotification : INotification
    {
    }

    internal class SmsNotification : INotification
    {
    }

    internal class OrderProcessor
    {
        public INotification Notification { get; }

        public OrderProcessor(INotification notification)
        {
            this.Notification = notification;
        }
    }
}

In this test, we expect that we will have an Order processor that notifies the user when an order has been processed, and with this approach, imagine we have a full application and we do the mapping at the start of the application (or during the initialization of a module), as such we can change what our application will do without touching any of the business logic code just by manipulating the container.

Of course, when we run the test, it will fail, but notice that our previous tests are still running.

Now let’s try to make this one pass as well.

C#
namespace IoCExample
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;

    public class IoCExampleImplementation
    {
        private Dictionary<Type, Type> mapping;

        public Dictionary<Type, Type> Mapping
        {
            get
            {
                this.mapping = this.mapping ?? new Dictionary<Type, Type>();
                return this.mapping;
            }
        }

        public T Create<T>()
        {
            return (T)this.Create(typeof(T));
        }

        private object Create(Type type)
        {
            Type instanceType;
            if (!Mapping.TryGetValue(type, out instanceType))
            {
                instanceType = type;
            }

            ConstructorInfo constructorInfo = instanceType.GetConstructors().Single();

            List<object> parameters = null;
            ParameterInfo[] constructorParameters = constructorInfo.GetParameters();
            foreach (ParameterInfo parameterInfo in constructorParameters)
            {
                parameters = parameters ?? new List<object>();
                parameters.Add(this.Create(parameterInfo.ParameterType));
            }

            object instance;
            if (parameters != null)
            {
                instance = constructorInfo.Invoke(parameters.ToArray());
            }
            else
            {
                instance = Activator.CreateInstance(instanceType);
            }

            return instance;
        }
    }
}

With just a small change and a new dictionary property, we made the test pass once more and all the previous tests pass as well.

Conclusion

What we did was a very very simple IoC container that works with mapped out types and creating nested types recursively. Of course, there are frameworks that have many features like using a config file to be manipulated in production, using attributes so that we don’t need to create the initial mapping, mapping multiple instances for a given interface, controlling the life cycle of the objects, and so on.

One big drawback as you can see is that all the instances need to be created using the container, though if the design of the application is done right, this won’t be too much of a setback.

I hope that now you see how a container works at least at its core idea and how to roll your own and extend it.

As mentioned at the start of this post, the repository with the code (and the commits to follow one by one) can be found here.

Thank you and see you next time.

License

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