为ASP.NET Core强类型配置对象添加验证的方法
前言
本篇博客中,我将描述如何在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
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。