FluentValidation 筆記

用於 .NET5。Blazor App - Form Validation.

安裝套件

/// 應用於 Blazor App
Install-Package Blazored.FluentValidation

/// 核心模組
Install-Package FluentValidation

/// 可註冊全部的 IValidator<T> 服務
Install-Package FluentValidation.DependencyInjectionExtensions

解析(resolve)取得欄位名稱

Startup.cs
public class Startup
{
    using System.ComponentModel.DataAnnotations;
    
    public void ConfigureServices(IServiceCollection services)
    {
        ///# for 多國語系(optional, used to support IStringLocalizer)
        services.AddLocalization(options => options.ResourcesPath = "Resources");
        services.Configure<RequestLocalizationOptions>(options =>
        {
            var cultures = Configuration.GetSection("Cultures").GetChildren().ToDictionary(x => x.Key, x => x.Value);
            var supoortedCultures = cultures.Keys.ToArray();

            options.AddSupportedCultures(supoortedCultures);
            options.AddSupportedUICultures(supoortedCultures);
            options.SetDefaultCulture("zh-TW");
            options.DefaultRequestCulture = new Microsoft.AspNetCore.Localization.RequestCulture("zh-TW");
        });

        ///# for FluentValidator 註冊所有 IValidator<T>
        var exeAsm = Assembly.GetExecutingAssembly();
        services.AddValidatorsFromAssembly(exeAsm);

        services.AddRazorPages();
        ....
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        /// 設定自 DisplayAttribute 取得欄位名稱
        /// FluentValidation 的組態改到 Global 設定。
        FluentValidation.ValidatorOptions.Global.DisplayNameResolver = (type, member, expression) =>
            (member.GetCustomAttributes(typeof(DisplayAttribute), false).FirstOrDefault() as DisplayAttribute)?.GetName();

        if (env.IsDevelopment())
        ....
    }
}

Model範例

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using FluentValidation;

namespace BlazorServerApp.Data
{
    public class Customer
    {
        [Display(Name = "識別碼")]
        public int Id { get; set; }
        [Display(Name = "姓氏")]
        public string Surname { get; set; }
        [Display(Name = "名字")]
        public string Forename { get; set; }
        [Display(Name = "折扣")]
        public decimal Discount { get; set; }
        [Display(Name = "住址")]
        public string Address { get; set; }
    }

    public class CustomerValidator : AbstractValidator<Customer>
    {
        public CustomerValidator()
        {
            RuleFor(c => c.Id).NotNull();

            // .WithMessage("姓氏就是不可以是空的!");
            RuleFor(c => c.Surname).NotEmpty();
            
            RuleFor(c => c.Forename).NotEmpty();
            
            RuleFor(c => c.Address).NotEmpty().MinimumLength(10);
            
            RuleFor(c => c.Address).EmailAddress();

            // .WithMessage("當姓氏為money時折扣值需小於500哦。")
            RuleFor(c => c.Discount).LessThan(500)
                .When(SurnameIsMoney);

            RuleFor(c => c.Discount).InclusiveBetween(1000, 9000)
                .Unless(SurnameIsMoney);

            When(SurnameIsMoney, () =>
            {
                RuleFor(c => c.Discount).LessThan(500).WithMessage("當姓氏為money,值需小於500哦。");
                RuleFor(c => c.Discount).LessThan(500).WithMessage("當姓氏為money,值需小於500哦。");
                RuleFor(c => c.Discount).LessThan(500).WithMessage("當姓氏為money,值需小於500哦。");
            });

            RuleFor(c => c.Discount)
                .LessThan(500)
                .DependentRules(() =>
                {
                    RuleFor(c => c.Surname).Equal("money");
                });

            RuleForEach(c => c.itemList)....
            
            // 補充 on 2023/12/01:
            // 上班時間:必填且需符合時間格式。HH:mm
            RuleFor(m => m.OnWorkTime)
              .NotEmpty()
              .Must(text => TimeSpan.TryParse(text, out TimeSpan result))
              .WithMessage($"'上班時間' 時間格式不對。");

            // 下班時間:空白或需符合時間格式。HH:mm
            RuleFor(m => m.OffWorkTime)
              .Must(offWorkTime => TimeSpan.TryParse(offWorkTime, out TimeSpan result))
              .When(m => !string.IsNullOrWhiteSpace(m.OffWorkTime))
              .WithMessage("'下班時間' 時間格式不對。")
              .GreaterThan(m => m.OnWorkTime)
              .When(m => m.OverNight != "Y") // 當沒有跨夜下班。
              .WithMessage("'下班時間' 需大於 '上班時間'。");
        }

        bool SurnameIsMoney(Customer c) => c.Surname == "money";
    }
}

應用範例

@using BlazorServerApp.Data
@using Serilog
@using Blazored.FluentValidation
@page "/fluent-validation-lab"

