前言:在本文中,我將描述ASP.NET Core 3.0中新的“validate on build”功能。 這可以用來檢測您的DI service provider是否配置錯誤。 具體而言,該功能可檢測您對未在DI容器中注冊的服務的依賴關系。首先,我將展示該功能的工作原理,然后舉一些場景,在這些場景下,您可能會有一個配置錯誤的DI容器,而該功能不會被識別為有問題。
翻譯: Andrew Lock https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/
探索ASP.NET Core 3.0系列一:新的項目文件、Program.cs和generic host
探索ASP.Net Core 3.0系列二:聊聊ASP.Net Core 3.0 中的Startup.cs
探索ASP.Net Core 3.0系列四:在ASP.NET Core 3.0的應用中啟動時運行異步任務
探索 ASP.Net Core 3.0系列五:引入IHostLifetime並弄清Generic Host啟動交互
探索ASP.Net Core 3.0系列六:ASP.NET Core 3.0新特性啟動信息中的結構化日志
一、一個簡單的APP
在這篇文章中,我將使用基於默認dotnet new webapi模板的應用程序。 它由單個控制器WeatherForecastService組成,該控制器根據一些靜態數據返回隨機生成的數據。
為了稍微練習一下DI容器,我將提取一些服務。 首先,將控制器重構為:
[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private readonly WeatherForecastService _service; public WeatherForecastController(WeatherForecastService service) { _service = service; } [HttpGet] public IEnumerable<WeatherForecast> Get() { return _service.GetForecasts(); } }
因此,控制器依賴WeatherForecastService。 如下所示(我已經省略了實際的實現,因為它對這篇文章並不重要):
public class WeatherForecastService { private readonly DataService _dataService; public WeatherForecastService(DataService dataService) { _dataService = dataService; } public IEnumerable<WeatherForecast> GetForecasts() { var data = _dataService.GetData(); // use data to create forcasts return new List<WeatherForecast>{ new WeatherForecast { Date = DateTime.Now, TemperatureC = 31, Summary="Sweltering", } }; } }
此服務依賴於另一個DataService,如下所示:
public class DataService { public string[] GetData() => new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; }
這就是我們需要的所有服務,因此剩下的就是將它們注冊到DI容器中。
Startup.ConfigureServices
:
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddSingleton<WeatherForecastService>(); services.AddSingleton<DataService>(); }
在此示例中,我已將它們注冊為單例,但這對於此功能並不重要。 一切設置正確后,向/ WeatherForecast發送請求將返回對應的數據:
這里的一切看起來都很不錯,所以讓我們看看如果我們搞砸了DI注冊會發生什么。
二、在啟動時檢測未注冊的依賴項
讓我們修改一下代碼,然后“忘記”在DI容器中注冊DataService依賴項:
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddSingleton<WeatherForecastService>(); // services.AddSingleton<DataService>(); }
如果我們使用dotnet run再次運行該應用程序,則會出現異常,堆棧跟蹤,並且該應用程序無法啟動。 我已經截斷並格式化了以下結果:
此錯誤很清楚-“嘗試激活'TestApp.WeatherForecastService'時無法解析'TestApp.DataService'類型的服務”。 這是DI驗證功能,它應該有助於減少在應用程序正常運行期間發現的DI錯誤的數量。 它不如編譯時的錯誤有用,但這是DI容器提供的靈活性的代價。
如果我們忘記注冊WeatherForecastService怎么辦:
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); // services.AddSingleton<WeatherForecastService>(); services.AddSingleton<DataService>(); }
在這種情況下,該應用程序可以正常啟動!是不是很納悶!下面讓我們來看看這是怎么一回事,到底有哪些陷阱,了解了這些陷阱我們就可以在日常的開發中避免很多問題。
(1)不檢查控制器構造函數的依賴關系
驗證功能未解決此問題的原因是沒有使用DI容器創建控制器DefaultControllerActivator從DI容器中獲取控制器的依賴關系,而不是控制器本身。 因此,DI容器對控制器一無所知,因此無法檢查其依賴項是否已注冊。
幸運的是,有一種解決方法。 您可以更改控制器激活器,以便使用IMvcBuilder上的AddControllersAsServices()方法將控制器添加到DI容器中:
public void ConfigureServices(IServiceCollection services) { services.AddControllers() .AddControllersAsServices(); // Add the controllers to DI // services.AddSingleton<WeatherForecastService>(); services.AddSingleton<DataService>(); }
這將啟用ServiceBasedControllerActivator,並將控制器作為服務注冊到DI容器中。 如果我們現在運行應用程序,則驗證會檢測到應用程序啟動時缺少的控制器依賴性,並引發異常:
這似乎是一個方便的解決方案,但我不確定要權衡些什么,但這應該很好(畢竟這是受支持的方案)。但是,我們還沒有走出困境,因為構造函數注入並不是依賴項注入的唯一方法……
(2)不檢查[FromServices]注入的依賴項
在MVC actions中使用模型綁定來控制如何根據傳入請求使用[FromBody]和[FromQuery]等屬性來創建 action方法的參數。同樣,可以將[FromServices]屬性應用於操作方法參數,並通過從DI容器中獲取這些參數來創建。 如果您具有僅單個操作方法所需的依賴項,則此功能很有用。 無需將服務通過構造函數注入DI容器中(並因此為該控制器上的每個action創建服務),而是可以將其注入到特定action中。
例如,我們可以重寫WeatherForecastController以使用[FromServices]注入,如下所示:
[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { [HttpGet] public IEnumerable<WeatherForecast> Get( [FromServices] WeatherForecastService service) // injected using DI { return service.GetForecasts(); } }
顯然,這里沒有理由這樣做,但這很重要。 不幸的是,DI驗證將無法檢測到此未注冊服務的使用(不管你是否添加了AddControllersAsServices)。 該應用程序可以啟動,但是當您嘗試調用該操作時將拋出異常。
一種簡單的解決方案是在可能的情況下避免使用[FromServices]屬性,這應該不難實現,如果需要使用,您總是可以通過構造函數注入。
還有另外一種從DI容器中獲取服務的方法-使用服務位置。
(3)不檢查直接來自IServiceProvider的服務
讓我們再重寫一次WeatherForecastController。 我們將直接注入IServiceProvider,而不是直接注入WeatherForecastService,並使用服務位置反模式來檢索依賴關系。
using Microsoft.Extensions.DependencyInjection;
[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private readonly WeatherForecastService _service; public WeatherForecastController(IServiceProvider provider) { _service = provider.GetRequiredService<WeatherForecastService>(); } [HttpGet] public IEnumerable<WeatherForecast> Get() { return _service.GetForecasts(); } }
在您注入IServiceProvider的地方,像這樣的代碼通常不是一個好主意,這種寫法 除了使開發人員更難以推理之外,這還意味着DI驗證程序不了解依賴項。 因此,該應用程序可以正常啟動。
不幸的是,您不能總是避免利用IServiceProvider。 有一種情況:你有一個單例對象,該對象需要作用域的依賴項。 另一中情況:你有一個單例對象,該對象不能具有構造函數依賴性,例如驗證屬性。 不幸的是,這些情況是無法解決的。
(4)不檢查使用工廠功能注冊的服務
讓我們回到原始控制器,將WeatherForecastService注入到構造函數中,然后使用AddControllersAsServices()在DI容器中注冊控制器。 但是,我們將進行兩項更改:
- 忘記注冊DataService。
- 使用工廠函數創建WeatherForecastService。
說到工廠功能,是指在服務注冊時提供的lambda,它描述了如何創建服務。 例如:
public void ConfigureServices(IServiceCollection services) { services.AddControllers() .AddControllersAsServices(); services.AddSingleton<WeatherForecastService>(provider => { var dataService = new DataService(); return new WeatherForecastService(dataService); }); // services.AddSingleton<DataService>(); // not required }
在上面的示例中,我們為WeatherForecastService提供了一個lambda,其中描述了如何創建服務。 在lambda內部,我們手動構造DataService和WeatherForecastService。這不會在我們的應用程序中引起任何問題,因為我們能夠使用上述工廠方法從DI容器中獲取WeatherForecastService。 我們永遠不必直接從DI容器解析DataService。 我們僅在WeatherForecastService中需要它,並且我們正在手動構造它,因此沒有問題。
如果我們在工廠函數中使用注入的IServiceProvider提供程序,則會出現問題:
public void ConfigureServices(IServiceCollection services) { services.AddControllers() .AddControllersAsServices(); services.AddSingleton<WeatherForecastService>(provider => { var dataService = provider.GetRequiredService<DataService>(); return new WeatherForecastService(dataService); }); // services.AddSingleton<DataService>(); // Required! }
就DI驗證而言,此工廠功能與上一個功能完全相同,但實際上存在問題。 我們正在使用IServiceProvider在運行時使用服務定位器模式來解析DataService。 所以我們有一個隱式依賴。 這實際上與陷阱3相同-服務提供者驗證程序無法檢測直接從服務提供者獲取服務的情況。與以前的陷阱一樣,有時需要這樣的代碼,並且沒有輕松的方法來解決它。 如果是這種情況,請格外小心,以確保您請求的依賴項已正確注冊。
(5)不檢查開放的泛型類型
來看個例子,例如,假設我們有一個泛型 的ForcastService <T>,它可以生成多種類型。
public class ForecastService<T> where T: new() { private readonly DataService _dataService; public ForecastService(DataService dataService) { _dataService = dataService; } public IEnumerable<T> GetForecasts() { var data = _dataService.GetData(); // use data to create forcasts return new List<T>(); } }
在Startup.cs中,我們注冊了該泛型,但再次忘記注冊DataService:
public void ConfigureServices(IServiceCollection services) { services.AddControllers() AddControllersAsServices(); // register the open generic services.AddSingleton(typeof(ForecastService<>)); // services.AddSingleton<DataService>(); // should cause an error }
服務提供者驗證完全跳過了泛型注冊,因此它永遠不會檢測到丟失的DataService依賴項。 該應用程序啟動時沒有錯誤,並且在嘗試請求ForecastService <T>時將引發運行時異常。
但是,如果您在任何地方的應用程序中都使用了此依賴關系的封閉版本(這很有可能),那么驗證將檢測到該問題。 例如,我們可以通過以T作為WeatherForecast關閉泛型來更新WeatherForecastController以使用泛型服務:
[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private readonly ForecastService<WeatherForecast> _service; public WeatherForecastController(ForecastService<WeatherForecast> service) { _service = service; } [HttpGet] public IEnumerable<WeatherForecast> Get() { return _service.GetForecasts(); } }
服務提供者驗證確實會檢測到這一點! 因此,實際上,缺少開放的泛型測試可能不會像服務定位器和工廠功能陷阱那樣重要。 您總是需要關閉一個泛型以將其注入到服務中(除非該服務本身是一個開放的泛型),因此希望您可以選擇很多情況。 例外情況是,如果您要使用服務定位器IServiceProvider來獲取開放的泛型,那么無論如何,您實際上又回到了陷阱3和4!
三、在其他環境中啟用服務驗證
這是我所知道的最后一個陷阱,值得記住的是,默認情況下僅在開發環境中啟用了服務提供者驗證。 那是因為它有啟動成本,與scope 驗證相同。但是,如果您有任何類型的“條件服務注冊”,而在Development中注冊的服務與在其他環境中注冊的服務不同,則您可能還希望在其他環境中啟用驗證。 您可以通過在Program.cs中向默認主機生成器添加一個UseDefaultServiceProvider調用來實現。 在下面的示例中,我已在所有環境中啟用ValidateOnBuild,但僅在開發中保留了范圍驗證:
public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }) // Add a new service provider configuration .UseDefaultServiceProvider((context, options) => { options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); options.ValidateOnBuild = true; });
四、總結
在這篇文章中,我描述了.NET Core 3.0中新增的ValidateOnBuild功能。 這允許Microsoft.Extensions DI容器在首次構建服務提供程序時檢查服務配置中的錯誤。 這可用於檢測應用程序啟動時的問題,而不是在運行時檢測錯誤配置服務。盡管很有用,但在很多情況下無法進行驗證,例如,使用IServiceProvider服務定位器將其注入MVC控制器,以及泛型。 您可以解決其中的一些問題,但是即使您不能解決這些問題,也要牢記它們,並且不要依賴您的應用程序來解決100%的DI問題!
翻譯: Andrew Lock https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/
作者:郭崢
出處:http://www.cnblogs.com/runningsmallguo/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。