视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
为ASP.NET Core强类型配置对象添加验证的方法
2020-11-27 22:34:40 责编:小采
文档


前言

本篇博客中,我将描述如何在ASP.NET Core程序启动时,确保强类型配置对象正确的绑定成功。通过使用IStartupFilter接口对象,你可以更早的验证你的配置对象是否绑定了正确的值,并不需要等待程序启动之后的某个时间点再验证。

这里我将简单描述一下ASP.NET Core的配置系统,以及如何使用强类型配置。我将主要描述一下如何去除对IOptions接口的依赖,然后我会描述一下强类型配置对象绑定不正确的问题。最后,我将给出一个在程序启动时验证强类型配置对象的方案。

ASP.NET Core中的强类型配置

ASP.NET Core的配置系统非常的灵活,它允许你从多种数据源中读取配置信息,例如Json文件,YAML文件,环境变量,Azure Key Vault等。官方推荐方案是使用强类型配置来获取IConfiguration接口对象的值。

强类型配置使用POCO对象来呈现你的程序配置的一个子集,这与IConfiguration接口对象存储的原始键值对不同。例如,现在你正在你的程序中集成Slack, 并且使用Web hooks向频道中发送消息,你需要配置Web hook的URL, 以及一些其他的配置。

public class SlackApiSettings
{
 public string WebhookUrl { get; set; }
 public string DisplayName { get; set; }
 public bool ShouldNotify { get; set; }
}

你可以在Startup类中使用扩展方法Configure,将强类型配置对象和你程序配置绑定起来。

public class Startup
{
 public Startup(IConfiguration configuration) 
 {
 Configuration = configuration;
 }

 public IConfiguration Configuration { get; }

 public void ConfigureServices(IServiceCollection services)
 {
 services.AddMvc();
 services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi")); 
 }

 public void Configure(IApplicationBuilder app)
 {
 app.UseMvc();
 }
}

当你需要读取配置的时候,你只需要在你当前方法所在类的构造函数中注入一个IOptions接口对象,即可使用这个对象的Value属性,获取到配置的值, 这里ASP.NET Core配置系统自动帮你完成了强类型对象和配置之间的绑定。

public class TestController : Controller
{
 private readonly SlackApiSettings _slackApiSettings;
 public TestController(IOptions<SlackApiSettings> options)
 {
 _slackApiSettings = options.Value
 }

 public object Get()
 {
 return _slackApiSettings;
 }
}

解除对IOptions接口的依赖

可能有些人和我一样,不太喜欢让自己创建的类依赖于IOptions接口,我们只希望自己创建的类仅依赖于配置对象。这里你可以使用如下所述的方法来解除对IOptions接口的依赖。这里我们可以在依赖注入容器中显式的注册一个SlackApiSetting配置对象,并将解析它的方法委托给一个IOptions对象

public void ConfigureServices(IServiceCollection services)
{
 services.AddMvc();

 services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi")); 

 services.AddSingleton(resolver => 
 resolver.GetRequiredService<IOptions<SlackApiSettings>>().Value);
}

现在你可以在不引用Microsoft.Extensions.Options程序集的情况下,注入了一个“原始”的配置对象了。

public class TestController : Controller
{
 private readonly SlackApiSettings _slackApiSettings;

 public TestController(SlackApiSettings settings)
 {
 _slackApiSettings = settings;
 }

 public object Get()
 {
 return _slackApiSettings;
 }
}

这个解决方案通常都非常有效, 但是如果配置出现问题,例如在JSON文件中出现了错误拼写,这里会发生什么事情呢?

如果绑定失败,程序会发生什么事情?

我们绑定强类型配置对象的时候有以下几种错误的可能。

节点名称拼写错误

当你绑定配置的时候,你需要显式的指定绑定的配置节点名称,如果你当前使用的appsetting.json作为配置文件,json文件中的key即是配置的节点名称。例如下面代码中的"Logging"和“SlackApi”

{
 "Logging": {
 "LogLevel": {
 "Default": "Warning"
 }
 },
 "AllowedHosts": "*",
 "SlackApi": {
 "WebhookUrl": "http://example.com/test/url",
 "DisplayName": "My fancy bot",
 "ShouldNotify": true
 }
}

为了绑定"SlackApi"节点的值到强类型配置对象SlackApiSetting, 你需要调用一下代码

services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi")); 

这时候,假设我们将appsettings.json中的"SlackApi"错误的拼写为"SackApi"。现在我们去调用前面例子中的TestController中的GET方法,会得到一下结果

{
 "webhookUrl":null,
 "displayName":null,
 "shouldNotify":false
}

所有的key都是绑定了他们的默认值,但是没有发生任何错误,这意味着他们绑定到了一个空的配置节点上。这看起来非常糟糕,因为你的代码并没有验证webhookUrl是否是一个合法的Url。

属性名拼写错误

相似的,有时候拼写的节点名称正确,但是属性名称可能拼写错误。例如, 我们将appSettings.json文件中的"WebhookUrl"错误的拼写为"Url"。这时我们调用前面例子中的TestController中的GET方法,会得到以下结果

{
 "webhookUrl":null,
 "displayName":"My fancy bot",
 "shouldNotify":true
}

强类型配置类的属性缺少SET访问器