<h2>Fluent Validation Lab</h2>

<EditForm Model="formData" OnValidSubmit="@SubmitForm">
    @* <DataAnnotationsValidator /> *@
    <FluentValidationValidator />
    <ValidationSummary />

    <p>
        <Label For="()=>formData.Id" />
        <InputNumber TValue="int" @bind-Value="formData.Id" />
        <ValidationMessage For="()=>formData.Id" />
    </p>

    <p>
        <Label For="()=>formData.Surname" />
        <InputText @bind-Value="formData.Surname" />
        <ValidationMessage For="()=>formData.Surname" />
    </p>

    <p>
        <Label For="()=>formData.Forename" />
        <InputText @bind-Value="formData.Forename" />
        <ValidationMessage For="()=>formData.Forename" />
    </p>

    <p>
        <Label For="()=>formData.Discount" />
        <InputNumber TValue="decimal" @bind-Value="formData.Discount" />
        <ValidationMessage For="()=>formData.Discount" />
    </p>

    <p>
        <Label For="()=>formData.Address" />
        <InputTextArea @bind-Value="formData.Address" />
        <ValidationMessage For="()=>formData.Address" />
    </p>

    <button type="submit" class="btn btn-primary">送出</button>
    <button type="button" class="btn btn-secondary" @onclick="HandlePostForm">手動檢查</button>
</EditForm>

@code{
    Customer formData = new Customer();
    CustomerValidator validator = new CustomerValidator(); // 用於手動檢查
    // 預設EditForm會自動整合 CustomerValidator,這裡是了手動再檢查一次。
        
    private void SubmitForm()
    {
        // 自動檢查通過才會觸發此函式
        //... proceed...
        Log.Information("SubmitForm {@formData}", formData);
    }
        
    private void HandlePostForm()
    {
        // 手動再檢查一次  
        var vr = validator.Validate(formData);
        if (!vr.IsValid)
        {
            foreach (var failure in vr.Errors)
            {
                Console.WriteLine("Property " + failure.PropertyName + " failed validation. Error was: " + failure.ErrorMessage);
            }
        }

        //... proceed...
        Log.Information("HandlePostForm {@formData}", formData);
    }
}

專業應用:Dependency Injection & Localization

基本的應用不需要 DI 注入服務,有全球化(在地方)的應用就需要注入才能取得多國語言的資源。

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using FluentValidation;
using FluentValidation.Results;
using Microsoft.Extensions.Localization;

namespace BlazorServerApp.Data
{
    public class Person
    {
        [Display(Name = "序號")]
        public int Sn { get; set; }
        [Display(Name = "姓氏")]
        public string Surname { get; set; }
        [Display(Name = "名字")]
        public string Forename { get; set; }
    }

    public class PersonValidator : AbstractValidator<Person>
    {
        public PersonValidator(IStringLocalizer<Person> localizer)
        {
            // 透過 IStringLocalizer 介面取得多國語言資源
            RuleFor(x => x.Surname)
                .NotNull()
                .WithMessage(x => localizer["Surname is required"]);
            
            // 透過 IStringLocalizer 介面取得多國語言資源    
            RuleFor(x => x.Forename)
                .NotNull()
                .WithMessage(x => localizer["Forename is required"]);
        }
    }
}

沒圖沒真象之 FluentValidation 透過IStringLocalizer介面取得多國語言資源的設定紀錄。

FluentValidation 透過 DI 取得實作IValidator<T>介面的 PersonValidator 服務。

PersonForm.razor
@using BlazorServerApp.Data
@using Blazored.FluentValidation
@using FluentValidation
@page "/fluent-validation-lab"

<h2>Fluent Validation Lab</h2>

<EditForm Model="personData" OnValidSubmit="@SubmitPersonForm">
    <FluentValidationValidator />
    <ValidationSummary />

    <p>
        <Label For="() => personData.Surname" />
        <InputText @bind-Value="personData.Surname" />
        <ValidationMessage For="() => personData.Surname" />
    </p>

    <p>
        <Label For="() => personData.Forename" />
        <InputText @bind-Value="personData.Forename" />
        <ValidationMessage For="() => personData.Forename" />
    </p>

    <button type="submit" class="btn btn-primary">送出</button>
    <button type="button" @onclick="HandlePostPersonForm" class="btn btn-primary">手動檢查</button>
</EditForm>
<pre>@personVrJson</pre>

@code{
    Person personData = new Person();
    [Inject] IValidator<Person> personValidator { get; set; }
    // 透過DI注入服務

    string personVrJson = "";

    private void SubmitPersonForm()
    {
		    // 自動檢查通過
    }

    private void HandlePostPersonForm()
    {
		    // 手動檢查
        var vr = personValidator.Validate(personData);
        personVrJson = Utils.JsonSerialize(vrP);
    }
}

Last updated