Writing a Minimal IoC Container in C#

Mentoring new engineers and developers is an exciting part of working at Encora.

 

What is the difference between Inversion of Control and Dependency Injection? This sort of confusion around the two terms comes up frequently when working with new engineers or interns. As a mentor, it’s a great opportunity to clarify and share shortcuts. 

While initially they might appear confusing, writing a minimal container is a great way to illustrate the differences of these two concepts. 

It’s easiest to understand Inversion of Control (IoC) and Dependency Injection (DI) by writing an IoC container as small as possible. The process of writing the IoC container makes obvious why dependency injection is useful.

Heading

Even though they are related, IoC is broader than DI. IoC means that one code calls another; DI goes beyond that and implements IoC by using composition.
In IoC, a framework controls how modules are instantiated and managed.
DI solves the problem of an application’s independence from its objects. This is very useful when a configurable application is needed or when unit testing involves mock objects. DI helps modify an application more easily as it grows in size and complexity.
A DI or IoC container needs to instantiate objects (dependencies) and provide them to the application. To do so, it must to deal with
Constructor injection
Setter injection
Interface injection
The container needs to automatically instantiate objects of specific types and pass them in the constructors, setter methods, or interfaces to classes. Constructor injection will be the focus here since it is the most common injection.
The container will support registering concrete types of interfaces and it will resolve them when requested by the user. It will auto resolve the constructor parameters as well.

An Example

An interface was written called IWriter, including a Write method. The IWriter interface is implemented by a Writer concrete class. This is registered in the container when initializing the application. Later, the app can request an IWriter instance to the container.

container.Register<IWriter, Writer>(); … … Writer writer = container.Create();

In an article Ayende impressively wrote an IoC container in only 15 lines of code. 

This post, however, is old and can be improved upon with the help of generics and reflection. Ayende’s code was modified resulting in an IoC container in 11 lines of code. It does not use delegates to instantiate objects.

This minimal IoC container does type checking and ensures that registered types implement the interfaces/classes with which they are registered. Generics are used to accomplish this.

It also auto-instantiates objects for the types registered. It resolves constructor parameters and auto instantiates them when resolving types.

 


public class MinimalContainer
{
	private readonly Dictionary<Type, Type> types = new Dictionary<Type, Type>();
    
    public void Register<TInterface, TImplementation>() where TImplementation : TInterface
    {
    	types[typeof(TInterface)] = typeof(TImplementation);
    }
    
    public TInterface Create()
    {
    	return (TInterface)Create(typeof(TInterface));
    }
    
    private object Create(Type type)
    {
    	//Find a default constructor using reflection
        var concreteType = types[type];
        var defaultConstructor = concreteType.GetConstructors()[0];
        //Verify if the default constructor requires params
        var defaultParams = defaultCtor.GetParameters();
        //Instantiate all constructor parameters using recursion
        var parameters = defaultParams.Select(param => Create(param.ParameterType)).ToArray();
        
        return defaultConstructor.Invoke(parameters);
    }
    
}

 

This is how to use the container.

 


var container = new MinimalContainer();

container.Register<IWelcomer, Welcomer>();
container.Register<IWriter, ConsoleWriter>();

var welcomer = container.Create();
welcomer.SayHelloTo("World");

 

Here are the classes used by the example above,

 


public interface IWelcomer {
	void SayHelloTo(string name);
}

public class Welcomer : IWelcomer
{
    private IWriter writer;

    public Wey(IWriter writer) {
        this.writer = writer;
    }

    public void SayHelloTo(string name)
    {
        writer.Write($"Hello {name}!");            
    }
}

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

public class ConsoleWriter : IWriter
{
    public void Write(string s)
    {
        Console.WriteLine(s);
    }
}

This is NOT production ready, obviously. Note that there is no config file: this container only supports constructor injection and registration in the code. A complete IoC container would support more features and other types of injections, registration in configuration files, and managing the object life-cycle.

Full blown containers such AutoFac, Ninject or StructureMap are recommended when writing production code. This exercise is an easy way to teach a less experienced developer the difference between IoC and DI in a simple way.

Stay up to date!

Here you can find the everyday problems developers solve, testing best practices, and lots of posts about our unique culture.

Contact Us

Key Takeaways:

  • It’s easiest to understand Inversion of Control (IoC) and Dependency Injection (DI) by writing an IoC container as small as possible. The process of writing the IoC container makes obvious why dependency injection is useful.
  • Even though they are related, IoC is broader than DI. IoC means that one code calls another; DI goes beyond that and implements IoC by using composition.
  • A DI or IoC container needs to instantiate objects (dependencies) and provide them to the application. To do so, it must deal with constructor injection, setter injection, and interface injection. 
  • The container will support registering concrete types of interfaces and it will resolve them when requested by the user. It will auto resolve the constructor parameters as well.

About Encora

At Encora, we create competitive advantages for client partners through accelerated technology innovation. We would be delighted to take you further along your journey.

Why Encora 

  • We’re global: With 20+ offices and innovation labs in 12 countries, Encora is globally dispersed. Since we operate in different time zones, there is always someone ready to assist you.
  • We’re full-service: Technology innovation encompasses a huge array of topics, skills, and strategies. We offer a full-service concept, each component custom tailored as per your needs, to present a complete solution. 
  • We’re dedicated: Our ever-growing team of over 4,000 skilled and dedicated programmers, developers, engineers and strategic thinkers is the reason Encora has become an award-winning tech company with an enviable reputation.

We’re experienced: We partner primarily with fast-growing tech companies who are driving innovation and growth within their industries. Our unique delivery model, agile methodology, and consistent unmatched quality have contributed to our steady growth.

Share this post

Table of Contents