Implementing a basic IoC container using C#


Implementing a basic IoC container using C#, step by step.

Table of contents

Introduction

I use Unity IoC container at work and I was wondering how difficult it would be to implement an IoC container, so I decided to create one with some basic functionalities. The purpose of this article is to show you step by step how the container was implemented and why some of the decisions were made.

Please note that this is nothing but a coding exercise and should not be used in a production application – use one of the many existing IoC containers for .NET instead:

Requirements

The IoC container should be able to:

  1.  Register a type mapping by specifying the source and destination types;
  2.  Register a type mapping using a delegate to create the requested object;
  3.  Check if a mapping is registered;
  4.  Get an instance of the requested type;
  5.  All methods above will have a generic and a non-generic version;
  6.  All methods will have support for named instances, so it’s possible to create more than one existing object registration using the same registered type.

3. Implementing the container

In this section I’ll explain how the IoC container was implemented, step by step.

Defining the Interface

Following the Dependency Inversion principle (program against abstractions), I defined an interface that meets all the above requirements – people that use Unity IoC container (like me) should be familiar with the method names:

/// <summary>
/// IoC container
/// </summary>
public interface IContainer
{
    /// <summary>
    /// Register a type mapping
    /// </summary>
    /// <param name="from">Type that will be requested</param>
    /// <param name="to">Type that will actually be returned</param>
    /// <param name="instanceName">Instance name (optional)</param>
    void Register(Type from, Type to, string instanceName = null);


    /// <summary>
    /// Register a type mapping
    /// </summary>
    /// <typeparam name="TFrom">Type that will be requested</typeparam>
    /// <typeparam name="TTo">Type that will actually be returned</typeparam>
    /// <param name="instanceName">Instance name (optional)</param>
    void Register<TFrom, TTo>(string instanceName = null) where TTo : TFrom;


    /// <summary>
    /// Register a type mapping
    /// </summary>
    /// <param name="type">Type that will be requested</param>
    /// <param name="createInstanceDelegate">A delegate that will be used to
    /// create an instance of the requested object</param>
    /// <param name="instanceName">Instance name (optional)</param>
    void Register(Type type, Func<object> createInstanceDelegate, string instanceName = null);


    /// <summary>
    /// Register a type mapping
    /// </summary>
    /// <typeparam name="T">Type that will be requested</typeparam>
    /// <param name="createInstanceDelegate">A delegate that will be used to
    /// create an instance of the requested object</param>
    /// <param name="instanceName">Instance name (optional)</param>
    void Register<T>(Func<T> createInstanceDelegate, string instanceName = null);


    /// <summary>
    /// Check if a particular type/instance name has been registered with the container
    /// </summary>
    /// <param name="type">Type to check registration for</param>
    /// <param name="instanceName">Instance name (optional)</param>
    /// <returns><c>true</c>if the type/instance name has been registered
    /// with the container; otherwise <c>false</c></returns>
    bool IsRegistered(Type type, string instanceName = null);


    /// <summary>
    /// Check if a particular type/instance name has been registered with the container
    /// </summary>
    /// <typeparam name="T">Type to check registration for</typeparam>
    /// <param name="instanceName">Instance name (optional)</param>
    /// <returns><c>true</c>if the type/instance name has been registered
    /// with the container; otherwise <c>false</c></returns>
    bool IsRegistered<T>(string instanceName = null);

    
    /// <summary>
    /// Resolve an instance of the requested type from the container.
    /// </summary>
    /// <param name="type">Requested type</param>
    /// <param name="instanceName">Instance name (optional)</param>
    /// <returns>The retrieved object</returns>
    object Resolve(Type type, string instanceName = null);


    /// <summary>
    /// Resolve an instance of the requested type from the container.
    /// </summary>
    /// <typeparam name="T">Requested type</typeparam>
    /// <param name="instanceName">Instance name (optional)</param>
    /// <returns>The retrieved object</returns>
    T Resolve<T>(string instanceName = null);
}

Storing the mappings

