This post is part of the series SOLID Wash Tunnel.

Definition

IoC Principle

Inversion of control (IoC) is a design principle which basically boils down to the question.

Who has control about the objects in an application?

Every OOP application has objects, and those objects have to be managed. Objects can be temporary (method level), while some are passed around the entire application to provide specific functionality. The creation, lifespan, and destruction of objects is usually let to you, the developer, to control.

What IoC does is that it inverts the responsibility of managing these objects, and shifts the control away, from you the developer, towards the framework you are using (.NET, Spring, Django etc.)

IoC Container

An IoC container in the other hand is responsible to manage object creation, lifespan, destruction, and also inject dependencies where needed.
This is achived by means of injecting the dependencies through a constructor, a property or a method at run-time.

We can define an IoC container as follows:

An IoC container is a framework for implementing automatic dependency injection.


Implementation

Right of the bat, I do not suggest to build your own IoC container, unless you need something very specific that existing containers do not provide. Other than that always use well established containers such as:


It is a good practice to sepparate the abstractions of the container from its implementation. That is why we have created 2 projects:

  • SOLIDWashTunnel.DI
  • SOLIDWashTunnel.DI.Abstractions

SOLIDWashTunnel.DI.Abstractions

This project contains one interface IContainer.

Every project that needs to register its components, makes use of this interface.
The interface enables consuming libraries to perform the following actions:

  • Transient service registration via delegate invocation (implementationFactory).
  • Transient service registration via type declaration (TService, TImplementation).
  • Singleton service registration via delegate invocation (implementationFactory).
  • Singleton service registration via type declaration (TService, TImplementation).
  • Service decoration via type declaration (TService, TImplementation).
    (Lifetime of the decorator service is inherited from the service being decorated).
  • Resolving via concrete type (type).
  • Resolving via type declaration (TService).
public interface IContainer
{
void AddTransient<TService>(Func<TService> implementationFactory);
void AddTransient<TService, TImplementation>() where TImplementation : TService;
void AddSingleton<TService>(Func<TService> implementationFactory);
void AddSingleton<TService, TImplementation>() where TImplementation : TService;
void Decorate<TService, TImplementation>() where TImplementation : TService;
object GetService(Type type);
TService GetService<TService>();
}

Ideally you would sepparate the methods used to resolve services, from the container into a service provider. After services have been registered to the container you could build a service provider, from which the registered services can be resolved from. This would align with SRP and ISP (contribute 😉)

SOLIDWashTunnel.DI

This project contains the implementation of interface IContainer namely Container. The only project that references it, is the console app SOLIDWashTunnel.Customers. The reason why is because it is the Composition Root!

The composition root is an application infrastructure component that
only executables should contain. Libraries and frameworks shouldn't!

It is a (preferably) unique location in the overall project, where all modules are composed together. The implementation of our custom IoC container is very minimalistic but it servers as a good learning step.

The service descriptors are stored in a Dictionary which is empty when a container instance gets created by the client code. The dictionary Key is the type of the service (usually its interface but can be the type of the concrete service itself) whereas the Value is a Func<object> delegate that gets invoked when the service needs to be resolved, which in turn provides the concrete service instance.

Based on the type of registration both AddTransient and AddSingleton store the Key as the type of the generic TService parameter, and the Value as a callback of either:

  • The implementationFactory invocation.
  • Or the GetService return type.

AddSingleton methods makes use of lazy initialization which basically means that its creation is deferred until it is first used, and that only one instance is returned.

In a multi-threaded scenario you could use ThreadLocal<T>.

Decorate retrieves the current implementation of the decorated service and creates a new instance of the decorator. Since our container does not support multiple services of the same type, we remove from the registrations the current service and add our decorator.*

For real projects, I suggest using Scrutor, or any other IoC container that supports decoration.

Resolving services via GetService checks if type is assignable from IContainer, if that is the case it returns the container instance itself. This means that some part of the codebase (which did not create the container), is requesting for it.

There could be 2 reasons for it:

  • The calling code wants to register more services to the container.
  • The calling code wants to resolve a certain services from the container.

If possible always try to avoid this in real world projects, as it represents the Service Locator anti-pattern.

Going back to GetService. If type is not assignable from IContainer, than it checks if it already has been registered. If it is the case than it invokes the delegate implementationFactory and returns the service implementation.

If type has not yet been registered, is not an interface, and is not abstract than it creates the service and returns it.

CreateService takes the implementationType and creates an instance of it. It does so by gathering all the neccessary information (via reflection) about the class constructor parameters and dependecies that it needs.

One thing that should be noted is that the dependencies need to have been registered to the container beforehand. This means order of registration is important!

At the end all these parameters and the type get fed to the Activator.CreateInstance which creates the instance we need.

public class Container : IContainer
{
private Dictionary<Type, Func<object>> _serviceDescriptors;

public Container()
{
_serviceDescriptors = new Dictionary<Type, Func<object>>();
}

public void AddTransient<TService>(Func<TService> implementationFactory)
=> _serviceDescriptors.Add(typeof(TService), () => implementationFactory());

public void AddTransient<TService, TImplementation>() where TImplementation : TService
=> _serviceDescriptors.Add(typeof(TService), () => GetService(typeof(TImplementation)));


public void AddSingleton<TService>(Func<TService> implementationFactory)
{
var lazy = new Lazy<TService>(implementationFactory);
AddTransient(() => lazy.Value);
}

public void AddSingleton<TService, TImplementation>() where TImplementation : TService
{
var service = (TService)GetService(typeof(TImplementation));
var lazy = new Lazy<TService>(service);
AddTransient(() => lazy.Value);
}


public void Decorate<TService, TImplementation>() where TImplementation : TService
{
Type serviceType = typeof(TService);
Type implementationType = typeof(TImplementation);

TService decorated = GetService<TService>();

if (decorated == null)
throw new InvalidOperationException($"Can not register decorator {implementationType}, if no decorated service of {serviceType} has be registered.");

var decorator = CreateService(implementationType);

_serviceDescriptors.Remove(serviceType);
_serviceDescriptors.Add(serviceType, () => decorator);
}

public object GetService(Type type)
{
if (type.IsAssignableFrom(typeof(IContainer)))
return this;
if (_serviceDescriptors.TryGetValue(type, out Func<object> implementationFactory))
return implementationFactory();
else if (!type.IsInterface && !type.IsAbstract)
return CreateService(type);
else
throw new InvalidOperationException($"No registration for {type}.");
}

public TService GetService<TService>()
{
return (TService)GetService(typeof(TService));
}

private object CreateService(Type implementationType)
{
var ctor = implementationType.GetConstructors().Single();
var parameterTypes = ctor.GetParameters().Select(p => p.ParameterType);
var dependencies = parameterTypes.Select(t => GetService(t)).ToArray();
return Activator.CreateInstance(implementationType, dependencies);
}
}

Continue the series on Dependency Injection.

If you found this article helpful please give it a share in your favorite forums 😉.
The solution project is available at GitHub.