Really simple IoC/DI container implementation

I’m sure you know (or at least have heard of) Inversion of Control/Dependency Injection principle. It’s a very handy principle that leads to plugable application design, simplifies unit-testing and so I can recommend its using.
I wanted to use some really simple IoC/DI container with small footprint. Unity or PicoContainer.NET are propagated as “lightweight IoC/DI containers” so I wanted to use one of them. Before I had looked into source code of Unity. And…what the hell? Lightweight? There are dozens of classes and interfaces! I’m sure that Unity is really good piece of software but I needed something less sophisticated and more fast. Despite IoC/DI is very strong principle it’s very simple to implement – so I decided to write my own really lightweight IoC/DI container. Just because I love programming 😉

I can never say that I will not change the used IoC/DI container (e.g. when I would want to use some heavy Unity features or separate file configuration) so I decided that my container will implement the ServiceLocatorImplBase abstract class from CommonServiceLocator library. So I had to implement two methods – DoGetInstance and DoGetAllInstances. Implementing of this abstract class ensures that I will use one interface (from CommonServiceLocator) in whole application (except registrations). And so if I want to use some other IoC/DI container then I would change types registration only (because it’s always container specific). The rest of application wouldn’t require changes.

The storage

What does container do? It maps type to some instance. So let’s start with this storage:

Dictionary<Type, Func<Type, object>> _Registrations;

We map Type to delegate. The delegate has one parameter (Type) and returns instance of this type. So we map type to method that obtains type instance. The method can just call parameterless constructor or it can be something more sophisticated. We are not limited when writing this method – it’s a regular method.
The DoGetInstance method has actually two parameters – Type and string (key). So we have to add one mapping to our storage and one parameter to our delegate:

Dictionary<Type, Dictionary<string, Func<Type, string, object>>> _Registrations;

Actually, it could be handy to have access to container from creating method so I add one more parameter to the delegate:

Dictionary<Type, Dictionary<string, Func<SimpleContainer, Type, string, object>>> _Registrations;

And that’s all! We are now able to store all data we need.

Methods

Next, we have to implement DoGetInstance method. It’s very straightforward:

protected override object DoGetInstance(Type serviceType, string key)
{
	var creators = _Registrations[serviceType]; // map type
	if (creators != null && creators.Count > 0)
	{
		Func<SimpleContainer, Type, string, object> creator;
		if (string.IsNullOrEmpty(key) || !creators.TryGetValue(key, out creator) || creator == null) // try to map key
		{
			// use empty key as fallback
			creators.TryGetValue(string.Empty, out creator);
		}
		if (creator != null)
		{
			return creator(this, serviceType, key);
		}
	}
	throw new ActivationException(string.Format("Creator for '{0}' for key '{1}' not found.", serviceType, key));	
}

Next, we have to implement some method that will register method for type instance obtaining:

public void Register(Type serviceType, string key, Func<SimpleContainer, Type, string, object> creator)
{
	Dictionary<string, Func<SimpleContainer, Type, string, object>> creators;
	if (!_Registrations.TryGetValue(serviceType, out creators))
	{
		_TypeToCreators.Add(serviceType, creators = new Dictionary<string, Func<SimpleContainer, Type, string, object>>(StringComparer.OrdinalIgnoreCase));
	}
	creators[key ?? string.Empty] = creator;
}

Very easy, I think. And that’s all! We now have IoC container that is able to handle most of our requirements. Don’t you believe me? Let’s implement some supporting extensions methods that handle common scenarios.
Before we start I would like to notice that I implemented DoGetAllInstances abstract method in very similar way too. And I added common IDisposable pattern into our container too (check source code download at the end of this article).

Basic overloads

I really like strong typed languages and so I like generics. Let’s implement generic Register method:

public static void Register<T>(this SimpleContainer container, string key, Func<SimpleContainer, Type, string, object> creator)
{
	container.Register(typeof(T), key, creator);
}

Our container should be able to serve singleton for us:

public static void RegisterInstance<T>(this SimpleContainer container, string key, T instance)
{
	// we store instance reference in scope of lambda function bellow
	container.Register(typeof(T), key, (c, t, k) => instance);
}

Do you want to dispose singleton at the end of program (more exactly – when container is disposing) ?

public static void RegisterSingleton<T>(this SimpleContainer container, string key, T instance)
{
	var asIDisposable = instance as IDisposable;
	if (asIDisposable != null)
	{
		container.Disposing += (sender, eventArgs) => asIDisposable.Dispose();
	}
	container.Register(typeof(T), key, (c, t, k) => instance);
}

Dependency injection

Well, we have very handy and lightweight container but it is not IoC/DI container – we don’t manage dependency injection at all. If we want to use dependency injection then we can do that by hand:

