原文:Model Validation
作者:Rachel Appel
翻譯:婁宇(Lyrics)
校對:孟帥洋(書緣)
在這篇文章中:
章節:
介紹模型驗證
在一個應用程序將數據存儲到數據庫之前,這個應用程序必須驗證數據。數據必須檢查潛在的安全隱患,驗證類型和大小是正確並且符合你所制定的規則。盡管驗證的實現可能會是冗余和繁瑣的,卻是有必要的。在 MVC 中,驗證發生在客戶端和服務器端。
幸運地是, .Net 有一些擁有抽象驗證的驗證 Attribute 。這些 Attribute 包含驗證代碼,從而減少你必須寫的代碼量。
驗證 Attribute
驗證 Attribute 是一種配置模型驗證的方法,類似在數據庫表中驗證字段的概念。它包含了指定數據類型或者必填字段等約束。其它類型的驗證包括將強制的業務規則應用到數據驗證,比如驗一個信用卡號,一個手機號碼,或者一個 Email 地址。 驗證 Attribute 使這些要求更簡單,更容易使用。
下面是一個存儲了電影和電視節目信息的應用程序中被注解的 Movie
模型。大部分屬性是必填的,幾個字符串類型的屬性有長度限制。此外,在 Price
屬性上通過自定義驗證 Attribute 實現了 0 到 $999.99 的數字范圍限制。
public class Movie
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Title { get; set; }
[Required]
[ClassicMovie(1960)]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
[Required]
[StringLength(1000)]
public string Description { get; set; }
[Required]
[Range(0, 999.99)]
public decimal Price { get; set; }
[Required]
public Genre Genre { get; set; }
public bool Preorder { get; set; }
}
簡單地通過閱讀模型了解了這個應用程序的數據規則,(這種編碼方式)讓維護代碼變得更簡單。以下是幾個常用的內置驗證 Attribute :
[CreditCard]
: 驗證屬性是信號卡號格式。[Compare]
: 驗證模型中的兩個屬性匹配。[EmailAddress]
: 驗證屬性是 Email 格式。[Phone]
: 驗證屬性是 電話號碼 格式。[Range]
: 驗證屬性在指定的范圍內。[RegularExpression]
: 驗證數據匹配指定的正則表達式。[Required]
: 使屬性成為必填。[StringLength]
: 驗證字符串類型屬性的最大長度。[Url]
: 驗證屬性是 URL 格式。
MVC 支持任何為了驗證目的而從 ValidationAttribute
繼承的 Attribute 。需要有用的驗證 Attribute 可以在 System.ComponentModel.DataAnnotations 命名空間下找到。
可能在某些情況下,你需要使用比內置 Attribute 更多的驗證功能。在那時,你可以通過創建繼承自 ValidationAttribute
的自定義驗證 Attribute 或者修改你的模型去實現 IValidatableObject
接口。
模型狀態
模型狀態表示在 HTML 表單提交值的一系列驗證錯誤。
MVC 將持續驗證字段直到錯誤數達到最大值(默認200)。你可以通過在 Startup.cs
文件下的 ConfigureServices
方法中插入以下代碼來配置這個最大值:
services.AddMvc(options => options.MaxModelValidationErrors = 50);
處理模型狀態異常
模型驗證發生在每個控制器(Controller)的行為(Action)被調用之前,而檢查 ModelState.IsValid
和做出適當的反應是行為(Action)方法的職責。在許多情況下,適當的反映是返回某種錯誤響應,理想情況下詳細介紹了模型驗證失敗的原因。
一些應用程序將選擇遵循一個標准的慣例來處理模型驗證錯誤,在這種情況下,過濾器可能是一個適當的方式來實現這種策略。你需要分別用有效和無效的模型狀態來測試 Action 的行為。
手動驗證
當模型綁定和驗證完成后,你也許想重復其中的部分操作。例如,用戶可能輸入了一個被期望為 integer 類型的字段的文本,或者你需要為模型中的一個屬性計算一個值。
你需要手動去執行驗證。像這樣,調用 TryValidateModel
方法:
TryValidateModel(movie);
自定義驗證
驗證 Attribute 滿足大多數的驗證需求。然而你的業務存在一些特殊的驗證規則,它們不僅僅是通用的數據驗證,如確保字段必填或者符合一個值的范圍之類的。對於這些情況,自定義驗證 Attribute 是一個不錯的解決方案。在 MVC 中創建你自己的自定義驗證 Attribute 是非常容易的。只需要繼承 ValidationAttribute
並且重寫 IsValid
方法。 IsValid
方法接受兩個參數,第一個是命名為 value
的 object 對象,第二個參數是一個命名為 validationContext
的 ValidationContext
對象。 Value
指的是你的自定義驗證器驗證的字段的值。
在下面的示例中,一個業務規則規定,用戶可能不會將在1960年之后發布的電影的 Genre
設置為 Classic
。[ClassicMovie]
Attribute 首先檢查 Genre
,如果它是 Genre.Classic
,接下來檢查電影發布日期是否晚於1960年。如果發布晚於1960年,驗證失敗。這個 Attribute 接受一個 integer 類型的參數作為驗證數據的年份。你可以在這個 Attribute 的構造函數中對這個值進行賦值,如同這里顯示的:
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
方法按照規定檢查了日期和分類( Genre )。當驗證成功, IsValid
方法返回一個 ValidationResult.Success
枚舉碼;當驗證失敗,返回一個帶有錯誤消息的 ValidationResult
。當用戶修改了 Genre
字段並且提交表單, ClassicMovieAttribute
中的 IsValid
方法將驗證電影是否是經典( Classic )。如同其他內置的 Attribute 一樣,應用 ClassicMovieAttribute
到比如 ReleaseDate
這個屬性上來確保驗證發生,如果之前例子中的演示代碼一樣。因為這個例子僅對 Movie
類型有效,一個更好的選擇使用下面段落介紹的 IValidatableObject
。
另外,相同的代碼可以放在模型里,通過去實現 IValidatableObject
接口中的 Validate
方法。當自定義驗證 Attribute 能夠很好的驗證各個屬性時,實現 IValidatableObject
接口可以用來實現類等級(Class-Level)的驗證,如下。
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 腳本來進行客戶端驗證,如下。
<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.11.3.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.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>
除了模型屬性的類型元數據外,MVC還是用驗證 Attribute 通過 JavaScript 驗證數據並展示所有錯誤信息。當你使用 MVC 去渲染使用 Tag Helpers 或者 HTML helpers 的表單數據之時,它將在需要驗證的表單元素中添加 HTML 5 data- attributes,如同下面看到的。 MVC 對所有內置驗證 Attribute 和自定義驗證 Attribute 生成 data-
特性。你可以通過相關的 Tag Helper 在客戶端顯示驗證錯誤,如同這里展示的:
<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>
上面的 Tag Helper 渲染的 HTML 如下。 注意輸出的 HTML 中 data-
特性對應 ReleaseDate
屬性的驗證 Attribute。下面的 data-val-required
特性包含一個用於展示的錯誤消息,如果用戶沒有填寫 ReleaseDate 字段,錯誤消息將隨着 <span>
元素一起顯示。
<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]
Attribute 來覆蓋。基礎的 [DataType]
Attribute 並不是真正的服務端認證。瀏覽器選擇它們自己的錯誤消息並按照它們希望的那樣顯示這些錯誤,然而 jQuery Validation Unobtrusive 包可以重寫這些消息並且讓他們顯示方式保持一致。當用戶應用 [DataType]
的子類比如 [EmailAddress]
的時候,這種情況最明顯。
客戶端模型驗證器
你也許會為你的自定義 Attribute 創建客戶端邏輯,unobtrusive validation 會在客戶端將它作為驗證的一部分自動執行。第一步
是向下面一樣,通過實現 IClientModelValidator
接口來控制那些被添加的 data- 特性:
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);
}
Attribute 實現這個接口后可以添加 HTML 特性到生成的字段。檢查輸出的 HTML 中的 ReleaseDate
元素,和上一個例子差不多,除了通過 IClientModelValidator
接口的 AddValidation
方法定義了一個 data-val-classicmovie
特性。
<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="" />
Unobtrusive validation 使用 data-
特性中的數據來顯示錯誤消息。然而 JQuery 在你添加 JQuery 的 validator
對象之前是不知道規則和消息的。在顯示在下面的例子中將一個包含自定義客戶端驗證代碼的命名為 classicmovie
的方法添加到 JQuery 的 validator
對象中。
$(function () {
jQuery.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;
});
jQuery.validator.unobtrusive.adapters.add('classicmovie',
[ 'element', 'year' ],
function (options) {
var element = $(options.form).find('select#Genre')[0];
options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
options.messages['classicmovie'] = options.message;
});
}(jQuery));
現在 JQuery 擁有執行自定義 JavaScript 驗證以及當驗證代碼返回 false 時用來顯示的錯誤消息的信息了。
遠程驗證
當你需要在客戶端上使用服務器上的數據進行驗證的時候,遠程驗證是一個很棒的功能。比如,你的應用程序也許需要驗證一個 Email 或者用戶名是否已經被使用,這樣做必須查詢大量的數據。為了驗證一個或幾個字段下載大量的數據,消耗了過多的資源。並且可能會暴露敏感信息。另一個辦法是使用回傳請求來驗證字段。
你可以用兩個步驟實現遠程驗證。首先,你需要用 [Remote]
Attribute 注解你的模型。[Remote]
Attribute 接受多個重載可以直接使用客戶端 JavaScript 到適當的代碼來調用。下面的例子指向 Users
Controller 的 VerifyEmail
Action 。
public class User
{
[Remote(action: "VerifyEmail", controller: "Users")]
public string Email { get; set; }
}
第二步是將驗證代碼放到 [Remote]
Attribute 中定義的相應 Action 方法中。Action 方法返回一個 JsonResult
,如果需要,客戶端可以用來繼續或者暫停並顯示錯誤。
[AcceptVerbs("Get", "Post")]
public IActionResult VerifyEmail(string email)
{
if (!_userRepository.VerifyEmail(email))
{
return Json(data: $"Email {email} is already in use.");
}
return Json(data: true);
}
現在當用戶輸入一個 Email ,View 中的 JavaScript 進行遠程調用來檢查 Email 是否被占用,如果被占用就顯示錯誤消息。否則,用戶可以和往常一樣提交表單。