本文示例代码,均采用 .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 中的依赖关系注入