首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 實體驗證演進實戰:從硬編碼 If 到 Fluent 鏈式驗證與錯誤收集器模式

     編輯:關於C#

實體驗證是業務邏輯核心。從最初的 if-else 堆疊,到利用 C# 3.0 擴充方法建構 Fluent 鏈式 API,再到引入驗證規則物件與錯誤收集器實現聲明式、可組合、可測試的驗證框架。本文記錄完整演進歷程與程式碼。

階段一:原始 If-Else 地獄


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

    public IEnumerable Validate()
    {
        var errors = new List();
        if (string.IsNullOrWhiteSpace(Username)) errors.Add("用戶名不可為空");
        else if (Username.Length < 3) errors.Add("用戶名至少 3 字元");
        if (string.IsNullOrWhiteSpace(Email)) errors.Add("Email 不可為空");
        else if (!Email.Contains("@")) errors.Add("Email 格式錯誤");
        if (Age < 18) errors.Add("年齡需滿 18 歲");
        return errors;
    }
}

缺點:驗證邏輯與實體耦合、難以複用、難以單元測試個別規則、新增規則需修改實體類別 (違反 OCP)。

階段二:Fluent 擴充方法鏈 (消除 If、但仍有缺陷)


public static class ValidationExtensions
{
    public static ValidationContext Validate(this T entity) => new ValidationContext(entity);
}

public class ValidationContext
{
    private readonly T _entity;
    private readonly List _errors = new();
    public ValidationContext(T entity) { _entity = entity; }

    public ValidationContext Check(Func predicate, string errorMessage)
    {
        if (!_errors.Any(e => e == errorMessage) && !predicate(_entity))
            _errors.Add(errorMessage);
        return this;
    }

    public IEnumerable GetErrors() => _errors;
}

// 使用
var errors = user.Validate()
    .Check(u => !string.IsNullOrWhiteSpace(u.Username), "用戶名不可為空")
    .Check(u => u.Username.Length >= 3, "用戶名至少 3 字元")
    .Check(u => u.Email.Contains("@"), "Email 格式錯誤")
    .GetErrors();

缺陷:第一個失敗即停止? 否,上述會全跑。但無法取消後續驗證、規則不可複用 (寫死在 Lambda)、錯誤訊息硬編碼、無法針對屬性分組錯誤 (前端欄位標紅需要知道哪個屬性錯)。

階段三:規則物件 + 錯誤收集器 (成熟模式)

1. 核心介面


public interface IValidationRule
{
    string PropertyName { get; }
    string ErrorMessage { get; }
    bool Validate(T entity);
}

public class ValidationResult
{
    public bool IsValid => !Errors.Any();
    public List Errors { get; } = new();
    public void AddError(string propertyName, string message) => Errors.Add(new ValidationError(propertyName, message));
}

public record ValidationError(string PropertyName, string Message);

2. 內建常用規則實作


public class RequiredRule : IValidationRule
{
    public string PropertyName { get; }
    public string ErrorMessage { get; }
    private readonly Func _selector;

    public RequiredRule(string propertyName, Func selector, string message = null)
    {
        PropertyName = propertyName;
        _selector = selector;
        ErrorMessage = message ?? $"{propertyName} 不可為空";
    }

    public bool Validate(T entity)
    {
        var value = _selector(entity);
        if (value == null) return false;
        if (value is string s && string.IsNullOrWhiteSpace(s)) return false;
        if (value is IEnumerable e && !e.Any()) return false;
        return true;
    }
}

public class RangeRule : IValidationRule where T : IComparable
{
    public string PropertyName { get; }
    public string ErrorMessage { get; }
    private readonly Func _selector;
    private readonly T _min, _max;

    public RangeRule(string propertyName, Func selector, T min, T max, string message = null)
    {
        PropertyName = propertyName; _selector = selector; _min = min; _max = max;
        ErrorMessage = message ?? $"{propertyName} 需在 {_min} ~ {_max} 之間";
    }

    public bool Validate(T entity)
    {
        var val = _selector(entity);
        return val.CompareTo(_min) >= 0 && val.CompareTo(_max) <= 0;
    }
}

// 更多:RegexRule, CustomRule, UniqueRule (需 DB)...


3. 實體掛載驗證器 (或領域服務)


public class UserValidator
{
    private readonly List> _rules = new()
    {
        new RequiredRule("Username", u => u.Username),
        new StringLengthRule("Username", u => u.Username, 3, 50),
        new RequiredRule("Email", u => u.Email),
        new RegexRule("Email", u => u.Email, @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"),
        new RangeRule("Age", u => u.Age, 18, 120)
    };

    public ValidationResult Validate(User user)
    {
        var result = new ValidationResult();
        foreach (var rule in _rules)
        {
            if (!rule.Validate(user))
                result.AddError(rule.PropertyName, rule.ErrorMessage);
        }
        return result;
    }
}

4. 使用與擴充


var validator = new UserValidator();
var result = validator.Validate(user);

if (!result.IsValid)
{
    // 綁定到前端 ModelState
    foreach (var err in result.Errors)
        ModelState.AddModelError(err.PropertyName, err.Message);
    
    // 或序列化回傳 API
    return BadRequest(new { errors = result.Errors.GroupBy(e => e.PropertyName)
        .ToDictionary(g => g.Key, g => g.Select(e => e.Message).ToArray()) });
}

進階:Fluent API 建構規則集 (最佳開發體驗)


public class ValidatorBuilder
{
    private readonly List> _rules = new();
    public ValidatorBuilder RuleFor(Expression> expr)
    {
        string propName = ((MemberExpression)expr.Body).Member.Name;
        return new RuleBuilder(this, propName, expr.Compile());
    }
    public IValidator Build() => new Validator(_rules);
}

public class RuleBuilder
{
    private readonly ValidatorBuilder _parent;
    private readonly string _propName;
    private readonly Func _selector;

    public RuleBuilder(ValidatorBuilder parent, string propName, Func selector)
    { _parent = parent; _propName = propName; _selector = selector; }

    public RuleBuilder Required(string msg = null)
    { _parent._rules.Add(new RequiredRule(_propName, _selector, msg)); return this; }
    public RuleBuilder Length(int min, int max, string msg = null)
    { _parent._rules.Add(new StringLengthRule(_propName, _selector, min, max, msg)); return this; }
    // ... Match, Range, Custom ...
}

// 使用:極度優雅
var validator = new ValidatorBuilder()
    .RuleFor(u => u.Username).Required().Length(3, 50)
    .RuleFor(u => u.Email).Required().Match(EmailRegex)
    .RuleFor(u => u.Age).Range(18, 120)
    .Build();

總結:演進關鍵點

階段核心改進剩餘問題
If-Else直觀耦合、難測試、違反 OCP
Fluent Extension消除 If、鏈式可讀規則不可複用、錯誤無屬性關聯、無短路控制
Rule Object + Collector關注點分離、可複用、可測試、屬性級錯誤、開放擴充樣板代碼較多 (可用 Builder/Source Generator 解決)
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved