.NET6最通俗易懂的依赖注入之服务注册与注入

这篇文章是 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 个构造函数来说,

由于我们只注册了IAccountIMessage服务接口,所以服务容器只能够提供给前两个构造函数的所有参数,

而第三个构造函数只有一个ITool类型的参数,服务容器无法提供。

根据前面所说的基本条件「服务容器能够提供构造函数所需的所有参数」,此种情况下,也只有Test类型的前两个构造函数是符合条件的。

那么在所有符合条件的构造函数中,依赖注入系统又会如何选择呢?

这里用一个专业的说法来描述:「如果某个构造函数的参数类型集合,能够成为所有合法构造函数参数类型集合的超集,那么这个构造函数就会被依赖注入系统选择。」

简单来说,就是在所有符合条件的构造函数中,选择参数最多的那个。

如果按照这两个条件来分析的话,那么应该是第二个构造函数,以下执行结果证明了这一点。

.NET6最通俗易懂的依赖注入之服务注册与注入

接下来,我们对上面的示例改动一下,修改了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的两个构造函数的参数都可以由服务容器提供,并且也满足了第一个条件。

但是还有一个条件没有达成,那就是:没有一个构造函数的参数类型集合,能够成为所有合法构造函数类型集合的超集。

也就说,无法满足第二个条件,因此依赖注入系统无法就选择无能了。

运行这个示例,抛出如下的异常提示:无法从两个候选的构造函数中选择一个最优的来创建服务实例。

.NET6最通俗易懂的依赖注入之服务注册与注入

注入方式

实际上,在依赖注入的世界中,除了构造函数注入以外,注入方式还有很多种:如属性注入、方法注入、特性注入等。

「属性注入」,就是服务类把所依赖的其它服务,以属性方式声明,并以属性方式注入进来。

依赖注入系统在实例化服务类时,会通过属性把服务类所需要的对象注入进来。

「方法注入」,就是当服务类中的某个方法的参数依赖其它服务时,以参数形式注入进来。

「特性注入」,可以说是其它注入方式的一种补充。

比如有多个构造函数都符合注入的标准条件,但你只想让其中一个构造函数拥有被注入的能力,这时候可以通过特性来修饰这个构造函数,指定它是唯一可注入的构造函数。

目前,.NET 6 的依赖注入系统原生只支持构造函数注入,不过在某些类型的 Web 应用扩展下,也支持特定的方法注入、特性注入、甚至属性注入。