container.Register<IMyInterface>((c, t, k) => new MyInterfaceImplementation(
	c.GetInstance<ISomeOtherInterface>(),
	c.GetInstance<IDifferentInterface>()));

Our MyInterfaceImplementation class has constructor with two parameters and we have to resolve these parameters when we are constructing the object. So we just delegate this task recursively to container. Pretty cool!
But programmers (including me) are too lazy and so we want to simplify this very common task:

public static void Register<T, TImplementation>(this SimpleContainer container, string key)
{
	var ctors = type.GetConstructors();
	if (ctors == null || ctors.Length == 0)
	{
		throw new ActivationException(string.Format("Appropriate constructor not found in type '{0}'.", type));
	}
	var ctor = ctors[0];

	var containerParameter = Expression.Parameter(typeof(SimpleContainer), "container");
	var typeParameter = Expression.Parameter(typeof(Type), "type");
	var keyParameter = Expression.Parameter(typeof(string), "key");

	var creator = Expression.Lambda(typeof(Func<SimpleContainer, Type, string, object>),
		Expression.New(ctor,
			// ctor parameters
			ctor.GetParameters().Select(p => (Expression)Expression.Call(containerParameter, "GetInstance", new[] { p.ParameterType }))),
		containerParameter, typeParameter, keyParameter); // lambda parameters
	return (Func<SimpleContainer, Type, string, object>)creator.Compile();
}

This may look difficult but it’s very easy – we construct lambda expression that invokes constructor and supplies constructor parameters via container.GetInstance method call. Just the same that we did by hand.

Registration overriding

We usually use one main container but sometimes we could want to change resolving for some specific types. Then we can use containers hierarchy that handles registration overriding.
We have to add ParentContainer property (e.g. of type SimpleContainer) to our SimpleContainer class. Then we create two extension methods:

public static SimpleContainer CreatePermanentChildContainer(this SimpleContainer container)
{
	return new SimpleContainer { ParentContainer = container };
}

public static SimpleContainer CreateChildContainer(this SimpleContainer container)
{
	var child = container.CreatePermanentChildContainer();
	container.Disposing += (sender, eventArgs) => child.Dispose();
	return child;
}

We can use these extension method to implement very handy method:

public static void RegisterWithOverride<T>(this SimpleContainer container, string key, Func<SimpleContainer, Type, string, object> creator, Action<SimpleContainer> overrideContainer)
{
	container.Register<T>(key, (c, t, k) =>
	{
		using (var cc = container.CreatePermanentChildContainer())
		{
			overrideContainer(cc);
			return creator(cc, t, k);
		}
	});
}

Our new method has one more parameter – overrideContainer that is aimed for registration overrides.
And finally, we can use this pretty cool extension method:

// most of classes will use ServiceOne for IMyService
container.Register<IMyService, ServiceOne>();
// but IMyInterface will use ServiceTwo for IMyService
container.RegisterWithOverride<IMyInterface, MyInterfaceImplementation>(cc =>
	{
		cc.Register<IMyService, ServiceTwo>();
	});

Summary

As you can see, implementation of own lightweight IoC/DI can be very easy. It could be probably better to use some good known IoC/DI container in large enterprise application but if your requirements aren’t very high, you don’t need registration via external configuration file (so registration in code is sufficient) and you want to have good performance then own lightweight IoC/DI container can be good solution.
I created project SimpleContainer on CodePlex and so you can download source codes here. You can let me know if you (don’t) like that and/or use that.
I implemented extensions method for registering instance for one web-request too. It uses own implementation of IHttpModule so if you want to use this feature then you have to register this module in your web.config.
Footnote: I discovered that there are some light IoC/DI containers for .NET: Funq and Munq. But these containers are much more complicated – remember that SimpleContainer core is just one class and the second class contains set of extension methods only.


Příspěvek byl publikován v rubrice Programování a jeho autorem je Augi. Můžete si jeho odkaz uložit mezi své oblíbené záložky nebo ho sdílet s přáteli.

2 komentáře u „Really simple IoC/DI container implementation

  1. Stručně řečeno – vede k lepšímu návrhu aplikace. Pěkně je to rozebrané třeba zde.

    Člověk díky použití IoC/DI píše komponenty více nezávisle, takže kód je pak lépe unit-testovatelný, přehlednější, flexibilnější, znovupoužitelný a vůbec méně “prodrátovaný”.

    De facto člověk vyvíjí jen sadu nezávislých komponent, které o sobě navzájem nevědí. Až hlavní aplikace pomocí konfigurace kontejneru komponenty prodrátuje a složí do funkčního celku.

Napsat komentář

Vaše emailová adresa nebude zveřejněna.