我经常发现一些初级程序员会遇到这个问题,针对属性,他们只提供了GET访问器,而缺少SET访问器,在这种情况下强类型配置对象是不会正确绑定的。

public class SlackApiSettings
{
 public string WebhookUrl { get; }
 public string DisplayName { get; }
 public bool ShouldNotify { get; }
}

现在我们去调用前面例子中的TestController中的GET方法,会得到以下结果

{
 "webhookUrl":null,
 "displayName":null,
 "shouldNotify":false
}

不兼容的类型值

最后一种情况就是将一个不兼容的类型值,绑定到属性上。在配置文件中,所有的配置都是以文本形式保存的,但是绑定器需要将他们转换成.NET中支持的基础类型。例如ShouldNotify属性是一个布尔类型的值,我们只能将"True", "False"字符串绑定到这个值上,但是如果你在配置文件中,设置该属性的值为"THE VALUE", 当程序访问TestController时,程序就会报错

使用IStartupFilter创建一个配置验证

为了解决这个问题,我将使用IStartupFilter创建一个在应用启动时运行的简单验证步骤,以确保你的设置正确无误。

IStartupFilter接口允许你通过向依赖注入容器添加服务来间接控制中间件管道。 ASP.NET Core框架使用它来执行诸如“将IIS中间件添加到应用程序的中间件管道的开头, 或添加诊断中间件之类”的操作。

虽然IStartupFilter经常用来向管道中添加中间件,但是我们也可以不这么做。相反的,我们可以在程序启动时(服务配置完成之后,处理请求之前),使用它来执行一些简单的代码。

这里首先我们创建一个简单的接口,强类型配置类可以通过实现这个接口来完成一些必要的验证。

public interface IValidatable
{
 void Validate();
}

下一步,我们创建一个SettingValidationStartupFilter类, 它实现了IStartupFilter接口

public class SettingValidationStartupFilter : IStartupFilter
{
 readonly IEnumerable<IValidatable> _validatableObjects;
 public SettingValidationStartupFilter(IEnumerable<IValidatable> validatableObjects)
 {
 _validatableObjects = validatableObjects;
 }

 public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
 {
 foreach (var validatableObject in _validatableObjects)
 {
 validatableObject.Validate();
 }

 return next;
 }
}

在构造函数中,我们从依赖注入容器中取出了所有实现IValidatable接口的强类型配置对象,并在Configure方法中依次调用他们的Validate方法。

SettingValidationStartupFilter并没有修改任何中间件管道, Configure方法中直接返回了next对象。但是如果某个强类型配置类的验证失败,在程序启动时,就会抛出异常,从而阻止了程序。

接下来我们需要在Startup类中注册我们创建的服务SettingValidationStartupFilter

public void ConfigureServices(IServiceCollection services) 
{
 services.AddTransient<IStartupFilter, SettingValidationStartupFilter>()
 // 其他配置
}

最后你需要让你的配置类实现IValidatable接口, 我们以SlackApiSettings为例,这里我们需要验证WebhoolUrl和DisplayName属性是否绑定成功,并且我们还需要验证 WebhoolUrl是否是一个合法的Url。

public class SlackApiSettings : IValidatable
{
 public string WebhookUrl { get; set; }
 public string DisplayName { get; set; }
 public bool ShouldNotify { get; set; }

 public void Validate()
 {
 if (string.IsNullOrEmpty(WebhookUrl))
 {
 throw new Exception("SlackApiSettings.WebhookUrl must not be null or empty");
 }

 if (string.IsNullOrEmpty(DisplayName))
 {
 throw new Exception("SlackApiSettings.WebhookUrl must not be null or empty");
 }

 // 如果不是合法的Url,就会抛出异常
 var uri = new Uri(WebhookUrl);
 }
}

当然我们还可以使用DataAnnotationsAttribute来实现上述验证。

public class SlackApiSettings : IValidatable
{
 [Required, Url]
 public string WebhookUrl { get; set; }
 [Required]
 public string DisplayName { get; set; }
 public bool ShouldNotify { get; set; }

 public void Validate()
 {
 Validator.ValidateObject(this, 
 new ValidationContext(this), 
 validateAllProperties: true);
 }
}

无论你使用哪一种方式,如果绑定出现问题,程序启动时都会抛出异常。

最后一步,我们需要将SlackApiSettings 以IValidatable接口的形式注册到依赖注入容器中,这里我们同样可以使用前文的方法解除对IOptions接口的依赖。

public void ConfigureServices(IServiceCollection services) 
{
 services.AddMvc();

 services.AddTransient<IStartupFilter, SettingValidationStartupFilter>()

 services.Configure<SlackApiSettings>(Configuration.GetSection("SlackApi")); 

 services.AddSingleton(resolver => 
 resolver.GetRequiredService<IOptions<SlackApiSettings>>().Value);

 services.AddSingleton<IValidatable>(resolver => 
 resolver.GetRequiredService<IOptions<SlackApiSettings>>().Value);
}

测试结果

我们可以任选之前列举的一个错误方式来进行测试,例如,我们将WebhookUrl错误的拼写为Url。 当程序启动时,就会抛出以下异常。

原文: Adding validation to strongly typed configuration objects in ASP.NET Core

作者: Andrew Lock

译文: Lamond Lu

总结

下载本文
显示全文
专题