.NET CORE dependency injection

ASP.NET Core applications rely on various components to provide services during startup and subsequent request processing. In order to facilitate customization, these components are generally standardized in the form of interfaces, and we collectively call these standardized components “Service”. The entire ASP.NET Core framework is built on a low-level dependency injection framework, which uses the Dependency Injection Container to provide the required service objects.

Inversion of Control

Inversion of flow control

The full English name of IoC is Inverse of Control, which can be translated as inversion of control or inversion of control. Both inversion of control and inversion of control embody the transfer of control rights

The control involved in IoC can be understood as “control for the process”. IoC is a basic idea adopted by the design framework. The so-called inversion of control is to transfer the application’s control over the process to the framework.

Class libraries and frameworks

By transferring the control of a set of common processes from the application to the framework, the process can be reused, and the interaction between the code of the application and the framework can be realized according to Hollywood rules.

The difference between a class library (Library) and a framework (Framework) is: the former often only provides an API to achieve a single function; while the latter arranges these single functions for a target task to form a complete process, and Utilize an engine to drive the flow

program executes automatically.

Generally speaking, the framework will provide a series of extension points in a corresponding form, and the application can customize a certain link of the process by registering extensions.

IOC thought

IoC is an inherent attribute of almost all frameworks. In this sense, IoC framework is actually a wrong statement. It can be said that there is no IoC framework in the world, and it can also be said that all frameworks are IoC frameworks.

We can implement IoC in different ways using several design patterns such as template method, factory method and abstract factory

Dependency injection is the way to realize the idea of IOC

Dependency injection

Object provided by the container

Dependency injection is an “object-providing” design pattern, and the provided objects can be collectively referred to as “service”, “service object” and “service instance”. In an application that uses dependency injection, when we define a certain type, we only need to directly inject the services it depends on in the corresponding way.

When the application starts, we register the required services globally. Generally speaking, services are mostly registered for implemented interfaces or inherited abstract classes, and service registration information will help us provide corresponding service instances in the subsequent consumption process.

The container used by the framework to provide services is called a dependency injection container

Since the service registration ultimately determines what kind of service instance the dependency injection container will provide according to the specified service type, we can customize the framework by modifying the service registration.

Three dependency injection methods

The coupling between classes can be reduced or removed by abstracting dependencies.

As the dependency injection container of the service object provider, it will provide all dependent service instances according to the dependency chain.

Constructor injection [optimal way]

Constructor injection is to inject the dependent object into the object created by it with the help of parameters in the constructor.

Constructor injection may have different strategies for selecting constructors. For example, an InjectionAttribute can be marked on the target constructor function.

Property Injection

If the dependency is directly reflected as an attribute of the class, and the attribute is not read-only, the dependency injection container can automatically assign a value to it after the object is created, thereby achieving the purpose of dependency injection.

Method Injection

Fields or properties that reflect dependencies can be initialized in the form of methods.

A more convenient injection method for ASP.NET Core

ASP.NET Core will call the registered Startup object to complete the registration of the middleware when it starts, and it does not need to implement an interface when defining the Startup type, so the Configure method for registering middleware does not have a fixed statement , so an interface can be passed into Configure as a parameter.

Middleware types under the ASP.NET Core framework also do not need to implement a predefined interface, and the InvokeAsync method or Invoke method used to process requests can also inject arbitrary dependent services.

Service Locator mode

The Service Locator pattern also has a global container created through service registration to provide the required service instances,

This container is called the Service Locator.

What are the main differences between dependency injection and Service Locator?

The consumer of a dependency injection container should be the framework, not the application. The Service Locator pattern is obviously not the case, but the application is using it to provide the required service instance, so its user is the application.

Distinguish the difference between the two from another angle. Since dependent services are provided in the form of “injection”, applications using dependency injection mode can be regarded as pushing services to the dependency injection container, while applications under Service Locator mode use Service Locator to pull required services. This “push”-“pull” also accurately reflects the difference between the two.

The essential difference between dependency injection and service locator mode:

A current service’s dependency on another service is fundamentally different from a dependency on a dependency injection container or Service Locator. The former is a type-based dependency, whether it is a service-based interface or an implementation type, which is a “contract”-based dependency. This dependence is not only explicit, but also guaranteed. But the dependency injection container or Service Locator is essentially a black box. The premise that it can provide the required services is that the corresponding service registration has been pre-added to the container, but this dependency is not only vague but also unreliable.

Dependency injection container programming experience

Hierarchical relationship between containers Cat

The above reflects the hierarchical structure of the attributes of the container, and reflects the logical structure. In fact, each Cat object will only refer to the root of the entire tree in the manner shown in the figure.

Predefined life cycle

The dependency injection framework uses ServiceLifetime to represent the three life cycle modes of Singleton, Scoped and Transient

Transient means that the container will create a new service instance for each service request;

Self saves the provided service instance in the current container, which represents a singleton pattern for a certain container scope;