After creating the contract (interface) I can now create a class that will implement it – Container.cs. The first and probably the most important thing to do is to define how to store the type mappings – I used a Dictionary:

/// <summary>
/// IoC container
/// </summary>
public class Container : IContainer
{
    /// <summary>
    /// Key: object containing the type of the object to resolve and the name of the instance (if any);
    /// Value: delegate that creates the instance of the object
    /// </summary>
    private readonly Dictionary<MappingKey, Func<object>> mappings;


    /// <summary>
    /// Creates a new instance of <see cref="Container"/>
    /// </summary>
    public Container()
    {
        mappings = new Dictionary<MappingKey, Func<object>>();
    }

    // IContainer methods here....

}

As you can see the key of the Dictionary is of type MappingKey. This is a custom object containing 2 properties: the type to map and the instance name (requirement #6). The instance name is not mandatory so this value can be null. Given that this object is used as the key of a Dictionary it’s very important to override the methods Equals() and GetHashCode():

/// <summary>
/// Mapping key. See <see cref="IocContainer"/>
/// </summary>
internal class MappingKey
{
    /// <summary>
    /// Type of the dependency
    /// </summary>
    public Type Type { get; protected set; }

    /// <summary>
    /// Name of the instance (optional)
    /// </summary>
    public string InstanceName { get; protected set; }


    /// <summary>
    /// Creates a new instance of <see cref="MappingKey"/>
    /// </summary>
    /// <param name="type">Type of the dependency</param>
    /// <param name="instanceName">Name of the instance</param>
    /// <exception cref="ArgumentNullException">type</exception>
    public MappingKey(Type type, string instanceName)
    {
        if (type == null)
            throw new ArgumentNullException("type");

        Type = type;
        InstanceName = instanceName;
    }


    /// <summary>
    /// Returns the hash code for this instance
    /// </summary>
    /// <returns>The hash code for this instance</returns>
    public override int GetHashCode()
    {
        unchecked
        {
            const int multiplier = 31;
            int hash = GetType().GetHashCode();

            hash = hash * multiplier + Type.GetHashCode();
            hash = hash * multiplier + (InstanceName == null ? 0 : InstanceName.GetHashCode());

            return hash;
        }
    }


    /// <summary>
    /// Determines whether the specified object is equal to the current object.
    /// </summary>
    /// <param name="obj">The object to compare with the current object</param>
    /// <returns>
    /// <c>true</c> if the specified object is equal to the current object; otherwise, <c>false</c>.
    /// </returns>
    public override bool Equals(object obj)
    {
        if (obj == null)
            return false;

        MappingKey compareTo = obj as MappingKey;

        if (ReferenceEquals(this, compareTo))
            return true;

        if (compareTo == null)
            return false;

        return Type.Equals(compareTo.Type) && 
            string.Equals(InstanceName, compareTo.InstanceName, StringComparison.InvariantCultureIgnoreCase);
    }

    /// <summary>
    /// For debugging purposes only
    /// </summary>
    /// <returns>Returns a string that represents the current object.</returns>
    public override string ToString()
    {
        const string format = "{0} ({1}) - hash code: {2}";

        return string.Format(format, this.InstanceName ?? "[null]",
            this.Type.FullName,
            this.GetHashCode()
        );
    }

    /// <summary>
    /// In case you need to return an error to the client application
    /// </summary>
    /// <returns></returns>
    public string ToTraceString()
    {
        const string format = "Instance Name: {0} ({1})";

        return string.Format(format, this.InstanceName ?? "[null]",
            this.Type.FullName
        );
    }
}

The value of the Dictionary will be a delegate that returns an object, because it’s flexible enough to satisfy requirements #1 and #2. The return type of the delegate is of type object so it can be used to satisfy requirement #5 (support for generics and non-generics methods).

Register method

The generic version of the Register method invokes the non-generic version of the method:

/// <summary>
/// Register a type mapping
/// </summary>
/// <typeparam name="TFrom">Type that will be requested</typeparam>
/// <typeparam name="TTo">Type that will actually be returned</typeparam>
/// <param name="instanceName">Instance name (optional)</param>
public void Register<TFrom, TTo>(string instanceName = null) where TTo : TFrom
{
    Register(typeof(TFrom), typeof(TTo), instanceName);
}

This is the non-generic implementation of the Register method:

/// <summary>
/// Register a type mapping
/// </summary>
/// <param name="from">Type that will be requested</param>
/// <param name="to">Type that will actually be returned</param>
/// <param name="instanceName">Instance name (optional)</param>
public void Register(Type from, Type to, string instanceName = null)
{
    if (to == null)
        throw new ArgumentNullException("to");

    if(!from.IsAssignableFrom(to))
    {
        string errorMessage =  string.Format("Error trying to register the instance: '{0}' is not assignable from '{1}'",
            from.FullName, to.FullName);

        throw new InvalidOperationException(errorMessage);
    }

    Func<object> createInstanceDelegate = () => Activator.CreateInstance(to);
    Register(from, createInstanceDelegate, instanceName);
}

There are 2 important details in this method. First, we need to ensure that the types are valid i.e. that they are not null and that the from type is assignable from the to type:

if(!from.IsAssignableFrom(to))

Then we need to create a delegate (remember that the Dictionary that stores the type mappings has a value of type Func<object>). The delegate uses the method Activator.CreateInstance, that creates an instance of the specified type (using the default constructor):

Func<object> createInstanceDelegate = () => Activator.CreateInstance(to);

At last, we can invoke the overload of the Register method that takes a delegate as a parameter. See the following section for the implementation details.

Register method using a delegate

The generic version invokes the non-generic version of the method that takes a delegate as a parameter. The generic delegate must be casted to return an object because that’s the type that will be stored in the Dictionary:

/// <summary>
/// Register a type mapping
/// </summary>
/// <typeparam name="T">Type that will be requested</typeparam>
/// <param name="createInstanceDelegate">A delegate that will be used to
/// create an instance of the requested object</param>
/// <param name="instanceName">Instance name (optional)</param>
public void Register<T>(Func<T> createInstanceDelegate, string instanceName = null)
{
	if (createInstanceDelegate == null)
		throw new ArgumentNullException("createInstanceDelegate");

	Func<object> createInstance = createInstanceDelegate as Func<object> ;
	Register(typeof(T), createInstance, instanceName);
}

This is the non-generic version of the method:

/// <summary>
/// Register a type mapping
/// </summary>
/// <param name="type">Type that will be requested</param>
/// <param name="createInstanceDelegate">A delegate that will be used to
/// create an instance of the requested object</param>
/// <param name="instanceName">Instance name (optional)</param>
public void Register(Type type, Func<object> createInstanceDelegate, string instanceName = null)
{
	if (type == null)
		throw new ArgumentNullException("type");

	if (createInstanceDelegate == null)
		throw new ArgumentNullException("createInstanceDelegate");

	var key = new MappingKey(type, instanceName);

	if (mappings.ContainsKey(key))
	{
		const string errorMessageFormat = "The requested mapping already exists - {0}";
		throw new InvalidOperationException(string.Format(errorMessageFormat, key.ToTraceString()));
	}

	mappings.Add(key, createInstanceDelegate);
}

As you can see, to register a type mapping is nothing more than adding a new value to the Dictionary.

First thing to do is to create an instance of MappingKey object – as mentioned before, this will be used as the key of the Dictionary. If the key already exists this means that the mapping was already registered so an exception is thrown. If the key doesn’t exist we can then add a new mapping to the Dictionary.

IsRegistered method

Just like the other generic methods, the generic method invokes the non-generic method:

/// <summary>
/// Check if a particular type/instance name has been registered with the container
/// </summary>
/// <typeparam name="T">Type to check registration for</typeparam>
/// <param name="instanceName">Instance name (optional)</param>
/// <returns><c>true</c>if the type/instance name has been registered
/// with the container; otherwise <c>false</c></returns>
public bool IsRegistered<T>(string instanceName = null)
{
	return IsRegistered(typeof(T), instanceName);
}

Then, in the non generic method all we need to do is to create an instance of a MappingKey object based on the requested type/instance name and check if the Dictionary contains that key:

/// <summary>
/// Check if a particular type/instance name has been registered with the container
/// </summary>
/// <param name="type">Type to check registration for</param>
/// <param name="instanceName">Instance name (optional)</param>
/// <returns><c>true</c>if the type/instance name has been registered
/// with the container; otherwise <c>false</c></returns>
public bool IsRegistered(Type type, string instanceName = null)
{
	if (type == null)
		throw new ArgumentNullException("type");

	var key = new MappingKey(type, instanceName);
	return mappings.ContainsKey(key);
}

Resolve method

This is the method that creates the instances of the requested objects. The generic version invokes the non-generic version:

/// <summary>
/// Resolve an instance of the requested type from the container.
/// </summary>
/// <typeparam name="T">Requested type</typeparam>
/// <param name="instanceName">Instance name (optional)</param>
/// <returns>The retrieved object</returns>
public T Resolve<T>(string instanceName = null)
{
	object instance = Resolve(typeof(T), instanceName);

	return (T) instance;
}

This is the non-generic method implementation:

/// <summary>
/// Resolve an instance of the requested type from the container.
/// </summary>
/// <param name="type">Requested type</param>
/// <param name="instanceName">Instance name (optional)</param>
/// <returns>The retrieved object</returns>
public object Resolve(Type type, string instanceName = null)
{
	var key = new MappingKey(type, instanceName);
	Func<object> createInstance;

	if (mappings.TryGetValue(key, out createInstance))
	{
		var instance = createInstance();
		return instance;
	}

	const string errorMessageFormat = "Could not find mapping for type '{0}'";
	throw new InvalidOperationException(string.Format(errorMessageFormat, type.FullName));
}

Similarly to the IsRegistered method, we need to create an instance of a MappingKey object based on the requested type/instance name, and then get the corresponding value (a delegate) from the Dictionary. Executing the delegate will give us the requested object. If the requested type/instance name does not exist an InvalidOperationException is thrown.

Container class full implementation

This is the full implementation of the Container class:

using System;
using System.Collections.Generic;


/// <summary>
/// IoC container
/// </summary>
public class Container : IContainer
{
    /// <summary>
    /// Key: object containing the type of the object to resolve and the name of the instance (if any);
    /// Value: delegate that creates the instance of the object
    /// </summary>
    private readonly Dictionary<MappingKey, Func<object>> mappings;


    /// <summary>
    /// Creates a new instance of <see cref="Container"/>
    /// </summary>
    public Container()
    {
        mappings = new Dictionary<MappingKey, Func<object>>();
    }


    /// <summary>
    /// Register a type mapping
    /// </summary>
    /// <param name="from">Type that will be requested</param>
    /// <param name="to">Type that will actually be returned</param>
    /// <param name="instanceName">Instance name (optional)</param>
    public void Register(Type from, Type to, string instanceName = null)
    {
        if (to == null)
            throw new ArgumentNullException("to");

        if(!from.IsAssignableFrom(to))
        {
            string errorMessage =  string.Format("Error trying to register the instance: '{0}' is not assignable from '{1}'",
                from.FullName, to.FullName);

            throw new InvalidOperationException(errorMessage);
        }

        Func<object> createInstanceDelegate = () => Activator.CreateInstance(to);
        Register(from, createInstanceDelegate, instanceName);
    }


    /// <summary>
    /// Register a type mapping
    /// </summary>
    /// <typeparam name="TFrom">Type that will be requested</typeparam>
    /// <typeparam name="TTo">Type that will actually be returned</typeparam>
    /// <param name="instanceName">Instance name (optional)</param>
    public void Register<TFrom, TTo>(string instanceName = null) where TTo : TFrom
    {
        Register(typeof(TFrom), typeof(TTo), instanceName);
    }


    /// <summary>
    /// Register a type mapping
    /// </summary>
    /// <param name="type">Type that will be requested</param>
    /// <param name="createInstanceDelegate">A delegate that will be used to 
    /// create an instance of the requested object</param>
    /// <param name="instanceName">Instance name (optional)</param>
    public void Register(Type type, Func<object> createInstanceDelegate, string instanceName = null)
    {
        if (type == null)
            throw new ArgumentNullException("type");

        if (createInstanceDelegate == null)
            throw new ArgumentNullException("createInstanceDelegate");


        var key = new MappingKey(type, instanceName);

        if (mappings.ContainsKey(key))
        {
            const string errorMessageFormat = "The requested mapping already exists - {0}";
            throw new InvalidOperationException(string.Format(errorMessageFormat, key.ToTraceString()));
        }


        mappings.Add(key, createInstanceDelegate);
    }


    /// <summary>
    /// Register a type mapping
    /// </summary>
    /// <typeparam name="T">Type that will be requested</typeparam>
    /// <param name="createInstanceDelegate">A delegate that will be used to 
    /// create an instance of the requested object</param>
    /// <param name="instanceName">Instance name (optional)</param>
    public void Register<T>(Func<T> createInstanceDelegate, string instanceName = null)
    {
        if (createInstanceDelegate == null)
            throw new ArgumentNullException("createInstanceDelegate");

        Func<object> createInstance = createInstanceDelegate as Func<object> ;
        Register(typeof(T), createInstance, instanceName);
    }


    /// <summary>
    /// Check if a particular type/instance name has been registered with the container
    /// </summary>
    /// <param name="type">Type to check registration for</param>
    /// <param name="instanceName">Instance name (optional)</param>
    /// <returns><c>true</c>if the type/instance name has been registered 
    /// with the container; otherwise <c>false</c></returns>
    public bool IsRegistered(Type type, string instanceName = null)
    {
        if (type == null)
            throw new ArgumentNullException("type");


        var key = new MappingKey(type, instanceName);

        return mappings.ContainsKey(key);
    }


    /// <summary>
    /// Check if a particular type/instance name has been registered with the container
    /// </summary>
    /// <typeparam name="T">Type to check registration for</typeparam>
    /// <param name="instanceName">Instance name (optional)</param>
    /// <returns><c>true</c>if the type/instance name has been registered 
    /// with the container; otherwise <c>false</c></returns>
    public bool IsRegistered<T>(string instanceName = null)
    {
        return IsRegistered(typeof(T), instanceName);
    }


    /// <summary>
    /// Resolve an instance of the requested type from the container.
    /// </summary>
    /// <param name="type">Requested type</param>
    /// <param name="instanceName">Instance name (optional)</param>
    /// <returns>The retrieved object</returns>
    public object Resolve(Type type, string instanceName = null)
    {
        var key = new MappingKey(type, instanceName);
        Func<object> createInstance;

        if (mappings.TryGetValue(key, out createInstance))
        {
            var instance = createInstance();
            return instance;
        }

        const string errorMessageFormat = "Could not find mapping for type '{0}'";
        throw new InvalidOperationException(string.Format(errorMessageFormat, type.FullName));
    }


    /// <summary>
    /// Resolve an instance of the requested type from the container.
    /// </summary>
    /// <typeparam name="T">Requested type</typeparam>
    /// <param name="instanceName">Instance name (optional)</param>
    /// <returns>The retrieved object</returns>
    public T Resolve<T>(string instanceName = null)
    {
        object instance = Resolve(typeof(T), instanceName);

        return (T) instance;
    }


    /// <summary>
    /// For debugging purposes only
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        if (mappings == null)
            return "No mappings";

        return string.Join(Environment.NewLine, this.mappings.Keys);
    }

}

