本文示例代码,均采用 .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>
的注册方法中。