What is wrong with ASP.NET Core DI abstraction?

A few months ago, when ASP.NET Core was still in RC1, I made the first awkward attempts to migrate my test project from MVC 5 to ASP.NET Core. At that time, the IOC Simple Injector library was already used in the project, and for this reason I wanted to continue using this library, since there was support with rc1. I watched the release of new versions of this library and relatively recently I came across a rather interesting, in my opinion, article posted on the thematic blog Simple Injector. Although the article relies on the appropriate library, its main value is to raise a more general problem - the new DI abstraction in ASP.NET Core. Simple Injector Library IOC Blog

Article
By Steve
I would be glad if you point out the errors and inaccuracies in the translation.


Over 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 compatibility and compatibility with cloud technologies. We closely monitored 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 may have noticed, Microsoft has added its own DI library as one of the core components of the framework. Someone may exclaim “finally!”. The absence of such a component gave rise to many open source DI libraries for .NET. And Simple Injector is obviously one of them.

Do not get me wrong, we are grateful to Microsoft for promoting DI as the main practice in .NET - this will probably lead to the appearance of even more developers practicing DI, which in turn will positively affect our industry. The problem, however, begins with the abstraction that Microsoft defined on top of its built-in DI container. Compared to previous Resolve abstractions ( IDependencyResolver and IServiceProvider ), the new abstraction adds Register API on top of IServiceCollection. The essence of this abstraction for Microsoft is that other (more functionally rich) DI libraries can connect to the platform, while developers of applications, 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 might 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, Microsoft's reasoning 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 as a wholehere , where, in my opinion, you can highlight the following key points:
  • This approach pulls towards 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 development of adapters will be handled by members of the open-source community, these adapters may have a different level of quality and may not be compatible with the latest version of the Conforming Container (approx. Referring to the template described here )

These are the real challenges we face 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 numerous configuration errors. One of the most striking examples (and there are many more) are his diagnostic abilities . This is one of the features that is fundamentally incompatible with the expectations that users of DI abstraction will have. But what will users expect from a 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, who are now thinking about adding registration of their dependencies through a common abstraction. Since for these two groups of developers it is almost impossible to verify 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 inline container defines both the contract and the abstraction behavior. Each adapter created must be an exact superset of the built-in container. Deviation from the norm is not allowed,

Diagnostics and verification in Simple Injector are some of the many features that allow you to conduct development 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 Simple Injector diagnostics. It is likely that they will register dependencies in such a way that Simple Injector will consider them suspicious, even if they (hopefully) tested the registration well in special cases with a standard container. It would be impossible for a hypothetical adapter for Simple Injector to distinguish between registrations of third-party dependencies and application dependencies. Disabling diagnostics will completely remove one of the most important safety mechanisms, while saving 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 for the most part is hidden from application developers, working with all these issues can be difficult, disappointing 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 arrows on us, because it is “obvious” that we have developed an “incompatible” adapter. while saving 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 for the most part is hidden from application developers, working with all these issues can be difficult, disappointing 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 arrows on us, because it is “obvious” that we have developed an “incompatible” adapter. while saving 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 for the most part is hidden from application developers, working with all these issues can be difficult, disappointing 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 arrows on us, because it is “obvious” that we have developed an “incompatible” adapter. Since registration of third-party components for the most part is hidden from application developers, working with all these issues can be difficult, disappointing 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 arrows on us, because it is “obvious” that we have developed an “incompatible” adapter. Since registration of third-party components for the most part is hidden from application developers, working with all these issues can be difficult, disappointing 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 arrows on us, because it is “obvious” that we have developed an “incompatible” adapter.

Diagnostic abilities in Simple Injector are one of the many incompatibilities we encountered when writing the adapter for the .NET Core DI abstraction. Other incompatibilities:

To make a fully compatible adapter for Simple Injector, you will need to remove many well-known features 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 development. Unattractive solution. Not only will it lead to the appearance of changes breaking compatibility, but also the opportunities and behavior for which Simple Injector and the developers liked it will disappear. In this sense, having an adapter is “stifling innovation,” as Mark described. At Simple Injector, we have made many innovations, and the adapter will make Simple Injector practically useless for its users. The adapter will also limit us from making further improvements and innovations. Some might consider the philosophy of Simple Injector to be radical, but we think differently. We designed it in a way that we believe is best suited for our users. And the number of downloads of the NuGet package indicates that many developers agree with us. Compliance with a particular adapter will prevent us from further satisfying the needs of our users.

Although the vision of Simple Injector 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 innovation” 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 contract of DI abstraction. The mere fact of having such an adapter can block progress in our industry.

With this explanation, I hopefully also made it clear that 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 is fully compatible with this abstraction. In part, this boils down to the fact that, although the built-in container defines an abstraction contract, a project with tests of this abstraction lacks a substantial number of test cases that would fully 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, just realized that they have quite serious compatibility issues and eventually came to the same conclusions as us .

It wouldn’t be so bad if the Microsoft DI library was rich in features and included features 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 so functionally rich, and Microsoft herself described their implementation as a

Minimalistic DI container, useful in cases where you do not need any additional injection capabilities.

Even worse, since the inline container defines the abstraction contract, adding new features to the inline container will break all existing adapters! Any third-party developer using abstraction will only test (their library) using the built-in library (.NET Core's DI). And when the library of a third-party developer begins to depend on some function added to the built-in container, which is not yet supported by the adapter, something will break and the application developer will suffer. This is one aspect of the versioning hell Mark Seemanndiscusses on his blog. Hopefully, at least Microsoft will increase the major version number each time they make changes. Not only is their current implementation “minimalistic” , it will never be able to evolve into a fully usable multi-functional DI container, because they have cornered themselves: every future change is a change that breaks compatibility, from which everyone will feel bad.

The best solution is to avoid using abstraction and its adapters completely. As Mark Seemann quite accurately explained 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 much harder to avoid using it. By defining abstraction and actively promoting its use, Microsoft leads thousands of third-party developers of libraries and frameworks to stop thinking about defining the right abstraction for the library and framework ( Mark Seemann articles 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 IViewComponentActivatorabstraction before the start of RC2). And if we see that the MVC team brings such errors to such a late stage of the development cycle, then what can we expect from all those developers who begin to develop on the new .NET platform?

Conclusion


Defining DI abstraction is Microsoft's painful mistake that will haunt us for years to come. It already suppresses innovation, breeds hell of versioning, and upsets many developers. The abstraction is incompatible with many DI libraries and, contrary to expert recommendations, Microsoft decided to keep it by dividing the world into incompatible and partially compatible containers, which leads to endless reports of problems with adapters 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.

Also popular now: