1. 程式人生 > >ASP.NET Core MVC 中的模型驗證

ASP.NET Core MVC 中的模型驗證

資料模型的驗證被視為是資料合法性的第一步,要求滿足型別、長度、校驗等規則,有了MVC的模型校驗能夠省卻很多前後端程式碼,為程式碼的簡潔性也做出了不少貢獻。

原文地址:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/validation?view=aspnetcore-2.1

作者:Rachel Appel

模型驗證簡介

在將資料儲存到資料庫之前,應用必須先驗證資料。 必須檢查資料是否存在潛在的安全威脅,確保資料已設定適當的型別和大小格式,並且必須符合相關規則。 實施驗證的過程可能有些單調乏味,但卻必不可少。 在 MVC 中,驗證發生在客戶端和伺服器上。

幸運的是,.NET 已將驗證抽象化為驗證屬性。 這些屬性包含驗證程式碼,從而減少了所需編寫的程式碼量。

在 ASP.NET Core 2.2 及更高版本中,如果能夠確定給定模型關係圖不需要進行驗證,ASP.NET Core 執行時便會簡化(跳過)驗證。 驗證無法或沒有關聯任何驗證程式的模型時,跳過驗證可能會顯著提升效能。 已跳過的驗證包括諸如基元集合(byte[]string[]Dictionary<string, string> 等)之類的物件,或沒有任何驗證程式的複雜物件關係圖。

檢視或下載 GitHub 中的示例

驗證屬性

驗證屬性用於配置模型驗證,因此,在概念上類似於資料庫表中欄位上的驗證。 它包括諸如分配資料型別或必填欄位之類的約束。 其他型別的驗證包括將向資料應用模式以強制實施業務規則,比如信用卡、電話號碼或電子郵件地址。 驗證屬性更易使用,並使這些要求的實施變得更簡單。

驗證特性在屬性級別指定:

C#

[Required]
public string MyProperty { get; set; } 

下面是一個應用的已批註 Movie 模型,該應用用於儲存電影和電視節目的相關資訊。 大多數屬性都是必需屬性,多個字串屬性具有長度要求。 此外,還有一個針對·Price 屬性設定的從 0 到 $999.99 的數值範圍限制,以及一個自定義驗證特性。

C#

public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; }

    [ClassicMovie(1960)]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Required]
    [StringLength(1000)]
    public string Description { get; set; }

    [Range(0, 999.99)]
    public decimal Price { get; set; }

    [Required]
    public Genre Genre { get; set; }

    public bool Preorder { get; set; }
}

通過讀取整個模型即可顯示有關此應用的資料的規則,從而使程式碼維護變得更輕鬆。 下面是幾個常用的內建驗證屬性:

  • [CreditCard]:驗證屬性是否具有信用卡格式。

  • [Compare]:驗證某個模型中的兩個屬性是否匹配。

  • [EmailAddress]:驗證屬性是否具有電子郵件格式。

  • [Phone]:驗證屬性是否具有電話格式。

  • [Range]:驗證屬性值是否落在給定範圍內。

  • [RegularExpression]:驗證資料是否與指定的正則表示式匹配。

  • [Required]:將屬性設定為必需屬性。

  • [StringLength]:驗證字串屬性是否最多具有給定的最大長度。

  • [Url]:驗證屬性是否具有 URL 格式。

MVC 支援從 ValidationAttribute 派生的所有用於驗證的屬性。 在 System.ComponentModel.DataAnnotations 名稱空間中可找到許多有用的驗證屬性。

在某些情況下,內建屬性可能無法提供所需的功能。 這時,就可以通過從 ValidationAttribute 派生或將模型更改為實現 IValidatableObject,來建立自定義驗證屬性。

必需屬性的使用說明

從本質上來說,需要不可以為 null 的值型別(如 decimalintfloatDateTime),但不需要 Required 特性。 應用不會對標記為 Required 的不可為 null 的型別執行任何伺服器端驗證檢查。

對於不可為 null 的型別,MVC 模型繫結(與驗證和驗證屬性無關)會拒絕包含缺失值或空白的表單域提交。 如果目標屬性上缺少 BindRequired 特性,模型繫結會忽略不可為 null 的型別的缺失資料,導致傳入表單資料中缺少表單域。

