The state of IOC containers in ASP.NET Core

Posted at 09:00 on 15 October 2018

One of the first things that I had to do at my new job was to research the IOC container landscape for ASP.NET Core. Up to now we've been using the built-in container, but it's turned out to be pretty limited in what it can do, so I've spent some time looking into the alternatives.

There is no shortage of IOC containers in the .NET world, some of them with a history stretching as far back as 2004. But with the arrival of .NET Core, Microsoft has now made dependency injection a core competency baked right into the heart of the framework, with an official abstraction layer to allow you to slide in whichever one you prefer.

This is good news for application developers. It is even better news for developers of libraries and NuGet packages, as they can now plug straight into whatever container their consumer uses, and no longer have to either do dependency injection by hand or to include their own copies of TinyIOC. But for developers of existing containers, it has caused a lot of headaches. And this means that not all IOC containers are created equal.

Conforming Containers in .NET Core

Originally, the .NET framework provided just a simple abstraction layer for IOC containers to implement: the IServiceProvider interface. This consisted of a single method, GetService(Type t). As such, all an IOC container was expected to do was to return a specific service type, and let the consumer do with it what it liked.

But there's a whole lot more to dependency injection than just returning a service that you're asked for. IOC containers also have to register the types to be resolved, and then -- if required to do so -- to manage their lifecycles, calling .Dispose() on any IDisposable instances at the appropriate time. When you add in the possibility of nested scopes and custom lifecycles, it quickly becomes clear that there's much more to it than just resolving services.

And herein lies the problem. For with the introduction of Microsoft.Extensions.DependencyInjection and its abstractions, Microsoft now expects containers to provide a common interface to handle registration and lifecycle management as well.

This kind of abstraction is called a Conforming Container. The specification that conforming containers have to follow is defined in a set of more than fifty specification tests in the ASP.NET Dependency Injection repository on GitHub. It includes such requirements as:

  • When you register multiple services for a given type, when you request one, the one that you get back has to be the last one registered.
  • When you request all of them, they have to be returned in the order that they were registered.
  • When a container is disposed, it has to dispose services in the reverse order to that in which they were created.
  • There are also rules around which constructor to choose, registration of open generics, requesting types that haven't been registered, resolving types lazily (Func<TService> or Lazy<TService>) and a whole lot more.

These specification tests are also available as a NuGet package.

There are two points worth noting here. First, conforming containers MUST pass these tests otherwise they will break ASP.NET Core or third party libraries. Secondly, some of these requirements simply cannot be catered for in an abstraction layer around your IOC container of choice. If a container disposes services in the wrong order, for example, there is nothing you can do about it. Cases such as these require fundamental and often complex changes to how your container works that in some cases might be breaking changes.

For what it's worth, this is a salutary lesson for anyone who believes that they can make their data access layer swappable simply by wrapping it in an IRepository<T> and multiple sets of models. Data access layers are far more complicated than IOC containers, and the differences between containers are small change compared to what you'll need to cater for if you want to swap out your DAL. As for making entire frameworks swappable, I'm sorry Uncle Bob, but you're simply living in la-la land there.

All containers are equal, but some are more equal than others

So should we just stick with the default container? While many developers will, that is not Microsoft's intention. The built in container was explicitly made as simple as possible and is severely lacking in useful features. It can not resolve unregistered concrete instances, for example. Nor does it implicitly register Func<T> or Lazy<T> (though the latter can be explicitly registered as an open generic). Nor does it have any form of validation or convention-based registration. It is quite clear that they want us to swap it out for an alternative implementation of our choice.

However, this is easier said than done. Not all IOC containers have managed to produce an adapter that conforms to Microsoft's specifications. Those that have, have experienced a lot of pain in doing so, and in some cases have said that there will be behavioral differences that won't be resolved.

For example, the authors of SimpleInjector have said that some of their most innovative features -- specifically, those that support strong, early validation of your registrations -- are simply not compatible with Microsoft's abstractions. Travis Illig, one of the authors of Autofac, noted that some of the problems he faced were incredibly complex. Several commenters on the ASP.NET Dependency Injection GitHub repo expressed concerns that the abstraction is fragile with a very high risk that any changes will be breaking ones.

There are also concerns that third party library developers might only test against the default implementation and that subtle differences between containers, which are not covered by the specification, may end up causing problems. Additionally, there is a concern that by mandating a standard set of functionality that all containers MUST implement, Microsoft might be stifling innovation, by making it hard (or even impossible) to implement features that nobody else had thought of yet.

But whether we like it or not, that is what Microsoft has decided, and that is what ASP.NET Core expects.

Build a better container?

So what is one to do? While these issues are certainly a massive headache for authors of existing IOC containers, it remains to be seen whether they are an issue for authors of new containers, written from scratch to implement the Microsoft specification from the ground up.

This is the option adopted by Jeremy Miller, the author of StructureMap. He recently released a new IOC container called Lamar, which, while it offers a similar API to StructureMap's, has been rebuilt under the covers from the ground up, with the explicit goal of conforming to Microsoft's specification out of the box.

Undoubtedly, there will be other new .NET IOC containers coming on the scene that adopt a similar approach. In fact, I think this is probably a good way forward, because it will allow for a second generation of containers that have learned the lessons of the past fifteen years and are less encumbered with cruft from the past.

Whether or not the concerns expressed by authors of existing containers will also prove to be a problem for authors of new containers remains to be seen. I personally think that in these cases, the concerns may be somewhat overblown, but whether or not that turns out to be the case remains to be seen. It will be interesting to see what comes out in the wash.