Stories

Detail Return Return

C#.NET FluentValidation 全面解析:優雅實現對象驗證 - Stories Detail

簡介

  • FluentValidation 是一個基於“流式 API”(Fluent API)的 .NET 驗證框架,用於在應用層對模型(DTO、ViewModel、Entity 等)進行聲明式驗證。
  • 核心優勢:

    • 高可讀性:通過鏈式方法配置驗證規則,邏輯清晰;
    • 可複用:將驗證代碼從業務邏輯中分離,易於單元測試;
    • 豐富的內置規則:郵箱、長度、正則、多字段聯動、集合驗證等;
    • 可擴展:支持自定義驗證器、異步驗證、跨屬性驗證。
  • 適用場景:

    • Web API 模型驗證
    • 複雜業務規則驗證
    • 需要高度可定製驗證邏輯的系統
    • 多語言驗證消息需求
    • 需要測試覆蓋的驗證邏輯

安裝與基礎配置

  • NuGet
Install-Package FluentValidation
Install-Package FluentValidation.DependencyInjectionExtensions
  • 引用命名空間
using FluentValidation;

核心用法

定義 Model 與 Validator

public class UserDto
{
    public string Username { get; set; }
    public string Email    { get; set; }
    public int    Age      { get; set; }
}

public class UserDtoValidator : AbstractValidator<UserDto>
{
    public UserDtoValidator()
    {
        // NotEmpty / NotNull
        RuleFor(x => x.Username)
            .NotEmpty().WithMessage("用户名不能為空")
            .Length(3, 20).WithMessage("用户名長度須在3到20之間");

        // Email 格式
        RuleFor(x => x.Email)
            .NotEmpty().WithMessage("郵箱不能為空")
            .EmailAddress().WithMessage("郵箱格式不正確");

        // 數值範圍
        RuleFor(x => x.Age)
            .InclusiveBetween(18, 120)
            .WithMessage("年齡須在18到120之間");
    }
}

執行驗證

var user = new UserDto { Username = "", Email = "bad", Age = 10 };
var validator = new UserDtoValidator();
var result = validator.Validate(user);

if (!result.IsValid)
{
    foreach (var failure in result.Errors)
    {
        Console.WriteLine($"{failure.PropertyName}: {failure.ErrorMessage}");
    }
}

// ASP.NET Core 自動驗證
[HttpPost]
public IActionResult CreateUser([FromBody] UserDto user)
{
    // 模型綁定後自動驗證
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    // ...
}

常用驗證規則

方法 作用
NotNull() / NotEmpty() 非空或非空串
Length(min, max) 字符串長度範圍
EmailAddress() 郵箱格式
Matches(regex) 正則匹配
InclusiveBetween(min, max) 數值範圍
GreaterThan(x) / LessThan(x) 大小比較
Must(predicate) 自定義同步條件
MustAsync(asyncPredicate) 自定義異步條件

進階特性

跨屬性驗證

RuleFor(x => x.EndDate)
    .GreaterThan(x => x.StartDate)
    .WithMessage("結束時間必須晚於開始時間");

條件驗證

RuleFor(x => x.Password)
    .NotEmpty().When(x => x.RequirePassword)
    .WithMessage("密碼不能為空");

集合與嵌套對象

public class OrderDto { public List<OrderItemDto> Items { get; set; } }
public class OrderItemDto { public int Quantity { get; set; } }

public class OrderDtoValidator : AbstractValidator<OrderDto>
{
    public OrderDtoValidator()
    {
        RuleForEach(x => x.Items)
            .ChildRules(items =>
            {
                items.RuleFor(i => i.Quantity)
                     .GreaterThan(0).WithMessage("數量須大於0");
            });
    }
}

異步驗證

RuleFor(x => x.Username)
    .MustAsync(async (name, ct) => !await userRepo.ExistsAsync(name))
    .WithMessage("用户名已存在");

級聯驗證

使用 CascadeMode.Stop 提高性能

RuleFor(x => x.Name).Cascade(CascadeMode.Stop).NotEmpty().MaximumLength(50);

驗證規則組織

規則集(RuleSets)

public class UserValidator : AbstractValidator<UserDto>
{
    public UserValidator()
    {
        // 公共規則
        RuleFor(user => user.Name).NotEmpty();
        
        // 創建規則集
        RuleSet("Admin", () => 
        {
            RuleFor(user => user.IsAdmin).Must(b => b == true)
                .WithMessage("管理員用户必須設置管理員標誌");
        });

        RuleSet("PaymentInfo", () => {
            RuleFor(c => c.CreditCardNumber).NotEmpty();
            RuleFor(c => c.CreditCardExpiry).NotEmpty();
        });
    }
}

