前言:在本文中,我将描述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 = ,
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/

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

探索 ASP.Net Core 3.0系列三:ASP.Net Core 3.0中的Service provider validation的更多相关文章

  1. 学习ASP.NET Core Razor 编程系列四——Asp.Net Core Razor列表模板页面

    学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...

  2. C#中的函数式编程:递归与纯函数(二) 学习ASP.NET Core Razor 编程系列四——Asp.Net Core Razor列表模板页面

    C#中的函数式编程:递归与纯函数(二)   在序言中,我们提到函数式编程的两大特征:无副作用.函数是第一公民.现在,我们先来深入第一个特征:无副作用. 无副作用是通过引用透明(Referential ...

  3. 学习ASP.NET Core Razor 编程系列五——Asp.Net Core Razor新建模板页面

    学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...

  4. Microsoft Enterprise Library 5.0 系列(三)

    一.简介及用途 在实际的项目开发中,我们总会需要对数据进行验证,以保证数据的可靠性,而为了使这些验证可以在不同的地方进行复用(如winform.web.WPF等),就需要将验证进行封装,EntLib的 ...

  5. [渣译文] SignalR 2.0 系列: 开始使用SignalR 2.0

    原文:[渣译文] SignalR 2.0 系列: 开始使用SignalR 2.0 英文渣水平,大伙凑合着看吧…… 这是微软官方SignalR 2.0教程Getting Started with ASP ...

  6. 022年9月12日 学习ASP.NET Core Blazor编程系列三——实体

    学习ASP.NET Core Blazor编程系列一--综述 学习ASP.NET Core Blazor编程系列二--第一个Blazor应用程序(上) 学习ASP.NET Core Blazor编程系 ...

  7. 学习ASP.NET Core Razor 编程系列三——创建数据表及创建项目基本页面

    一.创建脚本工具并执行初始迁移 在本节中,您将使用包管理控制台(PMC)来更新数据库: •添加VisualStudio Web代码生成包.这个包是运行脚本引擎所必需的. • 执行Add-Migrati ...

  8. 《ASP.NET Core In Action》读书笔记系列三 ASP.NET Core如何处理请求的?

    在本节中,您将看到ASP.NET Core应用程序如何运行的,从请求URL开始到页面呈现在浏览器中. 为此,您将看到 一个HTTP请求在Web服务器中是如何被处理的.ASP.NET Core如何扩展该 ...

  9. .NET CORE学习笔记系列(5)——ASP.NET CORE的运行原理解析

    一.概述 在ASP.NET Core之前,ASP.NET Framework应用程序由IIS加载.Web应用程序的入口点由InetMgr.exe创建并调用托管,初始化过程中触发HttpApplicat ...

随机推荐

  1. mysql数据库的十种查询方式及多表查询

    --mysql数据库的十种查询方式 -- (1)查询时起别名 SELECT id AS '编号',NAME AS '姓名',age AS '年龄' FROM student; -- (2)查询时添加常 ...

  2. C# windows服务,解决应用程序开机自启问题

    最近在东营做一个超市购物的项目,业务体量很小,是仅供内部员工使用的内网应用程序,其中涉及一个商品数据同步的winform应用程序,有一个问题就是服务器重启后,必须登录服务器操作系统,手动启动才行,于是 ...

  3. createscope

    /// <summary> /// Creates a new <see cref="IServiceScope"/> that can be used t ...

  4. powershell 提取 spotlight 图片

    powershell脚本来源于网络,有一些调整. # 将复制出来的缓存图片保存在下面的文件夹 $dir = Split-Path -Parent $MyInvocation.MyCommand.Def ...

  5. 如何在GibHub上传自己的项目

    如何上传项目至GinHub 准备好项目.在项目ssm-crud的目录下右击,点击Git Bash Here,打开git命令行. 在命令行中,输入git init,使项目文件夹加入git管理: 输入gi ...

  6. ASP.NET Core框架深度学习(四)宿主对象

    11.WebHost  第六个对象 到目前为止我们已经知道了由一个服务器和多个中间件构成的管道是如何完整针对请求的监听.接收.处理和最终响应的,接下来来讨论这样的管道是如何被构建出来的.管道是在作为应 ...

  7. Linux下正确修改Docker镜像和容器的默认存储位置,亲测有效

    我们通过 yum 的方式安装完Docker环境后,它默认的存储位置是 /var/lib/docker,默认的 pid 存放位置是 /var/run/docker.pid. 如果仅仅是做测试,我们可能没 ...

  8. HTML常用标签二

    图像标签和路径 目录文件夹:普通的文件夹,里面存放了我们做页面需要的相关素材,比如html文件,图片等 根目录:打开目录文件夹的第一层就是根目录 路径 相对路径 以引用文件所在位置为参考基础,而建立出 ...

  9. ABAP动态自建表维护程序Dynamin Process

    以前经常会遇到批量上传或修改数据到自建表的需求,所以在想是否可以做一个动态的程序,所有的自建表都可以用这个动态程序来维护. 于是就打算试着写动态的程序. 程序的要求:动态显示自建表ALV 动态下载Ex ...

  10. App iCON 尺寸

    120*120  180*180 58*58  87*87 80*80  120*120