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

时间:2021-07-25 06:05:13

前言:在本文中,我将描述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发送请求将返回对应的数据:

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

这里的一切看起来都很不错,所以让我们看看如果我们搞砸了DI注册会发生什么。

二、在启动时检测未注册的依赖项

让我们修改一下代码,然后“忘记”在DI容器中注册DataService依赖项:

public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<WeatherForecastService>();
// services.AddSingleton<DataService>();
}

如果我们使用dotnet run再次运行该应用程序,则会出现异常,堆栈跟踪,并且该应用程序无法启动。 我已经截断并格式化了以下结果:

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

此错误很清楚-“尝试激活'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容器中。 如果我们现在运行应用程序,则验证会检测到应用程序启动时缺少的控制器依赖性,并引发异常:

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

这似乎是一个方便的解决方案,但我不确定要权衡些什么,但这应该很好(毕竟这是受支持的方案)。但是,我们还没有走出困境,因为构造函数注入并不是依赖项注入的唯一方法……

(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/

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