.NET 中的依赖注入(五):注册方法

本文示例代码,均采用 .NET 6,具体的代码可以在这个仓库 Articles.DI 中获取。

通过前面的文章,了解到了服务的三种声明周期。那么如果我们需要注册服务时,框架都提供了哪些 API 呢?当我们要根据自身需求来声明服务,声明具体的实现时,又该如何编写代码呢?本文将探讨 .NET 内置的 DI 框架提供的 API,以及其使用方法,具体作用。

.NET API 浏览器,提供了详细的 API 文档。这个链接就展示了注册服务相关的 API。

注册方法

注册服务的方法,肯定不止 Add{LifeTime}<TService, TImplementation>() 这一个方法,具体的方法类型,可以见点击这个链接,查看文档提供的表格,我在下面直接将其粘贴出来。

方法 自动释放 多种实现 传递参数
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION})
Add{LIFETIME}<{IMPLEMENTATION}>()
AddSingleton<{SERVICE}>(new {IMPLEMENTATION})
AddSingleton(new {IMPLEMENTATION})

上面的表格,只列出了支持泛型的方法,还有形如 Add{LIFETIME}(typeof({SERVICE}), typeof({IMPLEMENTATION})) 的方法,其本质和泛型方法一样,这里就不列出来了。表格中右边三列,分别描述了这些方法的局限范围。

服务释放

容器负责服务的构建,但是如果一个服务到期,又是谁来负责服务的释放?是服务的依赖者?还是容器?

前面表格中的 自动释放 一列,如果为 ,则表明通过这类方法注册的服务,由容器统一进行服务的释放,如果这些服务中有实现 IDisposable 接口,那么也由容器来自动调用 Dispose 方法,无需在代码中显式调用释放方法。

查看表格,可以看到 AddSingleton<{SERVICE}>(new {IMPLEMENTATION})AddSingleton(new {IMPLEMENTATION}) 两类方法中,是不会自动释放的,容器框架不会去自行释放,那么就需要开发人员自行负责服务的释放。

多种实现

前面的例子中,我们注册时都是一个接口,一个实现类,在依赖函数注入时,注入的都是单个服务。如果我们想要将多个实现,都注册为同一种接口,又该使用哪些方法呢?

前面表格中可以看到在 多种实现 这一列,有两类方法不支持多种实现,Add{LIFETIME}<{IMPLEMENTATION}>()AddSingleton(new {IMPLEMENTATION})。其实查看方法描述就可以了解到,前者注册时,直接将 IMPLEMENTATION 类的实现,注册为 IMPLEMENTATION 类型的服务,没法做到 多种实现。而后者也是类似,直接将一个注册实例,自然也没法做到 多种实现

这里的 多种实现,指的是实现了同一种接口的多个实现类,在注册服务的时候,同时注册到这个接口上。在上一篇文章中,我们是用一个实现类实现了多种接口,在注册服务的时候,这个类同时注册了多个接口,这两种情况是不一样的,后者不算 多种实现

接下来我们直接使用代码,来展示如何做到 多种实现

// https://github.com/alva-lin/Articles.DI/tree/master/WorkerService4
// 1. 设定一个接口 IMyDependency,以及实现这个接口的多个实现类
public interface IMyDependency
{
    string Id { get; }

    bool Flag { get; }
}

public class MyDependency0 : IMyDependency
{
    public string Id { get; } = Guid.NewGuid().ToString()[^4..];

    public bool Flag { get; init; } = false;
}

public class MyDependency1 : MyDependency0 {}
public class MyDependency2 : MyDependency0 {}
public class MyDependency3 : MyDependency0 {}
public class MyDependency4 : MyDependency0 {}
// 2. 注册服务
using Microsoft.Extensions.DependencyInjection.Extensions;

IHost host = Host.CreateDefaultBuilder(args)
   .ConfigureServices(services =>
    {
        // 同时注册一个接口的多种实现
        services.AddTransient<IMyDependency, MyDependency1>();
        services.AddTransient<IMyDependency, MyDependency2>();

        // TryAdd 如果接口已被注册过,不会再注册
        // IMyDependency 已经被注册过,所以并不会将 MyDependency3 注册为它的实现
        services.TryAddTransient<IMyDependency, MyDependency3>();

        // TryAddEnumerable 如果接口的同一种实现被注册过,则不会重复注册
        // IMyDependency -> MyDependency4,这个关系没有被注册过,所以可以成功注册
        var descriptor = new ServiceDescriptor(
            typeof(IMyDependency),
            typeof(MyDependency4),
            ServiceLifetime.Transient);
        services.TryAddEnumerable(descriptor);

        // 不会检查是否注册过这个接口,也不会检查是否注册过这个关系,会重复注册
        // MyDependency2 被注册过两次
        // 那么在获取 IEnumerable<IMyDependency> 时
        // MyDependency2 类型的实例会有两个
        services.AddTransient<IMyDependency, MyDependency2>(_ => new MyDependency2
        {
            Flag = true // 单独使用工厂方法构造这个服务,用于区分最后一次注册的服务
        });
    })
   .Build();

Fun(host.Services);

static void Fun(IServiceProvider serviceProvider)
{
    using var scopeServices = serviceProvider.CreateScope();
    var       services      = scopeServices.ServiceProvider;

    var myDependency = services.GetRequiredService<IMyDependency>();
    GetData(myDependency);
    Console.WriteLine();

    var list = services.GetRequiredService<IEnumerable<IMyDependency>>();
    foreach (var dependency in list)
    {
        GetData(dependency);
    }
    Console.WriteLine();
}

static void GetData<T>(T item) where T : IMyDependency
{
    Console.WriteLine($"{item.GetType().Name} {item.Id}, {item.Flag}");
}

程序的运行结果如下:

MyDependency2 c432, True

MyDependency1 ea48, False
MyDependency2 9b9a, False
MyDependency4 c4ce, False
MyDependency2 77e9, True

查看整个程序的输出结果,因为将所有服务注册为 Transient 类型的声明周期,所以每个实例的 Id 都不一样。

查看第一行输出中,item.Flag 为 True,也就是说在解析 IMyDependency 类型时,最后一次成功注册的实现类将会被构造并注入(即本例中的 myDependency 变量)。

对比下面四行输出,其顺序正好就是服务注册时的顺序,即获取 IEnumerable<IMyDependency> 类型的变量时,会按照注册顺序,将所有成功注册的类型,统统注入进去。而这里面的第二行和第四行,都是 MyDependency2,说明注册服务,是可以重复注册同一实现的。

传递参数

方法 自动释放 多种实现 传递参数
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION})
Add{LIFETIME}<{IMPLEMENTATION}>()
AddSingleton<{SERVICE}>(new {IMPLEMENTATION})
AddSingleton(new {IMPLEMENTATION})

传递参数其实就是容器在构造服务实例时,是否可以使用工厂方法,使用多样化的方式构造实例,而非单纯调用构造函数。查看这个表格,若是传递了工厂方法的,其 传递参数 列都为 ,反之则为 。实际上,即使不传递工厂方法,也可以用其他途径,做到传递参数的目的。

比如在构造函数中使用 IOperation<> 类型。将需要的配置数据,注册到 IOperation<MyOperation> 中,就可以在构造函数中,拿到需要的实时数据,从而达到传递参数的目的。而具体配置数据的获取,则可以放在 IOperation<MyOperation> 的注册方法中。

参考链接

.NET 中的依赖关系注入

posted @ 2022-01-17 00:58  Asjun  阅读(336)  评论(0编辑  收藏  举报