BindRequired 特性(另請參閱 ASP.NET Core 中的模型繫結)可用於確保表單資料完整。 當應用於某個屬性時,模型繫結系統要求該屬性具有值。 當應用於某個型別時,模型繫結系統要求該型別的所有屬性都具有值。

使用 Nullable<T> 型別(例如,decimal?System.Nullable<decimal>)並將其標記為 Required 時,將執行伺服器端驗證檢查,就像該屬性是標準的可以為 null 的型別(例如,string)一樣。

客戶端驗證要求與標記為 Required 的模型屬性對應的表單域以及未標記為 Required 的不可為 null 的型別屬性具有值。 Required 可用於控制客戶端驗證錯誤訊息。

模型狀態

模型狀態表示已提交的 HTML 表單值中的驗證錯誤。

MVC 將繼續驗證欄位,直至達到錯誤數上限(預設為 200 個)。 可以使用 Startup.ConfigureServices 中的以下程式碼配置該數字:

C#

services.AddMvc(options => options.MaxModelValidationErrors = 50);

處理模型狀態錯誤

在執行控制器操作之前進行模型驗證。 該操作負責檢查 ModelState.IsValid 並做出相應響應。 在許多情況下,正確的反應是返回錯誤響應,理想狀況下會詳細說明模型驗證失敗的原因。

如果在使用 [ApiController] 屬性的 web API 控制器中,ModelState.IsValid 的計算結果為 false,將返回包含問題詳細資訊的自動 HTTP 400 響應。 有關詳細資訊,請參閱自動 HTTP 400 響應

某些應用會選擇遵循標準約定來處理模型驗證錯誤,在這種情況下,可以在篩選器中實現此類策略。 應測試操作在有效模型狀態和無效模型狀態下的行為方式。

手動驗證

完成模型繫結和驗證後,可能需要重複其中的某些步驟。 例如,使用者可能在應輸入整數的欄位中輸入了文字,或者你可能需要計算模型的某個屬性的值。

你可能需要手動執行驗證。 為此,請呼叫 TryValidateModel 方法,如下所示:

C#

TryValidateModel(movie);

自定義驗證

驗證屬性適用於大多數驗證需求。 但是,某些驗證規則特定於你的業務。 你的規則可能不是常見的資料驗證技術,比如確保欄位是必填欄位或符合一系列值。 在這些情況下,自定義驗證屬性是一種不錯的解決方案。 在 MVC 中建立你自己的自定義驗證屬性很簡單。 只需從 ValidationAttribute 繼承並重寫 IsValid 方法。 IsValid 方法採用兩個引數,第一個是名為 value 的物件,第二個是名為 validationContextValidationContext 物件。 Value 引用自定義驗證程式要驗證的欄位中的實際值。

在下面的示例中,一項業務規則規定,使用者不能將 1960 年以後發行的電影的流派設定為 Classic[ClassicMovie] 屬性會先檢查流派,如果是經典流派,則檢視發行日期是否晚於 1960 年。 如果晚於 1960 年,則驗證失敗。 此屬性採用一個表示年份的整數引數,可用於驗證資料。 可以在該屬性的建構函式中捕獲該引數的值,如下所示:

C#

public class ClassicMovieAttribute : ValidationAttribute, IClientModelValidator
{
    private int _year;

    public ClassicMovieAttribute(int year)
    {
        _year = year;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        Movie movie = (Movie)validationContext.ObjectInstance;

        if (movie.Genre == Genre.Classic && movie.ReleaseDate.Year > _year)
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }

上面的 movie 變量表示一個 Movie 物件,其中包含要驗證的表單提交中的資料。 在此例中,驗證程式碼會根據規則檢查 ClassicMovieAttribute 類的 IsValid 方法中的日期和流派。 驗證成功時,IsValid 返回 ValidationResult.Success 程式碼。 驗證失敗時,返回 ValidationResult 和錯誤訊息:

C#

private string GetErrorMessage()
{
    return $"Classic movies must have a release year earlier than {_year}.";
}

當用戶修改 Genre 欄位並提交表單時,ClassicMovieAttributeIsValid 方法將驗證該電影是否為經典電影。 將 ClassicMovieAttribute 像所有內建特性一樣應用於屬性(如 ReleaseDate)以確保執行驗證,如前面的程式碼示例所示。 由於此示例僅適用於 Movie 型別,因此建議使用 IValidatableObject,如下一段中所示。

也可以通過實現 IValidatableObject 介面上的 Validate 方法,將這段程式碼直接放入模型中。 如果自定義驗證特性可用於驗證各個屬性,則可使用 IValidatableObject 來實現類級別的驗證,如下所示。

C#

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
    if (Genre == Genre.Classic && ReleaseDate.Year > _classicYear)
    {
        yield return new ValidationResult(
            $"Classic movies must have a release year earlier than {_classicYear}.",
            new[] { "ReleaseDate" });
    }
}

