ASP.NET Core依赖注入系统学习教程:容器对构造函数选择的策略
.NET Core的依赖注入容器之所以能够为应用程序提供服务实例,这都归功于ServiceDescriptor对象提供的服务注册信息。另外,在ServiceDescriptor对象中,还为容器准备了3种提供服务实例的方式:
- 使用Func<IServiceProvider, object>类型的委托对象作为工厂进行提供;
- 直接使用实例化的对象进行提供;
- 根据服务的实现类型实例化进行提供;
如果容器选择“根据服务的实现类型实例化进行提供”(上面的第3种方式)作为提供服务实例的方式,那么容器就必须调用实现类型的构造函数才能创建相应类型的实例。在大部分的应用程序中,实现类型必然会对其他类型产生依赖,并且依赖的类型会存在多个,这就使实现类型为了初始化依赖的对象而定义多个构造函数。那么对于这样的情况而言,必然会产生一个问题:当容器发现实现类型中存在多个构造函数时,它会选择哪一个构造函数来创建服务实例?
其实我们可以将容器对构造函数选择的策略看作:使用两个条件在多个构造函数中筛选的过程。最终同时满足两个条件,筛选出的唯一构造函数,就是容器用于创建服务实例的构造函数,这两个条件如下:
- 构造函数中的参数类型,必须都进行了服务注册;
- 构造函数中的参数列表,是所有构造函数的超集;
为了使读者能够更加深刻地理解策略中的两个条件,下面我们会根据示例演示的方式进行讲述。
示例背景
本示例将定义4个服务接口(IServiceA、IServiceB、IServiceC和IServiceD),以及实现这个4个接口的类型(ServiceA、ServiceB、ServiceC和ServiceD)。示例将选择ServiceD类型作为切入点,所以容器将会在ServiceD类型的多个构造函数中选择一个。其他的几个类型将作为ServiceD类型的依赖项,并将它们通过ServiceD的构造函数对其初始化。
1 public interface IServiceA { }
2 public interface IServiceB { }
3 public interface IServiceC { }
4 public interface IServiceD { }
5
6 public class ServiceA: IServiceA { }
7 public class ServiceB : IServiceB { }
8 public class ServiceC : IServiceC { }
9 public class ServiceD : IServiceD
10 {
11 private IServiceA _serviceA;
12 private IServiceB _serviceB;
13 private IServiceC _serviceC;
14 private IServiceD _serviceD;
15 public ServiceD(IServiceA serviceA)
16 {
17 _serviceA = serviceA;
18 Console.WriteLine("选择的是构造函数是:ServiceD(ServiceA serviceA)");
19 }
20
21 public ServiceD(IServiceA serviceA, IServiceB serviceB)
22 {
23 _serviceA = serviceA;
24 _serviceB = serviceB;
25 Console.WriteLine("选择的是构造函数是:ServiceD(ServiceA serviceA, ServiceB serviceB)");
26 }
27 public ServiceD(IServiceA serviceA, IServiceB serviceB, IServiceC serviceC)
28 {
29 _serviceA = serviceA;
30 _serviceB = serviceB;
31 _serviceC = serviceC;
32 Console.WriteLine("选择的是构造函数是:ServiceD(ServiceA serviceA, ServiceB serviceB, ServiceC serviceC)");
33 }
34
35 }
在控制台的演示程序中创建了一个ServiceCollection对象,并在其中添加针对IServiceA、IServiceB、IServiceD这3个服务接口的服务注册,但未添加针对服务接口IServiceC的注册。然后使用ServiceCollection对象的BuildServiceProvider方法构建容器对象,并通过容器对象获取IServiceD的服务实例。
1 static void Main(string[] args)
2 {
3 var serviceCollextion = new ServiceCollection();
4 serviceCollextion.AddTransient<IServiceA,ServiceA>();
5 serviceCollextion.AddTransient<IServiceB, ServiceB>();
6 serviceCollextion.AddTransient<IServiceD, ServiceD>();
7
8 var provider = serviceCollextion.BuildServiceProvider();
9 provider.GetService<IServiceD>();
10
11 } // END Main()
分析
在定义和编写了示例中的代码后,在执行这个示例程序之前我们先使用策略中的第一个条件(构造函数中的参数类型,必须都进行了服务注册),在结合和“服务注册信息”。然后通过图例分析的方式来分析看看,容器会在ServiceD类型中筛选出哪些构造函数,使用第一个筛选条件的分析结果如下图:
从上图的分析内容可以看出,第三个构造函数被过滤掉了。这是因为第三个构造函参数列表的IServiceC类型并没有进行服务注册。另外,对于符合第一个筛选条件的构造函数,通常被称为“候选构造函数”。在获得第一个筛选条件的结果(候选构造函数列表)后,我们在使用第二个筛选条件(构造函数中的参数列表,是所有构造函数的超集)进行对其进行分析。
第二个筛选条件中的“超集”是筛选的核心,在这里超集的意思就是:超集构造函数的参数列表,会包含所有构造函数的参数列表。每个候选构造函数的参数列表,都属于超集构造函数参数列表的子集。
例如一个集合S2中的每个元素都在集合S1中,即便集合S1中可能存在S2中没有的元素,但S1中的元素始终都会包含S2中所有元素,对于这样的情况而言集合S1就是S2的一个超集。反过来,S2是S1的子集,S1是S2的超集。
有了对“超集”概念的理解,我们便可以看出在本示例的“候选构造函数”中,属于超集的构造函数是:ServiceD(IServiceA serviceA, IServiceB serviceB)。此时,完成对“容器对构造函数选择策略”的分析,我们可以判定:容器在面临ServiceD类型多个构造函数时,会选择使用其中第二个构造函数:ServiceD(IServiceA serviceA, IServiceB serviceB),来实例化对象。
接下来我们通过运行本示例的演示程序,发现运行结果和我们使用“容器对构造函数选择策略”分析的结果一致。
无超集
但是对于某些情况而言,如在使用第一个条件(构造函数所有的参数类型都进行了服务注册)筛选出了符合条件的“候选构造函数”之后,没有发现符合第二个条件(没有超集)的构造函数会发生怎么样的现象?
接下来为了验证这种情况会带来什么样的现象,我们将代码示例进行如下的改动:
1 using Microsoft.Extensions.DependencyInjection;
2 using Microsoft.Extensions.DependencyInjection.Extensions;
3 using System;
4 using System.Collections.Generic;
5 using System.Diagnostics;
6 namespace ConsoleApp1
7 {
8 public interface IServiceA { }
9 public interface IServiceB { }
10 public interface IServiceC { }
11 public interface IServiceD { }
12
13 public class ServiceA: IServiceA { }
14 public class ServiceB : IServiceB { }
15 public class ServiceC : IServiceC { }
16 public class ServiceD : IServiceD
17 {
18 #region 字段...
19 private IServiceA _serviceA;
20 private IServiceB _serviceB;
21 private IServiceC _serviceC;
22 private IServiceD _serviceD;
23 #endregion
24
25
26 public ServiceD(IServiceA serviceA, IServiceB serviceB)
27 {
28 _serviceA = serviceA;
29 _serviceB = serviceB;
30 Console.WriteLine("选择的是构造函数是:ServiceD(ServiceA serviceA, ServiceB serviceB)");
31 }
32 public ServiceD(IServiceA serviceA,IServiceC serviceC)
33 {
34 _serviceA = serviceA;
35 _serviceC = serviceC;
36 Console.WriteLine("选择的是构造函数是:ServiceD(ServiceA serviceA,ServiceC serviceC)");
37 }
38 }
39
40
41 internal class Program
42 {
43 static void Main(string[] args)
44 {
45 var serviceCollextion = new ServiceCollection();
46 serviceCollextion.AddTransient<IServiceA,ServiceA>();
47 serviceCollextion.AddTransient<IServiceB, ServiceB>();
48 serviceCollextion.AddTransient<IServiceC, ServiceC>();
49 serviceCollextion.AddTransient<IServiceD, ServiceD>();
50
51 var provider = serviceCollextion.BuildServiceProvider();
52 provider.GetService<IServiceD>();
53
54 } // END Main()
55
56 }
57 }
对于上面改动的示例而言,ServiceD类型所有构造函数上的参数类型虽然都进行了服务注册,即符合第一个筛选条件。但是并没有一个构造函数的参数列表,能够成为所有构造函数参数列表的超集,即不符合第二个筛选条件。接下来我们运行示例程序看看,当没有超集的构造函数时会发生什么样的后果。
如上图所示,在运行该示例程序后抛出了异常,其中的异常信息表示:无法从两个候选的构造函数中选择一个最优的来创建服务实例。这个异常也意味着并没有一个构造函数的参数列表,能够成为所有构造函数参数列表的超集。
总结
对于本篇文章讲解的主题——“容器对构造函数选择的策略”,我个人认为其中讲解的内容对于依赖注入框架而言是比较有实用性的。我们可以试想下,如果你没有了解“容器对构造函数选择的策略”,那么你在为类型定义构造函数时并不会遵循策略,这很可能会导致你的应用程序中的类型没有按预期方式实例化,或者出现无法实例化服务的异常现象。
所以为了稳妥的使用依赖注入框架,我们必须遵循“容器对构造函数选择的策略”,以此保证了应用程序所依赖的类型进行了服务注册,并且保证容器在面临多个构造函数选择时能够选出对应的“超集”。