2022/08/27

使用 FluentValidation 建立可重複利用的自訂驗證規則

當我們在進行表單填寫內容的格式或輸入值驗證時,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 驗證規則應該有所心得吧。這裡奉上實作後的畫面,希望上述的內容能幫助到跟我一樣卡關兩天的苦主同業朋友。


參考資料

  1. Using Fluent Validation in ASP.NET Core applications - Part 3 - Client side validation
    (此為經過 Google Translate 文章,原文為波斯文)
  2. Extending Client Side Validation with FluentValidation and jQuery Unobtrusive in an ASP.NET Core Application
  3. 第47章 表单验证之DataAnnotations与FluentValidation
  4. Announcement: Deprecation of RegisterValidator... methods within calls to AddFluentValidation #1963 
  5. Announcement: Changes to ASP.NET Integration registration methods (AddFluentValidation()) #1965 
     


沒有留言 :

張貼留言

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