- 4 May 2019: Grace now passes all Microsoft’s tests as of version 7.0.0. Updated other containers to the latest versions.
Since my last post on the state of IOC containers in .NET Core, I’ve ended up going down a bit of a rabbit hole with this particular topic. It occurred to me that since Microsoft has come up with a standard set of abstractions, it is probably best, when choosing a container, to pick one that conforms to these abstractions. After all, That Is How Microsoft Wants You To Do It.
But if you want to do that, what are your options? Which containers conform to Microsoft’s specifications? I decided to spend an evening researching this to see if I could find out.
Rather helpfully, there’s a fairly comprehensive list of IOC containers and similar beasties maintained by Daniel Palme, a .NET consultant from Germany, who regularly tests the various options for performance. He currently has thirty-five of them on his list. With this in mind, it was just an evening’s work to go down the list and see where they all stand.
I looked for two things from each container. First of all, it needs to either implement the Microsoft abstractions directly, or else provide an adapter package on NuGet that does. Secondly, it needs to pass the specification tests in the
In the end of the day, I was able to find adapters on NuGet for twelve of the containers on Daniel’s list. Seven of them passed all seventy-three test cases; five failed between one and four of them. They were as follows:
|AutoFac 4.9.2||AutoFac.Extensions.DependencyInjection 4.4.0||All passed|
|Castle Windsor 5.0.0||Castle.Windsor.MsDependencyInjection 3.3.1||All passed|
|DryIoc 4.0.4||DryIoc.Microsoft.DependencyInjection 3.0.3||All passed|
|Grace 7.0.0||Grace.DependencyInjection.Extensions 7.0.0||All passed|
|Lamar 3.0.2||Lamar 3.0.2||2 failed|
|LightInject 5.4.0||LightInject.Microsoft.DependencyInjection 2.2.0||4 failed|
|Maestro 3.5.0||Maestro.Microsoft.Dependencyinjection 2.1.2||4 failed|
|Microsoft.Extensions.DependencyInjection 2.2.0||All passed|
|Rezolver 1.4.0||Rezolver.Microsoft.Extensions.DependencyInjection 2.2.0||All passed|
|Stashbox 2.7.3||Stashbox.Extensions.Dependencyinjection 2.6.8||All passed|
|StructureMap 4.7.1||StructureMap.Microsoft.DependencyInjection 2.0.0||2 failed|
|Unity 5.10.3||Unity.Microsoft.DependencyInjection 5.10.2||All passed|
Which tests failed?
It’s instructive to see which tests failed. All but one of the failing tests failed for more than one container.
ResolvesMixedOpenClosedGenericsAsEnumerable. This requires that when you register an open generic type (for example, with
svc.AddSingleton(typeof(IRepository<>), typeof(Repository<>))) and a closed generic type (for example,
IRepository<User>), a request for
IEnumerable<IRepository<User>>should return both, and not just one. Lamar and StructureMap all failed this test.
DisposesInReverseOrderOfCreation. Does what it says on the tin: last in, first out. Lamar, Maestro and StructureMap fail this test.
LastServiceReplacesPreviousServicestests that when you register the same service multiple times and request a single instance (as opposed to a collection), the last registration takes precedence over the previous registrations. LightInject fails this test.
ResolvesDifferentInstancesForServiceWhenResolvingEnumerablechecks that when you register the same service multiple times, you get back as many different instances of it as you registered. LightInject fails three of the test cases here; Maestro fails two.
DisposingScopeDisposesServicechecks that when a container is disposed, all the services that it is tracking are also disposed. Maestro fails this test — most likely for transient lifecycles, because different containers have different ideas here about what a transient lifecycle is supposed to mean with respect to this criterion.
These failing tests aren’t all that surprising. They generally concern more complex and esoteric aspects of IOC container functionality, where different containers have historically had different ideas about what the correct behaviour should be. They are also likely to be especially difficult for existing containers to implement in a backwards-compatible manner.
Nevertheless, these are still tests that are specified by Microsoft’s standards, and furthermore, they may cause memory leaks or incorrect behaviour if ASP.NET MVC or third party libraries incorrectly assume that your container passes them. This being the case, if you choose one of these containers, make sure you are aware of these failing tests, and consider carefully whether they are ones that are likely to cause problems for you.
The most surprising result here was Lamar. Lamar is the succesor to StructureMap, which is now riding off into the sunset. It was also written by Jeremy Miller, who has said that two of his design goals were to be fully compliant with Microsoft’s specification from the word go, while at the same time having a clean reboot to get rid of a whole lot of legacy baggage that StructureMap had accumulated over the years and that he was sick of supporting. It is also the only container in the list that supports the DI abstractions in the core assembly; the others all rely on additional assemblies with varying amounts of extra complexity. However, the two failing tests in Lamar were exactly the same as the failing tests in StructureMap, so clearly there has been enough code re-use going on to make things difficult. Furthermore, the tests in question represent fairly obscure and low-impact use cases that are unlikely to be a factor in most codebases.
Most of the IOC containers on Daniel’s list for which I couldn’t find adapters are either fairly obscure ones (e.g. Cauldron, FFastInjector, HaveBox, Munq), dead (e.g. MEF), or not actually general purpose IOC containers at all (e.g. Caliburn Micro). There were, however one or two glaring omissions.
Probably the most prominent one was Ninject. Ninject was the first IOC container I ever used, when I was first learning about dependency injection about ten years ago, and it is one of the most popular containers in the .NET community. Yet try as I might, I simply have not been able to find a Ninject adapter for the .NET Core abstractions anywhere. If anyone knows of one, please leave a note in the comments below and I’ll update this post accordingly.
Having said that, it isn’t all that surprising, because Ninject does have some rather odd design decisions that might prove to be a stumbling block to implementing Microsoft’s specifications. For example, it eschews nested scopes in favour of tracking lifecycles by watching for objects to be garbage collected. Yes, seriously.
Another popular container that doesn’t have an adapter is Simple Injector. This is hardly surprising, though, because Simple Injector has many design principles that are simply not compatible with Microsoft’s abstraction layer. The Simple Injector authors recommend that their users should leave Microsoft’s built in IOC container to handle framework code, and use SimpleInjector as a separate container for their own application code. If SimpleInjector is your personal choice here, this is probably a good approach to consider.
Finally, there doesn’t seem to be an adapter for TinyIOC, which is not on Daniel’s list. However, since TinyIOC is primarily intended to be embedded in NuGet packages rather than being used as a standalone container, this is not really surprising either.
Some final observations
I would personally recommend — and certainly, this is likely to be my practice going forward — choosing one of the containers that implements the Microsoft abstractions, and using those abstractions to configure your container as far as it is sensible to do so. Besides making it relatively easy to swap out your container for another if need be (not that you should plan to do so), the Microsoft abstractions introduce a standard vocabulary and a standard set of assumptions to use when talking about dependency injection in .NET projects.
However, I would strongly recommend against restricting yourself to the Microsoft abstractions like glue. Most IOC containers offer significant added value, such as convention-based registration, lazy injection (
Lazy<T>), interception, custom lifecycles, or more advanced forms of generic resolution. By all means make full use of these whenever it makes sense to do so.
For anyone who wants to tinker with the tests (or alert me to containers that I may have missed), the code is on GitHub.