如何為ASP.NET Core的強型別配置物件新增驗證
原文: Adding validation to strongly typed configuration objects in ASP.NET Core
作者: 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
。 當程式啟動時,就會丟擲以下異常。