ASP.NET Core MVC 自訂模型驗證 ValidationAttribute (使用資料庫內容)

在 ASP.NET Core MVC 中如果前端填完表單,要傳遞給後端並執行驗證通常會使用 ViewModel 傳遞,並在 ViewModel 上面增加一些基礎的驗證,例如年齡欄位的內容必須要是數字且不能是負數等。

預設的驗證 Attributes 可以查看官方說明文件,但若預設的驗證無法滿足需求,則可以自訂驗證屬性(Custom attributes)。 只要將自訂類別繼承 ValidationAttribute 並複寫 IsValid 方法即可,在這裡筆者撰寫一個自訂的日期驗證方法做示範
    
/// <summary>
/// 驗證是否為空或是正確的日期格式 yyyy/MM/dd
/// </summary>
public class CanNullDateAttribute : ValidationAttribute
{
    private static readonly Regex DateRegex = new(@"^\d{4,}/\d{2}/\d{2}$");

    private const string DateTimeFormatErrorMessage = "日期格式錯誤";
    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        string? input = value as string;
        if (string.IsNullOrWhiteSpace(input)) return ValidationResult.Success;
        if (DateRegex.IsMatch(input)) return ValidationResult.Success;

        return new ValidationResult(DateTimeFormatErrorMessage);
    }
}
    

在上面的範例中只要是空白或是符合 2022/11/11 這種格式的日期就會通過驗證,反之會顯示「日期格式錯誤」的訊息
註: 其實預設 DateTime 就可以吃這種日期格式了,這裡只是做個範例

在使用時也很簡單,只要在 ViewModel 中加上註解(Attribute)即可
    
public class MyViewModel
{
    [CanNullDate] public string? Date { get; set; }
}
    

那假設要驗證資料庫中的資料呢?例如在介面中選擇的使用者類型是否有效,需要去資料庫中搜尋才能確認資料是否有效。 其實上面的這種情況可以使用 [Remote] 屬性,使用前端驗證的方式檢查,不過如果就是想要後端驗證,並且想要不使用 ModelState.AddModelError 這樣不容易復用的手動驗證驗證方式可以嗎?

用 ValidationContext 應該也是有辦法達成,那該如何連接資料庫?是否要使用依賴注入的方式在此屬性「建立時」將 ApplicationDbContext 等資料庫連接物件或連接資訊傳入供這個屬性查詢?筆者在這裡一直思考很久,一直想不到有什麼方法,後來研究了一下 IsValid 方法內傳入的 ValidationContext 參數才發現其實不用這麼麻煩,可以直接從 ValidationContext 中找尋已啟用的服務即可
    
/// <summary>
/// 註冊頁面選擇使用者類型時檢查此類型是否包含在資料庫中
/// </summary>
public class UserTypeAttribute : ValidationAttribute
{
    private const string NullDataErrorMessage = "請選擇使用用者類型";
    private const string NotFindDataErrorMessage = "使用者類型錯誤";

    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        ILogger<UserTypeAttribute>? logger = validationContext.GetService<ILogger<UserTypeAttribute>>();
        logger?.LogDebug("UserTypeAttribute.IsValid");

        ApplicationDbContext? dbContext = validationContext.GetService<ApplicationDbContext>();
        if (dbContext == null)
        {
            logger?.LogError("UserTypeAttribute.IsValid: dbContext is null");
            return new ValidationResult("無法取得使用者服務");
        }

        string? userType = value as string;
        if (string.IsNullOrWhiteSpace(userType)) return new ValidationResult(NullDataErrorMessage);
        if (dbContext.UserTypes.Any(x => x.Id == userType)) return ValidationResult.Success;

        logger?.LogWarning("UserTypeAttribute.IsValid: userType: {UserType} is not found", userType);
        return new ValidationResult(NotFindDataErrorMessage);
    }
}
    



參考資料:
Microsoft.Learn - Model validation in ASP.NET Core MVC and Razor Pages

留言