當我們在進行表單填寫內容的格式或輸入值驗證時,ASP.NET / ASP.NET Core MVC 框架內建的模型驗證功能可說非常方便。但隨著生態圈的變化,現在使用 ASP.NET Core 開發的情境越來越多。雖說是承襲過去 .NET Framework 的 MVC 5,但實際進行開發時仍會發現後進者與先進相比仍有些許的差異。譬如在多語系的 Web API 內實體化一個模型,並在沒有 POST 的動作下進行驗證時,若 Model 內包含子模型,子模型的驗證失敗訊息無法多語系化,最後會得到一堆字詞的語系代碼。後來經由網路前輩們的建議,改用 FluentValidation 即解決了這個問題。
FluentValidation 完美實現驗證邏輯抽離的情境,我們再也不用在 Model 內各屬性上方設定一堆 DataAnnotations 規則,不但可針對各種模型的驗證規則進行統一維護,Model 的定義上也不會因為制定太多規則而顯得雜亂。不過由於 FluentValidation 是伺服器端的驗證,雖然有提供基礎的前端驗證
data-val-*屬性綁定,但驗證規則若包含
When()或
Must()等內建條件式規則或自己開發的可重複利用性的複雜式驗證規則,前端的驗證屬性綁定就得自己來了。
由於 FluentValidation 10 至 11 有許多破壞性更新,而 11.1 至 11.2 光是整合至 ASP.NET 的註冊方式又有些許的異動,加上網路上查了許久的教學範例研判都是版本 9 之前的版本,光是驗證規則使用的 API 語法與當今最新的 11.2 版本就有很大的差異。在官方範例及文件乏善可陳且諸多異動都藏在 GitHub Issues 內的情況下,經過多方研究與嘗試,好不容易終於完成了適用於 11.2 版本的實作(累~),所以以下就來記錄一下完整的開發範例,造福一下跟我遇到相同狀況的迷途羔羊。
範例情境
現在 Model 內有 Length 與 LengthLimit 兩個屬性。有效的驗證規則為 Length 的填入值必須小於 LengthLimit 的輸入值。由於 Length 的最大值需自 LengthLimit 取當下的值,所以無法使用內建的 LessThan() 規則。這時,我們必須撰寫一個名為 LowerThanValidator 的伺服器及客戶端的自訂驗證規則。
前置作業
1. 安裝套件
首先,至 nuget 安裝 11.2 以上版本的 FluentValidation.AspNetCore 套件,或直接於套件主控管理器執行:
Install-Package FluentValidation.AspNetCore -Version 11.2.1
2. 接著進行 Model 定義
// Model.cs
/// <summary>範例用模型</summary>
/// <remarks>驗證邏輯參閱 <see cref="LengthModelValidator"/> 設定</remarks>
public class LengthModel
{
/// <summary>識別碼</summary>
[Key]
public long Key { get; set; }
/// <summary>標題</summary>
public string Title { get; set; } = null!;
/// <summary>長度</summary>
public int Length { get; set; } = 0;
/// <summary>長度填寫最大限制值</summary>
public int LengthLimit { get; set; } = 100;
}
Server 端
完成前置作業後,來開發伺服器端的自訂驗證規則。
1. 先制定介面以方便在後續進行 ASP.NET Core 註冊自訂客戶端驗證時使用
// Validator.cs
/// <summary>驗證:數值須小於同個模型中指定屬性的值</summary>
public interface ILowerThanValidator : IPropertyValidator
{
/// <summary>同模型中,與之比較的指定屬性的名稱</summary>
public string DependentProperty { get; }
}
2. 接著開始開發伺服器端的自訂驗證器
// Validator.cs
/// <summary><see cref="ILowerThanValidator"/> 實作</summary>
public class LowerThanValidator<T> : PropertyValidator<T, int>, ILowerThanValidator
{
public string DependentProperty { get; }
public LowerThanValidator(string dependentProperty)
{
DependentProperty = dependentProperty;
}
//建立驗證器名稱
public override string Name => "LowerThanValidator";
//建立驗證規則邏輯
public override bool IsValid(ValidationContext<T> context, int value)
{
//取得指定屬性的設定值
int dependentPropertyValue = context.InstanceToValidate.GetValue(DependentProperty, 0);
//替換錯誤訊息字串中 {PropertyName}、{LessThanValue} 的顯示文字
context.MessageFormatter.AppendArgument("PropertyName", DependentProperty);
context.MessageFormatter.AppendArgument("LessThanValue", dependentPropertyValue);
return value < dependentPropertyValue;
}
//建立預設的錯誤訊息
//若驗證規則設定時未使用 .WithMessage("...") 設定錯誤訊息,將回傳本方法預設的錯誤訊息
protected override string GetDefaultMessageTemplate(string errorCode)
=> "The value of {PropertyName} must less than the value of {DependentProperty}.";
}
3. 以自訂的驗證器建立驗證規則 "LowerThan()",以方便後續屬性驗證設定時將規則套用
// Validator.cs
/// <summary>自訂的驗證規則</summary>
public static class CustomFluentValidationExtensions
{
/// <summary>
/// 數值須小於同個模型中指定屬性的值
/// </summary>
/// <typeparam name="T">泛型類型實例</typeparam>
/// <param name="ruleBuilder">驗證規則生成器介面實體</param>
/// <param name="dependentProperty">同模型中,與之比較的指定屬性的名稱</param>
/// <returns>驗證規則生成器介面實體 (IRuleBuilder<T, TProperty>)</returns>
public static IRuleBuilderOptions<T, int> LowerThan<T>(
this IRuleBuilder<T, int> ruleBuilder, string dependentProperty)
{
return ruleBuilder.SetValidator(new LowerThanValidator<T>(dependentProperty));
}
}
4. 建立 Model 的驗證規則,並將自訂的驗證規則 LowerThan() 套用至 Length 屬性
/// <summary>範例用模型的驗證定義</summary> public class LengthModelValidator : AbstractValidator<LengthModel> { public LengthModelValidator(CascadeMode cascadeMode = CascadeMode.Continue) { ValidatorOptions.Global.DefaultClassLevelCascadeMode = cascadeMode; int title_maxlength = 50; RuleFor(x => x.Title) .NotNull().WithMessage("必填") .NotEmpty().WithMessage("必填") .MaximumLength(title_maxlength).WithMessage(string.Format("最大長度為 {0} 個字元", title_maxlength)) ; RuleFor(x => x.Length) .NotNull().WithMessage("需填入數字") .NotNull().WithMessage("需填入數字") //套用自訂的驗證規則 "LowerThan()", //並指定 Length 的值須與 LengthLimit 的值進行比較 .LowerThan(nameof(LengthModel.LengthLimit)).WithMessage("error_msg"); } }
Client 端
由於 FluentValidation 會自動將綁定驗證規則的屬性在產出 HTML 時建立 "data-val-*" 綁定屬性,所以我們必須額外替自訂的伺服器端驗證器加入客戶端驗證器。在本例中,預期應該產出以下 data-val-* 的 HTML 內容:
<input
id="txt_length"
data-val="true"
data-val-lowerthan="The value of Length must less than the value of LengthLimit."
data-val-lowerthan-dependentproperty="LengthLimit"
data-val-required="需填入數字"
......
/>
那麼,開始開發客戶端驗證器。
1. 建立客戶端驗證器
// Validator.cs
/// <summary>客戶端驗證:數值須小於同個模型中指定屬性的值</summary>
public class LowerThanClientValidator : ClientValidatorBase
{
ILowerThanValidator validator => (ILowerThanValidator)Validator;
public LowerThanClientValidator(IValidationRule rule, IRuleComponent component) : base(rule, component) { }
//在此建立要綁定在 HTML 上的驗證屬性,供 JavaScript 進行驗證綁定
public override void AddValidation(ClientModelValidationContext context)
{
MergeAttribute(context.Attributes, "data-val", "true");
//設定錯誤訊息
MergeAttribute(context.Attributes, "data-val-LowerThan", GetErrorMessage(context));
//所需相依的屬性名稱。
//設定項目依伺服器端所需傳入的參數為依據
MergeAttribute(context.Attributes, "data-val-LowerThan-dependentproperty", validator.DependentProperty);
}
//取得錯誤訊息
private string GetErrorMessage(ClientModelValidationContext context)
{
//自伺服器端驗證實體中,取得預設的錯誤訊息,並處理須替代的參數值
var cfg = context.ActionContext.HttpContext.RequestServices.GetRequiredService<ValidatorConfiguration>();
var formatter = cfg.MessageFormatterFactory()
.AppendPropertyName(Rule.GetDisplayName(null))
.AppendArgument("PropertyName", context.ModelMetadata.DisplayName)
.AppendArgument("DependentProperty", validator.DependentProperty);
string message;
try
{
message = Component.GetUnformattedErrorMessage();
}
catch (NullReferenceException)
{
message = "設定值有問題";
}
return formatter.BuildMessage(message);
}
}
2. 完成後,建立客戶端 JavaScript 驗證規則擴充
目前最常用的客戶端驗證為 jQuery.Validate.Unobtrusive,時至距今,在前端界日漸擺脫 jQuery 依賴趨勢下,已有另一套不依賴 jQuery、針對 ASP.NET Client 端驗證需求開發,且完全可作為 jQuery.Validate.Unobtrusive 替代的 aspnet-client-validation 可使用。以下將實作兩種 Client 端自訂驗證擴充。
使用 aspnet-client-validation(推薦):
// CustomValidation.js
<script src="~/lib/aspnet-client-validation/dist/aspnet-validation.min.js"></script>
<script type="text/javascript">
//建立驗證實體
var v = new aspnetValidation.ValidationService();
//新增 LowerThan() 的客戶端驗證擴充
v.addProvider('lowerthan', (value, element, params) => {
if (!value || isNaN(value)) {
return true;
}
var dependentProperty = 'txt' + params.dependentproperty;
var dependentControl = document.getElementById(dependentProperty);
if (dependentControl) {
var targetvalue = dependentControl.value;
if (!isNaN(targetvalue) && parseInt(targetvalue) > parseInt(value)) {
return true;
}
return false;
}
return true;
});
v.bootstrap
<script>
使用 jQuery.Validate.Unobtrusive:
// CustomValidation.js
//須載入 jQuery.js、jquery.validate.js 及 jquery.validate.unobtrusive.js
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
<script type="text/javascript">
//擴充客製驗證
$.validator.unobtrusive.adapters.add('LowerThan', ['dependentproperty'], function (options) {
options.rules['LowerThan'] = {
dependentproperty: options.params['dependentproperty']
};
options.messages['LowerThan'] = options.message;
});
$.validator.addMethod('LowerThan', function (value, element, parameters) {
var dependentProperty = '#txt' + parameters['dependentproperty'];
var dependentControl = $(dependentProperty);
if (dependentControl) {
var targetvalue = dependentControl.val();
if (parseInt(targetvalue) > parseInt(value)) {
return true;
}
return false;
}
return true;
});
</script>
ASP.NET Core 註冊
完成伺服器端及客戶端的自訂驗證器開發後,接著要將其註冊至 ASP.NET Core 才能使用。11.2 最大的變化,在於原先官方建議的 service.AddFluentValidation() 方法已添加「已取代」標籤,這意外著將來的版本會將這個已被替代的方法剃除。為了日後維護穩定性,將以官方建議的方法進行註冊,並提供舊版方式供參考。
建議的註冊方法:
// Program.cs
//註冊 FluentValidation
builder.Services
//將組建中所有的伺服器端驗證規則自動進行註冊
.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly())
//FluentValidation 11.2 官方建議的註冊方法
.AddFluentValidationAutoValidation()
.AddFluentValidationClientsideAdapters(cfg =>
{
//註冊自訂的 LowerThanValidator 客戶端驗證
cfg.ClientValidatorFactories.Add(typeof(ILowerThanValidator),
(context, rule, component) => new LowerThanClientValidator(rule, component));
});
舊版註冊方法:
// Program.cs
//註冊 FluentValidation (以下為即將被淘汰的註冊方法,不建議使用)
builder.Services
//已被 services.AddFluentValidationAutoValidation().AddFluentValidationClientsideAdapters 替代
.AddFluentValidation(cfg =>
{
//將組建中所有的伺服器端驗證規則自動進行註冊
//(RegisterValidatorsFromAssembly 已被 services.AddValidatorsFromAssembly() 替代)
cfg.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
//註冊自訂的客戶端驗證規則
//(ConfigureClientsideValidation 已被 services.AddFluentValidationClientsideAdapters() 替代)
cfg.ConfigureClientsideValidation(cs =>
{
//註冊自訂的 LowerThanValidator 客戶端驗證
cs.ClientValidatorFactories.Add(typeof(ILowerThanValidator),
(context, rule, component) => new LowerThanClientValidator(rule, component));
});
});
完成上述設定後,相信對於自訂擴充 FluentValidation 驗證規則應該有所心得吧。這裡奉上實作後的畫面,希望上述的內容能幫助到跟我一樣卡關兩天的苦主同業朋友。
參考資料
- Using Fluent Validation in ASP.NET Core applications - Part 3 - Client side validation
(此為經過 Google Translate 文章,原文為波斯文) - Extending Client Side Validation with FluentValidation and jQuery Unobtrusive in an ASP.NET Core Application
- 第47章 表单验证之DataAnnotations与FluentValidation
- Announcement: Deprecation of RegisterValidator... methods within calls to AddFluentValidation #1963
- Announcement: Changes to ASP.NET Integration registration methods (AddFluentValidation()) #1965

沒有留言 :
張貼留言
注意:只有此網誌的成員可以留言。