// 使用指定規則集
var result = validator.Validate(user, options => 
{
    options.IncludeRuleSets("Admin", "PaymentInfo");
});

繼承與組合

// 基礎驗證器
public class PersonValidator : AbstractValidator<PersonDto>
{
    public PersonValidator()
    {
        RuleFor(p => p.Name).NotEmpty();
        RuleFor(p => p.BirthDate).LessThan(DateTime.Now);
    }
}

// 繼承擴展
public class EmployeeValidator : PersonValidator
{
    public EmployeeValidator()
    {
        Include(new PersonValidator()); // 包含基礎規則
        RuleFor(e => e.EmployeeId).NotEmpty();
        RuleFor(e => e.Department).NotEmpty();
    }
}

// 組合驗證
public class AdvancedUserValidator : AbstractValidator<UserDto>
{
    public AdvancedUserValidator()
    {
        Include(new UserValidator());
        RuleFor(u => u.SecurityLevel).InclusiveBetween(1, 5);
    }
}

自定義擴展

自定義驗證器

public static class CustomValidators
{
    public static IRuleBuilderOptions<T, string> ValidIdCard<T>(this IRuleBuilder<T, string> ruleBuilder)
    {
        return ruleBuilder.Must(id => Regex.IsMatch(id, @"^[1-9]\d{16}[0-9X]$"))
                          .WithMessage("身份證格式不正確");
    }
}

// 使用
RuleFor(x => x.IdCard).ValidIdCard();

自定義屬性比較器

public class DateRangeValidator : PropertyValidator
{
    public DateRangeValidator() : base("{PropertyName} 時間範圍不合法") { }

    protected override bool IsValid(PropertyValidatorContext context)
    {
        var dto = (MyDto)context.InstanceToValidate;
        return dto.End > dto.Start;
    }
}

// 在 Validator 中
RuleFor(x => x.Start).SetValidator(new DateRangeValidator());

ASP.NET Core 集成

註冊服務(Program.cs)

builder.Services
    .AddControllers()
    .AddFluentValidation(cfg =>
    {
        // 自動註冊當前程序集所有繼承 AbstractValidator 的類型
        cfg.RegisterValidatorsFromAssemblyContaining<Startup>();
        // 禁用 DataAnnotations 驗證(可選)
        cfg.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
    });

自動觸發

  • ASP.NET Core 在模型綁定後會自動調用對應 Validator,並將錯誤添加到 ModelState
  • Controller 中可直接檢查 if (!ModelState.IsValid) 或依賴 [ApiController] 的自動返回行為。

自定義錯誤響應

// 配置全局異常處理
services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var errors = context.ModelState
            .Where(e => e.Value.Errors.Count > 0)
            .ToDictionary(
                kvp => kvp.Key,
                kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()
            );
        
        return new BadRequestObjectResult(new
        {
            Code = 400,
            Message = "請求驗證失敗",
            Errors = errors
        });
    };
});

最佳實踐

  • 分層組織規則:

    • 針對同一模型,可拆分多個 Validator(或使用 Include()),保持單一職責。
  • 複用規則集:

    • 對於常見字段(如郵箱、手機號),可定義公共規則並通過擴展方法重用。
  • 錯誤消息國際化:

    • 將消息文本放入資源文件,使用 .WithMessage(x => Resources.FieldRequired)
  • 性能考慮:

    • 異步驗證會序列化執行,若有多個異步規則,可合併或避免不必要的數據庫調用。
  • 測試驗證器:

    • 為每個 Validator 編寫單元測試,覆蓋正常和邊界情況,確保規則生效。
  • 日誌與監控:

    • 在全局捕獲驗證失敗日誌,統計常見錯誤,優化用户體驗。

資源與擴展

  • GitHub:https://github.com/FluentValidation/FluentValidation
  • 文檔:https://docs.fluentvalidation.net
  • NuGet 包:

    • FluentValidation:核心驗證庫。
    • FluentValidation.DependencyInjectionExtensionsASP.NET Core 集成。
  • 擴展:

    • 支持 Blazor、MVCWeb API
    • 提供多語言支持和客户端驗證。
user avatar sysin Avatar seth9shi Avatar invalidnull Avatar
Favorites 3 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.