原文: Adding validation to strongly typed configuration objects in ASP.NET Core
做者: Andrew Lock
译文: Lamond Lu程序员
本篇博客中,我将描述如何在ASP.NET Core程序启动时,确保强类型配置对象正确的绑定成功。经过使用IStartupFilter
接口对象,你能够更早的验证你的配置对象是否绑定了正确的值,并不须要等待程序启动以后的某个时间点再验证。web
这里我将简单描述一下ASP.NET Core的配置系统,以及如何使用强类型配置。我将主要描述一下如何去除对IOptions
接口的依赖,而后我会描述一下强类型配置对象绑定不正确的问题。最后,我将给出一个在程序启动时验证强类型配置对象的方案。json
ASP.NET Core的配置系统很是的灵活,它容许你从多种数据源中读取配置信息,例如Json文件,YAML文件,环境变量,Azure Key Vault等。官方推荐方案是使用强类型配置来获取IConfiguration
接口对象的值。c#
强类型配置使用POCO
对象来呈现你的程序配置的一个子集,这与IConfiguration
接口对象存储的原始键值对不一样。例如,如今你正在你的程序中集成Slack, 而且使用Web hooks向频道中发送消息,你须要配置Web hook的URL, 以及一些其余的配置。api
public class SlackApiSettings { public string WebhookUrl { get; set; } public string DisplayName { get; set; } public bool ShouldNotify { get; set; } }
你能够在Startup
类中使用扩展方法Configure
,将强类型配置对象和你程序配置绑定起来。app
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
对象asp.net
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 }
我常常发现一些初级程序员会遇到这个问题,针对属性,他们只提供了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
。 当程序启动时,就会抛出如下异常。