Root uniformly stores the service instances provided by each container in the root container, so this mode can ensure that the services provided are singletons within the scope of multiple “same root” containers.

 namespace App
{
public enum Lifetime
{
// Store the service instances provided by each container in the root container uniformly,
// So this mode can ensure that the services provided are singletons within the scope of multiple "same root" containers
Root,
//Save the provided service instance in the current container, which represents a singleton pattern for a certain container scope
Self,
//On behalf of the container, a new service instance will be created for each service request
Transient
}
}

Registration and consumption of services

The dependency injection framework mainly involves two NuGet packages. Some interfaces and basic data types that we frequently use in the programming process are defined in the NuGet package “Microsoft.Extensions.DependencyInjection.Abstractions“, and dependency injection The specific implementation is carried by the NuGet package “Microsoft.Extensions.DependencyInjection

IServiceCollection interface

Service registration is stored in the collection represented by the IServiceCollection interface. When the application starts, the registration for the service is essentially the process of creating the corresponding ServiceDescriptor object and adding it to the specified IServiceCollection object.

The dependency injection container created by this collection is represented as an IServiceProvider object. Since an IServiceProvider object always provides a corresponding service instance with the specified service type, services are always registered based on type.

ServiceCollection object (which is the default implementation of the IServiceCollection interface)

After completing the service registration, we call the BuildServiceProvider extension method of the IServiceCollection interface to create an IServiceProvider object representing the dependency injection container, and call the GetService method of the object to provide the corresponding service instance

The dependency injection framework adopts the method of “coming from behind ” strategy, that is, the dependency injection container always uses the most recently added service registry to create service instances. If the GetServices extension method is called, this method will utilize all service registrations for the specified service type to provide a set of service instances.

Lifecycle

The Singleton service instance is saved on the IServiceProvider object as the root container, so it can provide a true singleton guarantee among multiple IServiceProvider objects of the same root.

Scoped service instances are stored on the current IServiceProvider object, so it is only guaranteed that the provided instance is a singleton within the current scope.

Transient services that do not implement the IDisposable interface adopt the strategy of “build it out of the box and discard it after use”.

The IServiceProvider object as a dependency injection container can not only provide the required service instances, but also manage the life cycle of these service instances. If a service type implements the IDisposable interface, it means that when the life cycle ends, some resource release operations need to be performed by calling the Dispose method. These operations are also driven by the IServiceProvider object that provides the service instance. The release strategy of the dependency injection framework for providing service instances depends on the life cycle mode adopted by the corresponding service registration. The specific strategy is as follows.

● Transient and Scoped: All service instances that implement the IDisposable interface will be saved by the current IServiceProvider object. When the Dispose method of the IServiceProvider object is called, the Dispose method of these service instances will be called accordingly.

● Singleton: Since the service instances are saved on the IServiceProvider object as the root container, the Dispose method of these service instances will be called only when the Dispose method of the latter is called

Authentication for service registration

If a Singleton service depends on another Scoped service, the Scoped service instance will be referenced by a Singleton service instance, which means that the Scoped service instance becomes a Singleton service instance. Just imagine, if the resources (such as database connections) referenced by the Scoped service instance need to be released in time, this may cause incalculable consequences.

If you want the IServiceProvider object to check the validity of the service scope during the process of providing services, you only need to use a Boolean True value as a parameter when calling the BuildServiceProvider extension method of the IServiceCollection interface.

Once the verification for the service scope is enabled, it is impossible for the IServiceProvider object to provide Scoped services that exist in the form of singletons

The configuration option type ServiceProviderOptions provides two properties: the ValidateScopes property indicates whether to enable the verification of the service scope; the ValidateOnBuild property indicates whether it is necessary to pre-check whether each ServiceDescriptor object registered as a service can provide a corresponding service instance.

ServiceDescriptor

ServiceDescriptor is a description of a service registration item, and the IServiceProvider object as a dependency injection container is able to provide the service instance we need by using the description information provided by this object

The other three attributes of ServiceDescriptor reflect three ways of providing service instances, and correspond to three constructors respectively.

The first constructor: the service instance is obtained by calling the constructor

The second constructor: the service instance is provided through the factory

The third constructor: directly specify a ready-made object (the corresponding property is ImplementationInstance), then this object is the final service instance provided

If a ready-made service instance is used to create a ServiceDescriptor object, the corresponding service registration will adopt the Singleton life cycle mode. For ServiceDescriptor objects created through the other two constructors, it is necessary to explicitly specify the adopted lifecycle mode.

Consumption of services

The IServiceCollection collection containing the service registration information is ultimately used to create the IServiceProvider object as a dependency injection container.

IServiceProvider

The GetService method specifies the service type in the form of generic parameters, and the returned service instance will also perform corresponding type conversion. If the service registration of the specified service type does not exist, the GetService method will return Null, and if the GetRequiredService method or the GetRequiredService method is called, an exception of type InvalidOperationException will be thrown

Creation of service instance

