探索 ASP.Net Core 3.0系列三:ASP.Net Core 3.0中的Service provider validation

前言:在本文中,我將描述ASP.NET Core 3.0中新的「validate on build」功能。 這能夠用來檢測您的DI service provider是否配置錯誤。 具體而言,該功能可檢測您對未在DI容器中註冊的服務的依賴關係。首先,我將展現該功能的工做原理,而後舉一些場景,在這些場景下,您可能會有一個配置錯誤的DI容器,而該功能不會被識別爲有問題。html

 翻譯: Andrew Lock   https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/web

 

探索ASP.NET Core 3.0系列一:新的項目文件、Program.cs和generic hostapi

探索ASP.Net Core 3.0系列二:聊聊ASP.Net Core 3.0 中的Startup.cs異步

探索ASP.Net Core 3.0系列四:在ASP.NET Core 3.0的應用中啓動時運行異步任務ide

探索 ASP.Net Core 3.0系列五:引入IHostLifetime並弄清Generic Host啓動交互函數

探索ASP.Net Core 3.0系列六:ASP.NET Core 3.0新特性啓動信息中的結構化日誌測試

1、一個簡單的APP

在這篇文章中,我將使用基於默認dotnet new webapi模板的應用程序。 它由單個控制器WeatherForecastService組成,該控制器根據一些靜態數據返回隨機生成的數據。ui

爲了稍微練習一下DI容器,我將提取一些服務。 首先,將控制器重構爲:url

[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。 以下所示(我已經省略了實際的實現,由於它對這篇文章並不重要):spa

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註冊會發生什麼。

 

2、在啓動時檢測未註冊的依賴項

讓咱們修改一下代碼,而後「忘記」在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!

 

3、在其餘環境中啓用服務驗證

這是我所知道的最後一個陷阱,值得記住的是,默認狀況下僅在開發環境中啓用了服務提供者驗證。 那是由於它有啓動成本,與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;
            });

 

4、總結

在這篇文章中,我描述了.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/

本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文連接。

相關文章
相關標籤/搜索