4. Using the code

In this section you can find some unit tests (using NUnit) that show how to use the IoC container (you can find more unit tests in the demo project). This is the model used in the examples:

public interface ILogger
{
    void Write(string message);
}

public class UselessLogger : ILogger
{
    public void Write(string message)
    {
        // I'm a useless logger! I don't log anything ;-(
    }
}

public class ConsoleLogger : ILogger
{
    public ConsoleColor ConsoleColor { get; private set; }

    public ConsoleLogger() : this(ConsoleColor.Blue)
    {

    }

    public ConsoleLogger(ConsoleColor consoleColor)
    {
        ConsoleColor = consoleColor;

    }
    public void Write(string message)
    {
        Console.BackgroundColor = ConsoleColor;
        Console.Write(message);
        Console.ResetColor();
    }
}

Registering an object

// arrange
var container = new Container();
container.Register(typeof(ILogger), typeof(ConsoleLogger));

// act
bool isRegistered = container.IsRegistered(typeof(ILogger));

// assert
Assert.IsTrue(isRegistered);

Registering an object using generics

// arrange
var container = new Container();
container.Register<ILogger, ConsoleLogger>();

// act
bool isRegistered = container.IsRegistered<ILogger>();

// assert
Assert.IsTrue(isRegistered);

Registering an object using generics and a named instance

// arrange
const string instanceName = "console";

var container = new Container();
container.Register<ILogger, ConsoleLogger>(instanceName);

// act
bool isRegistered = container.IsRegistered<ILogger>(instanceName);

// assert
Assert.IsTrue(isRegistered);

Registering an object using a delegate

// arrange
var container = new Container();
container.Register(typeof(ILogger), () => new ConsoleLogger(ConsoleColor.Green));

// act
bool isRegistered = container.IsRegistered(typeof(ILogger));

// assert
Assert.IsTrue(isRegistered);

Registering an object using a delegate and generics

// arrange
var container = new Container();
container.Register<ILogger>(() => new ConsoleLogger(ConsoleColor.Green));

// act
bool isRegistered = container.IsRegistered<ILogger>();

// assert
Assert.IsTrue(isRegistered);

Resolving an object

// arrange
var container = new Container();
container.Register(typeof(ILogger), typeof(ConsoleLogger));

// act
object logger = container.Resolve(typeof(ILogger));

// assert
Assert.IsInstanceOf<ConsoleLogger>(logger);

Resolving an object using generics

// arrange
var container = new Container();
container.Register<ILogger, ConsoleLogger>();

// act
ILogger logger = container.Resolve<ILogger>();

// assert
Assert.IsInstanceOf<ConsoleLogger>(logger);