The ServiceDescriptor object has 3 different constructors, corresponding to the first 3 ways of providing service instances. If the service implementation type is provided, the final service instance will be created by calling a certain constructor of this type, then the constructor By what strategy is the function selected?

The IServiceProvider object can provide all parameters of the constructor

The set of parameter types of each candidate constructor is a subset of the set of parameter types of this constructor, that is, the set of parameter types of a constructor can be a superset of the set of parameter types of all valid constructors

Lifecycle

The life cycle determines how the IServiceProvider object provides and releases service instances.

Scope of service:

①Singleton

The service instance created by the IServiceProvider object is stored in the IServiceProvider object as the root container, so the service instances of the same type provided by multiple IServiceProvider objects of the same root are the same object

②Transient
The IServiceProvider object always creates a new service instance for each service provisioning request

③Scoped

The service instance created by the IServiceProvider object is saved by itself, so the same type of service instance provided by the same IServiceProvider object is the same object

Scoped refers to the service scope represented by the IServiceScope interface, which is created by the “service scope factory” represented by the IServiceScopeFactory interface.

The tree hierarchy shown in the figure above is just a logical structure. From the perspective of object reference, the IServiceProvider object encapsulated by a certain IServiceScope does not need to know who its “father” is, it only cares about the IServiceProvider object as the root node Where

The above figure reveals the relationship between IServiceScope/IServiceProvider objects from the physical level, and any IServiceProvider object has a reference to the root container

The recovery and release strategy adopted by the IServiceProvider object for the service instance depends on the life cycle mode adopted. The specific strategy is mainly reflected in the following two points:

● Singleton: Disposable service instances are provided and stored on the IServiceProvider object as the root container. These Disposable service instances can be released only when the IServiceProvider object is released.

● Scoped and Transient: The IServiceProvider object will save the Disposable service instances provided by it, and these Disposable service instances will be released when it is released.

ASP.NET Core application

The so-called service scope of the dependency injection framework has a clear boundary in the ASP.NET Core application, referring to the context of each HTTP request, that is, the life cycle of the service scope is bound to the context of each request.

The IServiceProvider objects used to provide service instances in ASP.NET Core applications are divided into two types: one is the IServiceProvider object that serves as the root container and has the same life cycle as the application, generally called ApplicationServices; One is the IServiceProvider object created and released in time according to the request, generally called RequestServices. The service instances used in the ASP.NET Core application initialization process (that is, the request pipeline construction process) are provided by ApplicationServices.

Implementation overview

ServiceProviderEngine

Indicates the provider engine that provides service instances. The service instances provided by the container are ultimately provided through this engine. There is only one globally unique ServiceProviderEngine object within an application scope

ServiceProviderEngineScope

Represents the scope of the service, which uses the cache of the provided service instance to realize the control of the life cycle

ServiceProviderEngine is an abstract class. The .NET Core dependency injection framework provides the following four specific implementation types. The default is DynamicServiceProviderEngine

ServiceProvider

A ServiceProvider object is created by calling the BuildServiceProvider extension method of the IServiceCollection collection.


Relationship between ServiceProviderEngine and ServiceProviderEngineScope

When using the IServiceCollection collection to create a ServiceProvider object, the provided service registration will be used to create a specific ServiceProviderEngine object. The RootScope of the ServiceProviderEngine object is a ServiceProviderEngineScope object created by it, and the Singleton service instance provided by the sub-container is maintained by it. 

The dependency injection framework has the following characteristics

Uniqueness of ServiceProviderEngine:

There is only one ServiceProviderEngine object in the entire service provider system

The identity of ServiceProviderEngine and IServiceFactory:

The only ServiceProviderEngine that exists will serve as the IServiceFactory factory that creates the service scope

Identity of ServiceProviderEngineScope and IServiceProvider:

The ServiceProviderEngineScope that represents the service scope also acts as a dependency injection container for service providers.

Extension

Adaptation

.NET Core has a Hosting system, which hosts services that need to run in the background, and an ASP.NET Core application is just a service hosted by the system. The hosting system always uses dependency injection to consume the services it needs in the service hosting process

For the hosting system, the original service registration is always embodied as an IServiceCollection collection, and the final dependency injection container is embodied as an IServiceProvider object. If you want to integrate a third-party dependency injection framework, you need to use them to solve the problem from IServiceCollection collection to Adaptation problem of IServiceProvider object.

Specifically, we can set a ContainerBuilder object for a third-party dependency injection framework between the IServiceCollection collection and the IServiceProvider object: First create a ContainerBuilder using the IServiceCollection collection containing the original service registration object, and then use this object to build an IServiceProvider object as a dependency injection container

IServiceProviderFactory

The two conversions are done using an IServiceProviderFactory object

The IServiceProviderFactory interface defines two methods: the CreateBuilder method uses the specified IServiceCollection collection to create the corresponding ContainerBuilder object; the CreateServiceProvider method further uses this ContainerBuilder object to create an IServiceProvider object as a dependency injection container.

Example of integrating third-party dependency injection framework