客戶端驗證

客戶端驗證極大地方便了使用者。 它節省了時間,讓使用者不必浪費時間等待伺服器往返。 從商業角度而言,即使每次只有幾分之一秒,但如果每天有幾百次,也會耗費大量的時間和成本,帶來很多不必要的煩惱。 簡單直接的驗證能夠提高使用者的工作效率和投入產出比。

你必須有一個包含適當的 JavaScript 指令碼引用的檢視,才能讓客戶端驗證正常工作,如下所示。

CSHTML

<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.0.min.js"></script>

CSHTML

<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.16.0/jquery.validate.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrusive.min.js"></script>

jQuery 非介入式驗證指令碼是一個基於熱門 jQuery Validate 外掛的自定義 Microsoft 前端庫。 如果沒有 jQuery 非介入式驗證,則必須在兩個位置編碼相同的驗證邏輯:一次是在模型屬性上的伺服器端驗證特性中,一次是在客戶端指令碼中(jQuery Validate 的 validate() 方法示例展示了這種情況可能的複雜程度)。 MVC 的標記幫助程式HTML 幫助程式則能夠使用模型屬性中的驗證特性和型別元資料,呈現需要驗證的表單元素中的 HTML 5 data- 特性。 MVC 為內建屬性和自定義屬性生成 data- 屬性。 然後,jQuery 非介入式驗證分析 data- 屬性並將邏輯傳遞給 jQuery Validate,從而將伺服器端驗證邏輯有效地“複製”到客戶端。 可以使用相關標記幫助程式在客戶端上顯示驗證錯誤,如下所示:

CSHTML

<div class="form-group">
    <label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
    <div class="col-md-10">
        <input asp-for="ReleaseDate" class="form-control" />
        <span asp-validation-for="ReleaseDate" class="text-danger"></span>
    </div>
</div>

上面的標記幫助程式將呈現以下 HTML。 請注意,HTML 輸出中的 data- 特性與 ReleaseDate 屬性的驗證特性相對應。 下面的 data-val-required 屬性包含在使用者未填寫發行日期欄位時將顯示的錯誤訊息。 jQuery 非介入式驗證將此值傳遞給 jQuery Validate required() 方法,該方法隨後在隨附的 <span> 元素中顯示該訊息。

HTML

<form action="/Movies/Create" method="post">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <div class="text-danger"></div>
        <div class="form-group">
            <label class="col-md-2 control-label" for="ReleaseDate">ReleaseDate</label>
            <div class="col-md-10">
                <input class="form-control" type="datetime"
                data-val="true" data-val-required="The ReleaseDate field is required."
                id="ReleaseDate" name="ReleaseDate" value="" />
                <span class="text-danger field-validation-valid"
                data-valmsg-for="ReleaseDate" data-valmsg-replace="true"></span>
            </div>
        </div>
    </div>
</form>

客戶端驗證將阻止提交,直到表單變為有效為止。 “提交”按鈕執行 JavaScript:要麼提交表單要麼顯示錯誤訊息。

MVC 基於屬性的 .NET 資料型別確定型別特性值(有可能使用 [DataType] 特性進行重寫)。 [DataType] 基本特性不執行真正的伺服器端驗證。 瀏覽器選擇自己的錯誤訊息,並根據需要顯示這些錯誤,但 jQuery 非介入式驗證包可以重寫訊息,並使它們與其他訊息的顯示保持一致。 當用戶應用 [DataType] 子類(比如 [EmailAddress])時,最常發生這種情況。

向動態表單新增驗證

