译者: Akini Xu
原文: New in ASP.NET Core 3: Service provider validation
作者: Andrew Lock
此文是 探索 ASP.NET Core 3.0 第3篇:
ASP.Net Core 3.0
.csproj文件,Program.cs及通用主机ASP.Net Core 3.0
Startup.cs在不同类型项目中的差异ASP.Net Core 3.0
新特性-Service provider validationASP.Net Core 3.0
应用程序启动时运行异步任务- 介绍IHostLifetime及与通用主机间的作用关系
ASP.Net Core 3.0
新特性-启动时的结构化日志.Net Core 3.0
新特性-本地工具
此篇文章来介绍ASP.NET Core 3.0中的新功能“编译时验证”。 此功能可以用来检测依赖注入中的配置错误。 具体来说,就是检查未在容器中注入但被依赖的服务。
首先,将展示该功能的工作原理,然后再介绍一些陷阱,当依赖注入容器有配置错误的时候,“编译时验证”并没有检查到这些问题。
需要指出的是:检查依赖注入配置并不是一个全新想法,这只是我们经常使用的一个功能
StructureMap
,参考Lamar
简单的示例
在这篇文章中,使用dotnet new webapi
来创建一个应用程序。 它包含WeatherForecastService
控制器,该控制器会返回一些随机数据。
首先,我们先重构一下控制器:
[ ] |
上面的控制器依赖于WeatherForecastService
。代码如下:
public class WeatherForecastService |
这个服务又依赖于DataService
,代码如下:
public class DataService |
以上就是我们需要的所有服务,然后我们在Startup.ConfigureServices
方法中,注入它们:
public void ConfigureServices(IServiceCollection services) |
在这个示例中,我们把服务全部注册为单例。在本章节中服务的生命周期(单例,瞬时,范围等)并不是要讲述的重点。运行程序,并访问/WeatherForecast
,我们会得到下面结果:
[{ |
程序现在是正常的。如果我们故意不注入某个服务,看看会发生什么?
在启动时检查未注入的依赖
我们故意遗漏 DataService
的注入,代码如下:
public void ConfigureServices(IServiceCollection services) |
当我们再次运行,会看到一个异常,包含异常的堆栈信息,提示程序无法启动。下面是该异常的部分信息:
Unhandled exception. System.AggregateException: Some services are not able to be constructed |
这个异常的错误信息非常明确:“ Unable to resolve service for type
‘TestApp.DataService’ while attempting to activate ‘TestApp.WeatherForecastService’ ” ,意思是:在试图创建’TestApp.WeatherForecastService’实例时,无法解析类型为’TestApp.DataService’的服务。可以看到依赖注入验证起了作用。它有助于我们,减少程序启动时因为依赖注入配置问题而产生的错误。但是相对于编译时就提示错误,这种运行时才抛出的异常就显的不那么方便了。
如果我们遗漏WeatherForecastService
注入又会发生什么?
public void ConfigureServices(IServiceCollection services) |
这种情况下,程序启动是正常的。但是当访问API时,就会发生异常!
陷阱1:控制器的构造函数不会被依赖注入验证器检查
验证器未能检查到这个问题的原因是:所有的控制器并不是由依赖注入容器创建的。正如之前的文章所说, DefaultControllerActivator
只是从容器中获取了服务间的依赖关系,而并未使用容器来创建。因此,容器中没有控制器,所以就无法检查控制器的依赖项是否已经注册。
幸运的是有方法来解决。我们可以通过 AddControllersAsServices()
方法将所有的控制器都作为服务添加到容器中:
public void ConfigureServices(IServiceCollection services) |
通过这种方式启用ServiceBasedControllerActivator
(查看之前的文章)并将控制器作为服务注入。再次运行程序,就可以看到因为缺少控制器的依赖而引发的异常:
Unhandled exception. System.AggregateException: Some services are not able to be constructed |
似乎这是一个简便的解决方法。但是不确定这是不是我想要的,毕竟它解决了问题。
另外构造函数注入并不是依赖注入唯一方式,我们还要看看其他注入方式。
陷阱2:[FromServices]方式不会被依赖注入验证器检查
有时候我们也会使用模型绑定来创建MVC Action的方法参数,例如: 常用的attributes方式[FromBody]
或FromQuery
。
同样,也可以将[FromServices]
属性应用于Action的方法参数,通过从依赖注入容器来创建这些参数。 此功能对某个服务只被单个Action方法所依赖时非常有用,可以避免将此服务注入到整个控制器中,其他Action方法并不依赖此服务。
例如,我们可以使用[FromServices]
来注入WeatherForecastController
:
[ ] |
通过这种方式,依赖注入验证器也是无法检查到的。程序运行是正常的,但是访问API时就会发生异常。
要规避这个问题,最简单的方式就是只使用构造函数注入,而不要使用[FromServices]
。
还有另外一种方式:通过IServiceProvider
直接解析。
陷阱3:通过IServiceProvider方式不会被依赖注入验证器检查
我们再来改写下WeatherForecastController
。这次不是直接注入WeatherForecastService
,我们先注入一个的IServiceProvider
,再用它直接解析一个服务(这是一种反模式)。
[ ] |
使用IServiceProvider
这种方式,并不是一个好主意。因为它没有明确表示控制器WeatherForecastController
对其他服务的依赖关系,隐藏了对WeatherForecastService
的依赖。除了开发人员难以理解之外,验证器也解析不了依赖关系。程序可以正常运行,但是访问API时就会发生异常。
不幸的是,在有些情况下是一定要用到IServiceProvider
的。 例如,有一个单例对象,该对象依赖scoped
服务,之前文章有讲过 。再或者有一个单例对象,该对象不能具有构造函数方式的依赖注入,例如验证attributes。 这些情况下验证器均无法验证。
当您使用工厂函数创建依赖项时,类似的陷阱就不太明显了。
陷阱4:通过工厂方式不会被依赖注入验证器检查
修改控制器,注入WeatherForecastService
到构造函数,并使用AddControllersAsServices()
方法。另外再做两个修改:
- 遗漏
DataService
的注入。 - 使用工厂创建
WeatherForecastService
对象。
说到工厂方式,是指在服务注册时使用lambda
表达式,返回需创建的服务实例。 例如:
public void ConfigureServices(IServiceCollection services) |
在上面的示例中,我们通过工厂方式使用lambda
表达式,创建了一个WeatherForecastService
,在lambda
内部,我们手动实例化了DataService
和WeatherForecastService
。
使用这种方式,应用程序不会出现任何问题,因为我们可以使用工厂方式从依赖注入容器中解析到了WeatherForecastService
。 我们手动创建了WeatherForecastService
需要析DataService
对象,而无需使用容器去解析它,因此程序没有问题。
再换一种写法:
public void ConfigureServices(IServiceCollection services) |
此种写法是通过IServiceProvider
在运行时解析DataService
服务,这属于隐式依赖。跟上面的陷阱3是一样的。验证器是无法验证的。
与之前的陷阱一样,有些情况下,这样的代码是必需的,而且没有简单的方法来解决它。 如果遇到这种情况,请格外小心,以确保您请求的依赖项已正确注入。
陷阱5:开放性的泛型不验证
最后一个小问题,ASP.NET Core 申明了对开放性的泛型不验证。
例如,假设我们有一个通用的ForcastService <T>
:
public class ForecastService<T> where T: new(); |
在Startup.cs
我们注入了的开放性的通用泛型,再次遗漏了注册DataService
:
public void ConfigureServices(IServiceCollection services) |
service provider完全跳过了开放性泛型注册验证,因此永远不会检测到丢失的DataService
依赖项。 应用程序运行时没有错误,但是在尝试请求ForecastService <T>
时将引发运行时异常。
但是,如果应用程序中使用约束性泛型,那么验证器将检测到该问题。 例如,我们可以使用类型WeatherForecast
约束泛型的T
,作为WeatherForecastController
的依赖项:
[ ] |
没问题,service provider验证器确实检测到了!实际上,使用开发性泛型注入的方式,不像使用IServiceProvider
或工厂方式方式那么重要。你可以采用约束性泛型注入来替代开放性泛型注入(除非该服务本身就是开放性的)。另外,如果使用IServiceProvider
来解析开放性泛型,恭喜你,又回到了陷阱3和陷阱4中了。
在其他环境中开启验证器
这是我所知道的最后一个陷阱,需要重点关注。默认情况下,service provider验证仅在开发环境中启用了。 因为开启此功能会有额外开销,与 scope validation 是相同。
但是,如果你的代码中有“条件服务注册”,例如,在Development环境中注册的服务与在其他环境中注册的服务不同,并且还希望在其他环境中也开启service provider验证。 可以在Program.cs代码中调用UseDefaultServiceProvider()
扩展方法实现。 在下面的示例中,我已在所有环境中启用ValidateOnBuild
,但仅在开发中保留了scope validation:
public class Program |
总结
在这篇文章中,我描述了.NET Core 3.0中新增的ValidateOnBuild
功能。 这使Microsoft.Extensions DI容器可以在首次编译service provider时就检查配置中的错误。 便于我们检测应用程序启动时的问题,而不是在运行时再暴露依赖注入的错误配置的问题。
虽然此功能很有用,但在一些情况下仍无法正常验证,例如,注入控制器、使用IServiceProvider
解析或使用开放性泛型注入等。 这个功能并不能百分之百的解决你程序的依赖注入问题,有些情况下需要你自己解决。