Tutorials

IoC Demystified (Part 2 of 2)

The second of a two part series discussing the depths of inversion-of-control and how it works

Danl + Liz
A man jumping on a intermodal container

Continuing from part 1 of this 2 part series, we'll conclude this series with building a simple dependency injection container as well as putting the tutorial together as a whole.

The container

For this section, we'll create our own simple dependency injection container, purely for educational reasons. In the real world, I would advise to use a battle-tested solution, as there are many to choose from, such as StructureMap, Unity, DryIoC, and many more. I will include a section at the end, listing my personal recommendations.

To build our own container, we'll need to list what we would like our container to accomplish:

  1. Must be able to register service interfaces to service implementations mappings into a managed registry

    Should be able to utilize some basic validations, such as ensuring registered implementations derive from the corresponding interfaces

  2. Must be able to request a service interface and container will populate implementation dependencies from managed registry

    Will require generics and System.Reflection to obtain type information from constructors, as well as dynamically creating instances from type information.

I tend to start with the more complicated parts first and make a proof-of-concept. So lets start with the second item of our requirements. In this sample, I'll create a method called CreateDefault and the purpose is to be able to accept a generic of T, constrained to only allow class types, and obtain the default constructor, invoke it, and return the new instance (or null if there was no default constructor).

Example

using System;

internal class Example
{
    public override string ToString() => "Hello World";
}

internal static class Program
{
    private static T CreateDefault<T>() where T : class
    {
        var defaultConstructor = typeof(T)
            .GetConstructor(Array.Empty<Type>());

        return defaultConstructor
            ?.Invoke(Array.Empty<Type>()) as T;
    }

    private static void Main()
    {
        var example = CreateDefault<Example>();
        Console.WriteLine(example);
    }
}

Now that we have a basic proof-of-concept, let's build an interface for our container.

public interface IContainer
{
    void Register<TServiceKey, TServiceValue>()
        where TServiceValue : class, TServiceKey
        where TServiceKey : class;
    TService Resolve<TService>() where TService : class;
    object Resolve(Type type);
}

Let's start stubbing out our implementation. Since we'll be using the TServiceKey as a means to retrieve the TServiceValue, the mechanism that made the most sense was to store our mappings as System.Type into a dictionary. This way, we can just use TServiceKey directly as a key, and ensure that there are no overlapping records stored.

using System;
using System.Collections.Generic;
using System.Linq;

public class Container : IContainer
{
    private readonly Dictionary<Type, Type> _registry;

    public Container()
    {
        _registry = new Dictionary<Type, Type>();
    }

    public void Register<TServiceKey, TServiceValue>()
        where TServiceValue : class, TServiceKey
        where TServiceKey : class
    {
        _registry.Add(typeof(TServiceKey), typeof(TServiceValue));
    }

    // ...
}

For the Resolve method, we'll start off by building two separate methods, a generic method and a non-generic method. The bulk of the work will exist in our non-generic method.

Breaking down the steps:

  1. This method will need to be used as a recursive method. The type parameter passed in will either represent the initial TService key we were trying to resolve, or it will be a nested dependency from a series of constructor parameters.

  2. Whether we were able to retrieve a value from the registry or default to the parameter, we will retrieve the corresponding constructors of that type and attempt to resolve the constructor parameter types from the registry. Each constructor parameter will use recursion to call this method until all nested parameters within are resolved.

  3. Once a type's constructor parameters are resolved, we will attempt to invoke the first available constructor that was resolved, or call the default constructor and return the result.

// ...
    public TService Resolve<TService>() where TService : class
    {
        var type = typeof(TService);
        return Resolve(type) as TService;
    }

    public object Resolve(Type type)
    {
        var resolvedType =
            _registry.TryGetValue(type, out var registeredType)
                ? registeredType
                : type;

        var resolvedConstructorArgs = resolvedType
            .GetConstructors()
            .Select(ctorInfo =>
                KeyValuePair.Create(
                    ctorInfo,
                    ctorInfo
                        .GetParameters()
                        .Select(parameter => Resolve(parameter.ParameterType))
                        .ToArray()))
            .Where(parameters => parameters.Value.Any())
            .ToList();

        if (resolvedConstructorArgs.Any())
        {
            var (constructor, constructorArgs) = resolvedConstructorArgs.FirstOrDefault();
            return constructor.Invoke(constructorArgs);
        }

        return resolvedType.GetConstructor(Array.Empty<Type>()).Invoke(Array.Empty<object>());
    }
}

Putting it all together

To complete our example, we'll register the IBlogPostService, IBlogPostRepository, and the INotificationService interfaces, then we'll resolve the IBlogPostService and create a blog post.

internal static class Program
{
    private static void Main()
    {
        var container = new Container();
        container.Register<IBlogPostService, BlogPostService>();
        container.Register<IBlogPostRepository, BlogPostRepository>();
        container.Register<INotificationService, NotificationService>();

        var service = container.Resolve<IBlogPostService>();
        service.CreateBlogPost("Demoing an IoC example", "Danl Barron", "...");
    }
}

When you run the console app, you should see two console messages. One telling you that the repository 'saved to disk' and the other one 'sending out a notification'.

Blog post: `Demoing an IoC example` by Danl Barron successfully saved to disk

Sending Message:
Hi subscribers!
  Danl Barron has just uploaded a new blog post, `Demoing an IoC example`.
  Check it out and be sure to add a thumbs up!
Thanks, The Coding Bruises staff

Choosing a battle-tested container.

As for choosing a battle-tested dependency injection container, I myself would start with the built-in solution starting with .NET Core and now available in .NET 5. If you decide you need to look into replacements, here are just a few to choose from.

Conclusion

I hope this helped demonstrate how inversion-of-control works. Typically in an application, you would limit this to just a few places where dependencies and business logic can naturally flow downwards as to avoid anti-pattern designs. A good example where inversion-of-control/dependency injection would be used, would be hooking into an web framework's controller factory pipeline.

Example:

public class HttpControllerFactory : IHttpControllerFactory
{
    private readonly IContainer _container;

    public HttpControllerFactory()
    {
        _container = new Container();

        // Add service registrations
        // _container.register<TServiceKey, TServiceValue>();
    }

    public IHttpController Create(
        HttpRequestMessage request,
        HttpControllerDescriptor controllerDescriptor,
        Type controllerType)
    {
        return _container.Resolve(controllerType) as IHttpController;
    }
}

The simple dependency injection container we created lacks many features you would find out of a battle-tested container framework, like service lifetimes, garbage collection, and a slew of other features. Feel free to clone this code-base over at github and try to add missing features you might find from one of the more popular inversion-of-control frameworks.