What’s wrong with the ASP.NET Core DI abstraction?
# Jun 30, 2016 by Steven and PeterFor the last couple of years Microsoft has been building the latest version of the .NET platform: .NET Core. .NET Core is a complete redesign of .NET, with the goals of being truly cross-platform and cloud friendly. We’ve been following the development of .NET Core closely and have released .NET Core compatible versions of Simple Injector since RC1. With the release of Simple Injector v3.2 we now officially support .NET Core.
As you may be aware Microsoft has added its own DI library as one of its core components. Some would yell “finally!” The omission of such a component has spawned many open source DI libraries for .NET. Simple Injector obviously being one of them.
Don’t get us wrong here, we applaud Microsoft for promoting DI as a core practice in .NET and it will likely lead to many more developers practicing DI, which is a win for our industry. The problem, however, starts with the abstraction Microsoft has defined on top of their built-in DI container. Compared to the previous Resolve abstractions, such as IDependencyResolver and IServiceProvider, this new abstraction adds a Register API on top IServiceCollection. With the definition of this abstraction, it is Microsoft’s vision that other (more feature rich) DI libraries could plug-in into the platform, while application developers, third-party tool builders, and framework developers use the standardized abstraction to add their registrations. This would allow application developers a standard for integrating their DI library of choice.
At first sight having an abstraction might seem like sound advice—a common saying in our industry is that there are few problems in software that can’t be solved by adding a (extra) layer of abstraction. In this instance though their reasoning is flawed. DI experts have been warning Microsoft about this problem from the beginning, without success. Mark Seemann quite accurately described the problems with this approach in general here, where IMO the main points of his reasoning are:
- It pulls in the direction of the lowest common denominator
- It stifles innovation
- It makes it more difficult to avoid using a DI container
- It introduces versioning hell
- If adapters are supplied by contributors, the adapters may have varying quality levels, and may not support the latest version of the Conforming Container.
These are real issues we are facing today with the new .NET Core DI abstraction. DI containers often have very unique and incompatible features when it comes to their registration API. Simple Injector, for instance, is very carefully designed in a way that enables the identification of numerous configuration errors. One very prominent example—but there are many more—is Simple Injector’s diagnostic abilities. This is one of the features that is fundamentally incompatible with the expectations that consumers of the DI abstraction will have. So what are the expectations consumers will have of the new abstraction?
Consumers of the DI abstraction can be divided into three groups. Framework components, third-party libraries, and application developers; especially framework components and third-party libraries, which are now expected to add their own registrations through the common abstraction. Because it is nigh on impossible for these two groups of developers to test their code with all the available adapters, they will test their code solely with the built-in container. And while using the built-in container, these developers will (and arguably should) implicitly expect the standardized behaviour of the built-in container—no matter which adapter is used. In other words, it is the built-in container that defines both the contract and the behaviour of the abstraction. Every implemented adapter must be an exact superset of the built-in container. Deviating from the norm is not allowed because it would break third-party tools that depend on the behaviour of the default, built-in container.
Simple Injector’s diagnostic and verification abilities is one of the many features that make Simple Injector users extremely productive. It detects problems that would be detected much later in the development cycle when using a different DI library. But running the diagnostics on both application and third-party registrations will cause problems because it is unlikely that all the external parties will automatically “play nice” with Simple Injector’s diagnostics. There is every chance they will define registrations that Simple Injector finds suspicious even though they have (hopefully) tested the registrations are fine for their specific case with the default container. It would be impossible for a hypothetical adapter for Simple Injector to distinguish between third-party registrations and application registrations.
Switching off diagnostics completely would remove one of Simple Injector’s most important safety nets, whilst leaving the diagnostics system in place would likely cause false-positives from the third-party tooling that would each need to be suppressed by application developers. As these third-party registrations are mostly hidden to the application developer, working around these issues could be daunting, frustrating and sometimes even impossible. One might argue that it would be good for Simple Injector to detect problems with third-party tools, but contacting those same tool developers to explain the “problem” would probably lead to fingers being pointed at us, because we “obviously” provided the user with an “incompatible” adapter.
Simple Injector’s diagnostic abilities is just one of the many incompatibilities that we would face when writing an adapter for .NET Core’s DI abstraction. Other incompatibilities include:
- The way Simple Injector explicitly separates the registration of collections from one-to-one mappings
- How Simple Injector handles open-generic registrations; they are not treated as fall-back registrations as they are by the built-in container
- How Simple Injector handles scoping—Scopes in Simple Injector are ambient while .NET Core forces an ambient-less scoping model (i.e. a Closure Composition Model).
Making a fully compatible adapter for Simple Injector requires removing many prominent features, and thereby changing the existing behaviour of the Simple Injector library to something that would violate the guiding principles that underpin our vision. This is not an attractive solution. Not only would it introduce major breaking changes, it would remove features and behaviours that make Simple Injector unique and it is this complete set of features that many developers love about Simple Injector. In this sense having an adapter “stifles innovation” as Mark Seemann describes.
With Simple Injector we made many innovations and not only would the adapter make Simple Injector almost useless to our users, it would restrict us from future improvement and innovation. Some might view Simple Injector’s philosophy as radical, but we think otherwise—we designed Simple Injector in a way that we think serves our users best. And the NuGet download count on the Simple Injector package indicates that many developers agree with us. Conforming to the defined Register API would prevent us from serving our users.
Although Simple Injector’s view may diverge from the norm more than most other containers, the simple act of defining this common abstraction blocks future DI libraries with an even more radical or innovative viewpoint from being used at all—it stifles innovation for future libraries. Just imagine one of the other containers introducing the same kind of verification that Simple Injector provides? Such feature can’t be introduced without breaking the contract of the DI abstraction. The mere act of having such an adapter blocks progress in our industry.
With this explanation, we hope we’ve also made it clear that Microsoft’s DI abstraction isn’t even the lowest common denominator, because the lowest common denominator implies compatibility with all DI libraries! As we expressed here the chances are that none of the existing DI libraries are fully compatible with the defined abstraction. The Autofac maintainers for instance, realized they have some quite severe incompatibility issues and eventually came to the same conclusion as we did. The Autofac maintainers publically stated that their adapter is not 100% compatible with Microsoft’s DI abstraction:
there will definitely be behavior differences between the Autofac DI container and the Microsoft DI container. We’re not trying to behave identically – if you choose to use Autofac as your backing container, you’re also choosing the Autofac behaviors. The difference in behavior you found is one of probably many
But while application developers do explicitly choose to use a particular container, like Autofac, framework and third-party library developers don’t. And when those latter developers depend on abstraction behavior that Autofac implements differently, it can break the application in very subtle ways.
Considering that Microsoft’s DI Container is heavily influenced by Autofac’s design, it is a telling sign that even Autofac can’t comply with the abstraction.
A similar story comes from the maintainer of StructureMap that stated:
ASP.Net Core DI compliance has been a huge pain in the ass to the point where I’ve openly wondered if the ASP.Net team has purposely tried to sabotage and wipe out all the existing ecosystem of IoC containers
UPDATE: Other maintainers of DI containers are also starting to notice how the new DI abstraction is stifling innovation. The developer of the LightInject DI Container, for instance, had to completely disable one of its library’s compelling features to allow his adapter to be used in a vanilla ASP.NET Core v2.2 application, to prevent it from completely crashing at startup.
UPDATE (2019-09): Due to similar incompatibilities with the built-in container, the Castle Windsor maintainers were forced to take a similar integration approach to ours, which we describe in our next blog post. In other words, Castle Windsor’s integration works around the ASP.NET Core DI abstraction as well.
This wouldn’t be so bad if Microsoft’s DI library was a feature-rich implementation that contained features like Simple Injector’s verification and diagnostic services so that we all use the same fully featured DI library. Sadly, the implementation is far from feature rich, Microsoft itself has described their implementation as a
minimalistic DI container [that] is useful in the cases when you don’t need any advanced injection capabilities
To make matters worse, since the built-in container defines the contract of the abstraction, adding new features to the built-in container will break all existing adapters! Third-party developers who use the abstraction will only test with the built-in container and when their libraries depend on a feature added to the built-in container that is not yet supported by an adapter, things will fail and the application developer is screwed. This is one aspect of the versioning hell that Mark Seemann discusses in his blog post. Not only is their current implementation “minimalistic,” it can never evolve to a feature rich, completely usable DI container, because they’ve painted themselves in a corner: every future change is breaking change that will piss everyone off.
UPDATE (2023-07): This is exactly what happened with the introduction of .NET 8, where Microsoft introduces keyed registrations to their DI Container. This —once more— frustrates maintainers of DI Container adapters, that now all have you upgrade their integration packages, again.
A better solution is to avoid using the abstraction and its adapters entirely. As Mark Seemann quite accurately explained here and here, reusable libraries and frameworks may not need to use a DI container at all.
Unfortunately, the mere act of defining an abstraction will make it much harder to avoid using it. By defining an abstraction and actively promoting its use, Microsoft is leading thousands of third-party library developers and framework developers to stop thinking about defining the right library and the right framework abstractions (Mark’s articles clearly describes this). They no longer think about this because Microsoft leads them to believe that the whole world needs one common abstraction. We have seen new factory interfaces for MVC appear very late in the game (such as the IViewComponentActivator
abstraction prior to RC2). And if we see the MVC team make these kinds of mistakes till very late in the development cycle, what can we expect from all those developers who are starting to build on top of the new .NET platform?
UPDATE: More than three years later, a similar issue popped up with the new ASP.NET Core 3 Razor Components, where Microsoft forgot to introduce an IComponentActivator
abstraction. Although the issue was reported six months before ASP.NET Core 3 was released, Microsoft decided not to add this abstraction, making it impossible for users of containers like Simple Injector and Castle Windsor to integrate with Razor Components. One would have hoped that Microsoft would keep its promise to add the “appropriate composition roots” (read: the “required abstractions”). This unfortunately proves that even framework developers stopped thinking about defining the right framework abstractions.
Conclusion
The definition of a DI abstraction is a painful mistake by Microsoft that will haunt us for many years to come. It has already stifled innovation, has introduced versioning hell, and frustrates many developers. The abstraction is incompatible with many, if not all, DI libraries and, against expert advice, Microsoft chose to retain the abstraction, dividing the world into incompatible and partially compatible containers, leading to endless issue reports for the adapter libraries that implement the DI abstraction and third-party libraries that use the abstraction.
Our view is that, as an application developer, you should refrain from using an adapter and in the next article we will explain more thoroughly how to approach this and why, even with a compatible container, it is the smarter way forward.
Stay tuned