A few months ago, when ASP.NET Core was still in RC1, I made the first awkward attempts to transfer my test project from MVC 5 to ASP.NET Core. At that time, the project had already used the Simple Injector IOC library, and for this reason I wanted to continue using this library, since there was support from rc1. I was following the release of new versions of this library and relatively recently I came across a rather interesting, in my opinion, article posted on the Simple Injector themed blog. Although the article relies on the corresponding library, its main value is in raising a more general problem - the new DI abstraction in ASP.NET Core.
An article from the IOC Blog Library Simple Injector
By Steve
I would be glad if you point out errors and inaccuracies in the translation.For the past few years, Microsoft has been developing a new version of the platform. NET: .NET Core. .NET Core is a complete redesign of the existing .NET platform aimed at true cross-platform and cloud compatibility. We closely followed the development of .NET Core and released platform-compatible versions of Simple Injector, starting with RC1. With the release of Simple Injector v3.2, we officially support .NET Core.
')
As you can see, Microsoft has added its own DI library as one of the main components of the framework. Someone may exclaim "finally!". The absence of such a component has generated many open source DI libraries for .NET. And Simple Injector is obviously one of them.
Don't get me wrong, we are grateful to Microsoft for promoting DI as the main practice in .NET - this will probably lead to the emergence of even more developers practicing DI, which in turn will have a positive impact on our industry. The problem, however, begins with the abstraction that Microsoft has identified on top of its embedded DI container. Compared to previous
Resolve abstractions (
IDependencyResolver and
IServiceProvider ), the new abstraction adds the
Register API on top of the
IServiceCollection . The essence of this abstraction for Microsoft is that other (more functionally rich) DI libraries can connect to the platform, while application developers, third-party tools and frameworks use a standardized abstraction to register dependencies. This gives application developers a standard for integrating DI libraries of their choice.
At first glance it may seem that having such an abstraction is a good idea. Generally speaking, there are few problems in our software industry that cannot be solved by adding (additional) levels of abstraction. Although in this case, the reasoning of Microsoft is erroneous. DI experts warned them about this problem from the very beginning, but to no avail.
Mark Seemann quite accurately described the problems with this approach in general
here , where, in my opinion, the following key points can be highlighted:
- This approach draws in the direction of the lowest common denominator.
- This approach suppresses innovation.
- This approach adds versioning hell.
- It becomes harder to work without using a DI container.
- If the adapters are developed by members of the open-source community, these adapters may have a different quality level and may not be compatible with the latest version of the Conforming Container .
These are the real problems facing us today in the new DI abstraction in .NET Core. DI containers often have very unique and incompatible features when it comes to their
registration API . Simple Injector, for example, is very carefully designed to detect multiple configuration errors. One of the most prominent examples (and there are much more) is
his diagnostic abilities . This is one of the features that are fundamentally incompatible with the expectations that users of DI abstraction will have. And what will users expect from the new abstraction?
DI abstraction users can be divided into three groups: developers of frameworks, external libraries, and the applications themselves; especially developers of frameworks and external libraries, which are now thinking of adding registration of their dependencies through a common abstraction. Since it is almost impossible for these two groups of developers to test their code with all available adapters, they will test their code using the built-in container. And while these developers use the built-in container, they will (and probably should) implicitly expect standard behavior from the built-in container - no matter which adapter is used. In other words, this built-in container defines both the contract and the behavior of the abstraction. Each adapter created must be an exact superset of the built-in container. Deviation from the norm is not allowed, as it would disrupt the work of external libraries, which depend on the default behavior of the built-in container.
Diagnostics and verification in Simple Injector are one of the many possibilities that allow to develop much more productively. They allow you to find problems that could be discovered much later in the development process if you used other DI libraries. But running diagnostics and applications and third-party components will cause problems — it is very unlikely that third-party components will automatically “play by the rules” with the diagnostics of Simple Injector. Chances are that they will register the dependencies in such a way that Simple Injector will consider them suspicious, even if they (hopefully) have tested the registration well in special cases with a standard container. The hypothetical adapter for Simple Injector would be impossible to distinguish between the registration of third-party dependencies and application dependencies. Turning off the diagnostics will completely remove one of the most important safety mechanisms, while maintaining the diagnostics will lead to false positives from third-party components, and these false positives will have to be suppressed by application developers. Since registration of third-party components is mostly hidden from application developers, working with all these issues can be difficult, frustrating and sometimes even impossible. It can be argued - well, that Simple Injector finds problems with third-party tools. But if you want to contact the developers of third-party libraries and try to explain the “problem” to them, then they will probably turn the arrows on us, because it’s “obvious” that we developed an “incompatible” adapter.
Simple Injector's diagnostic capabilities are one of the many incompatibilities we encountered when writing an adapter for .NET Core DI abstraction. Other incompatibilities:
To make a fully compatible adapter for Simple Injector, you need to remove many of the known capabilities of the framework, thereby changing the existing behavior of the library and turning it into something that violates the principles that guided us in the design. Unattractive solution. Not only does it lead to the appearance of breaking compatibility changes, but the opportunities and behaviors that Simple Injector was loved by the developers also disappear. In this sense, having an adapter is a
“choking innovation,” as
Mark described. In Simple Injector, we have made a lot of innovation, and the adapter will make the Simple Injector almost useless for its users. The adapter will also limit us from making further improvements and innovations. Someone might think the philosophy of Simple Injector is radical, but we think differently. We designed it in a way that we feel is best for our users. And the
number of downloads of the NuGet package indicates that many developers agree with us. Compliance with a specific adapter will prevent us from continuing to meet the needs of our users.
Although the Simple Injector vision may deviate from the norm more than most other containers, the very definition of a common abstraction for future DI libraries is an even more radical or innovative point of view that
“stifles the innovations” of future libraries. Just imagine one of the other containers implementing the same checks that Simple Injector provides? Such a feature cannot be introduced without violating the DI abstraction contract. The very fact of having such an adapter can block the progress in our industry.
This explanation, I hope, also clarified that the Microsoft DI abstraction is not even the “lowest common denominator”, because “the lowest common denominator” implies compatibility with all DI libraries. As I said
here , there is a chance that none of the existing DI libraries are fully compatible with this abstraction. This partly boils down to the fact that, although the embedded container defines an abstraction contract, the project with tests of this abstraction lacks a solid number of test cases that would completely determine the exact behavior in all scenarios. Until now, all adapter implementations have been an attempt to guess and hope for the best - that the adapter implementation is almost synchronized with the behavior of the built-in container. Autofac developers, for example, have
just realized that they have quite serious compatibility problems and as a result
came to the same conclusions as
we .
It would not be so bad if the DI Microsoft library were rich in features and contained functions such as verification and diagnostics from Simple Injector. Then we could all use the same full-featured DI library. Unfortunately, the implementation is far from being functionally rich, and Microsoft itself
described their implementation as
Minimalistic DI container, useful when you don’t need any extra injection features.Worse, since a built-in container defines an abstraction contract, adding new features to the built-in container will break all existing adapters! Any third-party developer using abstraction will test (his library) only with the built-in library (.NET Core's DI). And when a third-party library begins to depend on some function added to the built-in container, and which is not yet supported by the adapter, something will break and the application developer will suffer. This is one aspect of versioning hell that
Mark Seemann discusses on his blog. Let's hope that at least Microsoft will increase the major version number each time they make changes. Not only is their current implementation
"minimalistic" , it can never evolve into a fully usable multi-functional DI container, because they have driven themselves into a corner: every future change is a change that breaks compatibility, from which everyone will be ill.
The best solution is to avoid using abstraction and its adapters completely. As
Mark Seemann explained quite accurately
here and
here , libraries and frameworks may not need to use a DI container at all. Unfortunately, the very fact of defining abstraction makes it very difficult to avoid using it. Defining abstraction and actively promoting its use, Microsoft leads thousands of third-party developers of libraries and frameworks to stop thinking about determining the correct abstraction for the library and framework (articles by
Mark Seemann clearly describe this). Developers no longer think about it, because Microsoft makes them believe that the whole world needs one common abstraction. We have already seen how new factory interfaces for MVC came into play very late (for example, as an
IViewComponentActivator abstraction before the beginning of RC2). And if we see that the MVC team brings such errors to such a late stage of the development cycle, what can we expect from all those developers who are starting to develop on the new .NET platform?
Conclusion
Defining a DI abstraction is a painful Microsoft bug that will haunt us for many years to come. It already stifles innovation, generates versioning, and upsets many developers. Abstraction is incompatible with many DI libraries and, contrary to the recommendations of experts, Microsoft decided to save it, dividing the world into incompatible and partially compatible containers, which leads to endless reports of adapter problems that implement DI abstraction and third-party libraries that use this abstraction.
In our opinion, as an application developer, you should refrain from using an adapter and in the
next article I will describe in more detail how to approach this and why, even without an incompatible container, this is a reliable way forward.
Keep in touch.