ASP.NET Core依赖注入系统学习教程:针对服务注册的验证
1.避免Scoped模式注册的服务变成Singleton模式
当提供一个生命周期模式为Singleton的服务实例时,如果发现该服务中还依赖生命周期模式为Scoped的服务实例(Scoped服务实例将被一个Singleton服务实例所引用),那么这个被依赖的Scoped服务实例最终会成为一个Singleton模式的服务实例。这是因为提供Singleton服务的容器是根容器,Scoped服务间接的被根容器所创建提供了,如果Scoped服务由子容器进行提供,那么Singleton和Scoped这两种生命周期模式才会产生差别。
在ASP.NET Core应用中,将某个服务注册的生命周期设置为Scoped,其意图是希望Scoped服务实例的创建和释放是作用于某个HTTP请求范围内的。如果不注意,将Scoped服务实例引用到了Singleton服务实例中,对于这种情况Scoped和Singleton的服务实例没有区别的。这样的Scoped服务实例直到应用关闭才会被释放,这无疑违背我们使用Scoped模式的初衷。这种“混淆”如果没有察觉到,可能会在实际的应用中造成难以估量的后果,例如在Singleton服务中引用的Scoped服务是一个数据库连接对象,这会导致数据库长时间连接没有及时释放,从而导致程序出现异常。
为了避免Scoped模式注册的服务“隐式”的变成Singleton模式的服务带来的风险,.NET Core为我们提供了一种验证方式来规避这样的行为,这个方式就是将ServiceProviderOptions配置对象的ValidateScopes属性设置为True。
当开启了这个验证后,依赖注入框架则会对注册Scoped模式的服务进行检查,确保不会出现如下情况:
- 有根容器去提供Scoped的服务实例;
- Singleton服务中存在对Scoped服务的依赖;
一旦开启针对Scoped模式服务的注册验证,如果存在以上的两种情况,那么程序启动时会抛出异常。
ValidateScopes的值在开发环境下默认值是为True,为了确保在生产环境或其他环境始终开启验证,我们可以在Program类的CreateHostBuilder方法中配置ServiceProviderOptions对象。
该验证方式被官方称作为“服务范围”的验证,并建议应用程序开启此验证,以确保我们注册Scoped模式的服务仅作用于某个服务范围,而不会“悄悄地”演变成作用于整个应用程序范围的Singleton模式。
2.验证服务注册是否能够提供相应的实例
依赖注入框架中进行服务注册的信息一般都存放于ServiceDescriptor的对象中,而容器对象就是根据ServiceDescriptor对象中的注册信息进行服务实例的提供。ServiceProviderOptions配置类型除了用于针对“服务范围”的验证ValidateScopes属性之外,还有一个ValidateOnBuild属性。如果将该属性设置为True,这就意味着容器对象在构建时,会对每个ServiceDescriptor对象中的注册信息实施有效性验证,如果服务注册信息不能提供出对应的实例则会抛出异常。
使用ValidateOnBuild属性进行验证的目的是因为,往往有些服务能够正常注册但不代表,容器能够根据注册信息成功的提供实例。下面我将通过一个代码示例来印证这一情况,并演示使用ValidateOnBuild属性进行验证的方式。
1 using Microsoft.Extensions.DependencyInjection;
2 using System;
3 using System.Diagnostics;
4
5 namespace ConsoleApp1
6 {
7
8 public interface IFooBar { }
9 public class FooBar : IFooBar
10 {
11 private FooBar() { }
12 }
13
14 internal class Program
15 {
16 static void Main(string[] args)
17 {
18 Console.WriteLine("ValidateOnBuild的值为True:");
19 BuildServiceProvider(true);
20 Console.WriteLine();
21
22 Console.WriteLine("ValidateOnBuild的值为False:");
23 BuildServiceProvider(false);
24
25 } // END Main()
26
27 static void BuildServiceProvider(bool validateOnBuild)
28 {
29 try
30 {
31 var options = new ServiceProviderOptions { ValidateOnBuild = validateOnBuild };
32 var provider= new ServiceCollection()
33 .AddSingleton<IFooBar, FooBar>()
34 .BuildServiceProvider(options);
35 Console.WriteLine($"程序运行正常;");
36 }
37 catch (Exception e)
38 {
39 Console.WriteLine($"程序出现异常;异常信息:{e.Message};");
40 }
41
42 } // END BuildServiceProvider()
43
44
45 }
46 }
在上面的代码示例中,服务注册时指定的实现类型为FooBar,而该类型中唯一的构造函数是一个私有的。我们都知道创建对象的实例,则必须要调用对象类型的构造函数,而FooBar的构造函数是私有的,无法对外界所调用,则也意味着无法创建相应的实例。
运行上面的代码示例后我们会发现,对于ValidateOnBuild属性值设置为False的情况,程序可以正常的执行服务注册的方法,这种现象隐瞒了注册时的错误(类型中存在私有构造函数),这种做法显然会对后续使用到FooBar对象的程序功能带来影响。而对于ValidateOnBuild属性值设置为True的情况,程序则直接抛出了异常并且给出了详细的错误信息,对于这种做法我们可以更好的规避服务注册时所产生的风险。
本示例代码是在“服务定位器模式”下配置的ValidateOnBuild属性,这是为了更好的演示。当然,我们在实际的开发中通常是依赖注入的形式,那么相应的方式是通过在CreateHostBuilder方法中ServiceProviderOptions对象进行配置,如下图:
3.总结
本文介绍的关于“针对服务注册的验证”的主题,实际上就是介绍ServiceProviderOptions类型中ValidateScopes和ValidateOnBuild属性的使用场景和方式。VailedateScopes属性主要确保我们注册的Scoped服务不会在某些情况下变成Singleton服务,ValidateOnBuild属性主要用于验证服务注册信息是否能成功的提供出对应的服务示例。