探索 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/

本文版权归做者和博客园共有,欢迎转载,但未经做者赞成必须保留此段声明,且在文章页面明显位置给出原文连接。

相关文章
相关标签/搜索