由於 jQuery 非介入式驗證會在第一次載入頁面時將驗證邏輯和引數傳遞到 jQuery Validate,因此,動態生成的表單不會自動展示驗證。 你必須指示 jQuery 非介入式驗證在建立動態表單後立即對其進行分析。 例如,下面的程式碼展示如何對通過 AJAX 新增的表單設定客戶端驗證。

JavaScript

$.get({
    url: "https://url/that/returns/a/form",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add form. " + errorThrown);
    },
    success: function(newFormHTML) {
        var container = document.getElementById("form-container");
        container.insertAdjacentHTML("beforeend", newFormHTML);
        var forms = container.getElementsByTagName("form");
        var newForm = forms[forms.length - 1];
        $.validator.unobtrusive.parse(newForm);
    }
})

$.validator.unobtrusive.parse() 方法採用 jQuery 選擇器作為它的一個引數。 此方法指示 jQuery 非介入式驗證分析該選擇器內表單的 data- 屬性。 這些屬性的值隨後傳遞到 jQuery Validate 外掛中,以便表單展示所需的客戶端驗證規則。

向動態控制元件新增驗證

也可以在動態生成各個控制元件(比如 <input/><select/>)時,更新表單上的驗證規則。 不能將用於這些元素的選擇器直接傳遞到 parse() 方法,因為周圍表單已進行分析並且不會更新。 應當先刪除現有的驗證資料,然後重新分析整個表單,如下所示:

JavaScript

$.get({
    url: "https://url/that/returns/a/control",
    dataType: "html",
    error: function(jqXHR, textStatus, errorThrown) {
        alert(textStatus + ": Couldn't add control. " + errorThrown);
    },
    success: function(newInputHTML) {
        var form = document.getElementById("my-form");
        form.insertAdjacentHTML("beforeend", newInputHTML);
        $(form).removeData("validator")    // Added by jQuery Validate
               .removeData("unobtrusiveValidation");   // Added by jQuery Unobtrusive Validation
        $.validator.unobtrusive.parse(form);
    }
})

IClientModelValidator

可為自定義屬性建立客戶端邏輯,建立 jQuery 驗證的介面卡的非介入式驗證將在驗證過程中,在客戶端上自動為你執行此邏輯。 第一步是通過實現 IClientModelValidator 介面來控制要新增哪些 data- 屬性,如下所示:

C#

public void AddValidation(ClientModelValidationContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    MergeAttribute(context.Attributes, "data-val", "true");
    MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());

    var year = _year.ToString(CultureInfo.InvariantCulture);
    MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
}

實現此介面的屬性可以將 HTML 屬性新增到生成的欄位。 檢查 ReleaseDate 元素的輸出時,將顯示與上一示例類似的 HTML,唯一不同的是,此示例包含一個已在 IClientModelValidatorAddValidation 方法中定義的 data-val-classicmovie 屬性。

HTML

<input class="form-control" type="datetime"
    data-val="true"
    data-val-classicmovie="Classic movies must have a release year earlier than 1960."
    data-val-classicmovie-year="1960"
    data-val-required="The ReleaseDate field is required."
    id="ReleaseDate" name="ReleaseDate" value="" />

非介入式驗證使用 data- 屬性中的資料來顯示錯誤訊息。 不過,除非將規則或訊息新增到 jQuery 的 validator 物件,否則 jQuery 並不知道它們的存在。 如以下示例所示,將一個自定義 classicmovie 客戶端驗證方法新增到 validator 物件。 有關 unobtrusive.adapters.add 方法的說明,請參閱 ASP.NET MVC 中的非介入式客戶端驗證

JavaScript

$.validator.addMethod('classicmovie',
    function (value, element, params) {
        // Get element value. Classic genre has value '0'.
        var genre = $(params[0]).val(),
            year = params[1],
            date = new Date(value);
        if (genre && genre.length > 0 && genre[0] === '0') {
            // Since this is a classic movie, invalid if release date is after given year.
            return date.getFullYear() <= year;
        }

        return true;
    });

$.validator.unobtrusive.adapters.add('classicmovie',
    ['year'],
    function (options) {
        var element = $(options.form).find('select#Genre')[0];
        options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
        options.messages['classicmovie'] = options.message;
    });