Resolving an object using generics and a named instance

// arrange
const string instanceName1 = "logger1";
const string instanceName2 = "logger2";

var container = new Container();
container.Register<ILogger, ConsoleLogger>(instanceName1);
container.Register<ILogger, UselessLogger>(instanceName2);

// act
ILogger logger = container.Resolve<ILogger>(instanceName1);

// assert
Assert.IsInstanceOf<ConsoleLogger>(logger);

References

Downloads

Download the demo project containing the IoC container implementation and some unit tests: IocContainer – VS2010 solution.zip

2 thoughts on “Implementing a basic IoC container using C#

    • Hi Hiep Le,

      Please note that this is a very basic implementation of an IoC container, some functionalities such as managing the lifetime of an object or resolving dependencies dynamically are not implemented.

      Consider the following model:


      public interface ILogger
      {
      void Write(string message);
      }

      public class ConsoleLogger : ILogger
      {
      public ConsoleColor ConsoleColor { get; private set; }

      public ConsoleLogger() : this(ConsoleColor.Blue)
      {
      }

      public ConsoleLogger(ConsoleColor consoleColor)
      {
      ConsoleColor = consoleColor;
      }

      public void Write(string message)
      {
      Console.BackgroundColor = ConsoleColor;
      Console.Write(message);
      Console.ResetColor();
      }
      }

      public interface IFooService
      {
      void DoSomething();
      }

      public class FooService : IFooService
      {
      public ILogger Logger { get; set; }

      public FooService(ILogger logger)
      {
      Logger = logger;
      }

      public void DoSomething()
      {
      // ...
      }
      }

      A possible workaround (not tested):


      var container = new Container();
      container.Register<ILogger>(() => new ConsoleLogger(ConsoleColor.Green));
      container.Register<IFooService>(() => new FooService(container.Resolve<ILogger>()));

      Rui

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s