推荐关注「码侠江湖」加星标,时刻不忘江湖事
这篇文章是 ASP.NET 6 依赖注入系列文章的第 4 篇,点击上方蓝字可以阅读整个系列。
在上一篇文章中,我们讨论了依赖注入的服务容器与服务作用域。
接下来,在这篇文章中,我们继续深入了解服务注册与注入相关的内容。
服务注册
现在,让我们回头看一看ServiceCollection
服务集合类型。
我们现在已经知道,根容器是通过调用服务集合的BuildSerivceProvider
扩展方法创建的。
服务集合ServiceCollection
对象是一个存放服务注册信息的集合。
以下是它的部分源码,完整代码在这里。
public class ServiceCollection : IServiceCollection
{
private readonly List<ServiceDescriptor> _descriptors = new List<ServiceDescriptor>();
private bool _isReadOnly;
public int Count => _descriptors.Count;
public ServiceDescriptor this[int index]
{
get
{
return _descriptors[index];
}
set
{
CheckReadOnly();
_descriptors[index] = value;
}
}
// ...
}
通过这部分代码可以发现,ServiceCollection
本质上是一个元素类型为ServiceDescriptor
的集合。
ServiceDescriptor
是服务描述类,它描述的是某个注册服务的基本信息,其中包含了服务类型本身以及它的实现类型,还有生命周期模式。
服务注册的本质就是创建相应的服务描述对象,并将其添加到服务集合对象中的过程。
依赖注入系统就是利用服务描述信息,才能够提供我们所依赖的服务实例,并注入其中。
依赖注入系统就会根据指定的服务类型从服务集合中,找到相应的服务描述对象,并且根据它提供的类型信息,来创建服务实例。
构造函数注入
依赖注入系统在创建服务实例时会先调用它的构造函数。
如果构造函数有参数,那么传入构造函数的所有参数必须也必须先进行初始化。
所以,服务实现类的构造函数必须具备一个基本的条件,那就是服务容器能够提供构造函数所需的所有参数,这就是构造函数注入。
如果当我们的服务实现类有多个重载版本的构造函数时,那么依赖注入系统会如何进行选择呢?
比如下面这个示例:
public static class Sample
{
public interface IAccount{ }
public interface IMessage{ }
public interface ITool{ }
public interface ITest{ }
public class Account: IAccount{}
public class Message: IMessage{}
public class Tool: ITool{}
public class Test: ITest
{
public Test(IAccount account)
{
Console.WriteLine($"Ctor:Test(IAccount)");
}
public Test(IAccount account, IMessage message)
{
Console.WriteLine($"Ctor:Test(IAccount,IMessage)");
}
public Test(IAccount account, IMessage message, ITool tool)
{
Console.WriteLine($"Ctor:Test(IAccount,IMessage,ITool)");
}
}
public static void Main()
{
var test = new ServiceCollection()
.AddTransient<IAccount, Account>()
.AddTransient<IMessage, Message>()
.AddTransient<ITest, Test>()
.BuildServiceProvider()
.GetService<ITest>();
}
}
在这个例子中,定义了 4 个服务接口IAccount
IMessage
ITool
ITest
以及它们的实现类。
其中Test
类型中定义了 3 个构造函数,它们的每一个参数都是一个服务。
为了确定依赖注入系统,会选择哪个构造函数来创建服务实例,每个构造函数都会在控制台中输出自身的字符串标识。
Main 方法中创建了一个服务集合对象,并且注册了除ITool
以外的其它 3 个服务接口。
那么当我们获取ITest
的服务实例时,会发生什么情况呢?它会通过执行哪个构造函数创建实例呢?
我们可以尝试分析一下,对于定义在Test
类中的 3 个构造函数来说,
由于我们只注册了IAccount
和IMessage
服务接口,所以服务容器只能够提供给前两个构造函数的所有参数,
而第三个构造函数只有一个ITool
类型的参数,服务容器无法提供。
根据前面所说的基本条件「服务容器能够提供构造函数所需的所有参数」,此种情况下,也只有Test
类型的前两个构造函数是符合条件的。
那么在所有符合条件的构造函数中,依赖注入系统又会如何选择呢?
这里用一个专业的说法来描述:「如果某个构造函数的参数类型集合,能够成为所有合法构造函数参数类型集合的超集,那么这个构造函数就会被依赖注入系统选择。」
简单来说,就是在所有符合条件的构造函数中,选择参数最多的那个。
如果按照这两个条件来分析的话,那么应该是第二个构造函数,以下执行结果证明了这一点。
接下来,我们对上面的示例改动一下,修改了Test
类型的构造函数:
public class Test: ITest
{
public Test(IAccount account, IMessage message)
{
Console.WriteLine($"Ctor:Test(IAccount)");
}
public Test(IMessage message, ITool tool)
{
Console.WriteLine($"Ctor:Test(IAccount,IMessage)");
}
}
public static void Main()
{
var test = new ServiceCollection()
.AddTransient<IAccount, Account>()
.AddTransient<IMessage, Message>()
.AddTransient<ITool, Tool>()
.AddTransient<ITest, Test>()
.BuildServiceProvider()
.GetService<ITest>();
}
我们只为Test
类型定义了两个构造函数,它们都具有两个参数:
一个构造函数的参数是 IAccount 和 IMessage 接口;
另一个构造函数的参数是 IMessage 和 ITool 接口。
并且在 Main 方法中,将每个服务接口都进行了注册。
那么此种情况下,我们是否能成功获取一个ITest
对象呢?如果可以的话,它又是通过执行哪个构造函数创建的呢?
虽然Test
的两个构造函数的参数都可以由服务容器提供,并且也满足了第一个条件。
但是还有一个条件没有达成,那就是:没有一个构造函数的参数类型集合,能够成为所有合法构造函数类型集合的超集。
也就说,无法满足第二个条件,因此依赖注入系统无法就选择无能了。
运行这个示例,抛出如下的异常提示:无法从两个候选的构造函数中选择一个最优的来创建服务实例。
注入方式
实际上,在依赖注入的世界中,除了构造函数注入以外,注入方式还有很多种:如属性注入、方法注入、特性注入等。
「属性注入」,就是服务类把所依赖的其它服务,以属性方式声明,并以属性方式注入进来。
依赖注入系统在实例化服务类时,会通过属性把服务类所需要的对象注入进来。
「方法注入」,就是当服务类中的某个方法的参数依赖其它服务时,以参数形式注入进来。
「特性注入」,可以说是其它注入方式的一种补充。
比如有多个构造函数都符合注入的标准条件,但你只想让其中一个构造函数拥有被注入的能力,这时候可以通过特性来修饰这个构造函数,指定它是唯一可注入的构造函数。
目前,.NET 6 的依赖注入系统原生只支持构造函数注入,不过在某些类型的 Web 应用扩展下,也支持特定的方法注入、特性注入、甚至属性注入。
更多精彩内容,请关注我▼▼
如果喜欢我的文章,那么
在看和转发是对我最大的支持!
(戳下面蓝字阅读)
推荐关注微信公众号:码侠江湖
觉得不错,点个在看再走哟