作者: Andrew Lock
译文: Lamond Lu
本篇博客中, 我将描述如何在 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. 当程序启动时, 就会抛出以下异常.