原文: 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
。 當程序啓動時,就會拋出如下異常。