classicmovie 方法使用前面的程式碼對電影發行日期執行客戶端驗證。 如果該方法返回 false,則顯示錯誤訊息。

遠端驗證

遠端驗證是一項非常不錯的功能,可在需要根據伺服器上的資料驗證客戶端上的資料時使用。 例如,應用可能需要驗證某個電子郵件或使用者名稱是否已被使用,並且它必須為此查詢大量資料。 為驗證一個或幾個欄位而下載大量資料會佔用過多資源。 它還有可能暴露敏感資訊。 一種替代方法是發出往返請求來驗證欄位。

可以分兩步實現遠端驗證。 首先,必須使用 [Remote] 屬性為模型新增批註。 [Remote] 屬性採用多個過載,可用於將客戶端 JavaScript 定向到要呼叫的相應程式碼。 下面的示例指向 Users 控制器的 VerifyEmail 操作方法。

C#

[Remote(action: "VerifyEmail", controller: "Users")]
public string Email { get; set; }

第二步是按照 [Remote] 屬性中的定義,將驗證程式碼放入相應的操作方法。 根據 jQuery Validate remote 方法文件,伺服器響應必須是符合以下條件的 JSON 字串:

  • 對於有效元素,為 "true"
  • 對於無效元素,為 "false"undefinednull,使用預設錯誤訊息。

如果伺服器響應是一個字串(例如,"That name is already taken, try peter123 instead"),則該字串顯示為一條自定義錯誤訊息來替代預設字串。

VerifyEmail 方法的定義遵循這些規則,如下所示。 如果電子郵件已被佔用,它會返回驗證錯誤訊息;如果電子郵件可用,則返回 true,並將結果包裝在 JsonResult 物件中。 然後,客戶端可以使用返回的值,繼續進行下一步操作或根據需要顯示錯誤。

C#

[AcceptVerbs("Get", "Post")]
public IActionResult VerifyEmail(string email)
{
    if (!_userRepository.VerifyEmail(email))
    {
        return Json($"Email {email} is already in use.");
    }

    return Json(true);
}

現在,當用戶輸入電子郵件時,檢視中的 JavaScript 會發出遠端呼叫,以瞭解該電子郵件是否已被佔用,如果是,則顯示錯誤訊息。 如果不是,使用者就可以像往常一樣提交表單。

[Remote] 特性的 AdditionalFields 屬性可用於根據伺服器上的資料驗證欄位組合。 例如,如果上面的 User 模型具有兩個附加屬性,名為 FirstNameLastName,你可能想要驗證該名稱對尚未被現有使用者佔用。 按以下程式碼所示定義新屬性:

C#

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(LastName))]
public string FirstName { get; set; }
[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName))]
public string LastName { get; set; }

AdditionalFields 可能已顯式設定為字串 "FirstName""LastName",但使用 nameof 這樣的操作符可簡化稍後的重構過程。 然後,用於執行驗證的操作方法必須採用兩個引數,一個用於 FirstName 的值,一個用於 LastName 的值。

C#

[AcceptVerbs("Get", "Post")]
public IActionResult VerifyName(string firstName, string lastName)
{
    if (!_userRepository.VerifyName(firstName, lastName))
    {
        return Json(data: $"A user named {firstName} {lastName} already exists.");
    }

    return Json(data: true);
}

現在,當用戶輸入名和姓時,JavaScript 會:

  • 發出遠端呼叫,以瞭解該名稱對是否已被佔用。
  • 如果被佔用,則顯示一條錯誤訊息。
  • 如果未被佔用,則使用者可以提交表單。

如果需要使用 [Remote] 特性驗證兩個或更多附加欄位,可將其以逗號分隔的列表形式列出。 例如,若要向模型中新增 MiddleName 屬性,可按以下程式碼所示設定 [Remote] 特性:

C#

[Remote(action: "VerifyName", controller: "Users", AdditionalFields = nameof(FirstName) + "," + nameof(LastName))]
public string MiddleName { get; set; }

AdditionalFields 與所有屬性引數一樣,必須是常量表達式。 因此,不能使用內插字串或呼叫 string.Join() 來初始化 AdditionalFields。 對於新增到 [Remote] 特性的每個附加欄位,都必須向相應的控制器操作方法另外新增一個引數。