問題
想要 ASP.NET Web API 執行模型驗證,同時可以和 ASP.NET MVC 共享一些驗證邏輯。
解決方案
ASP.NET Web API 與 ASP.NET MVC 支持一樣的驗證機制,都是通過System.ComponentModel.DataAnnoataions 的屬性驗證。使用框架提供的相關驗證屬性,已足夠來用來驗證模型。
想要更細粒度的驗證,我們可以選擇在我們的模型中實現 IValudateObject(來自於System.ComponentModel.DataAnnotations)。如果所有的屬性都驗證通過,ASP.NET Web API 將會調用接口的Validate 方法,在這裡我們可以進行更進一步的進行實體驗證。這是和 MVC 裡面的行為一樣,並且,我們甚至可以在 Web API 和 MVC 中使用同一個 DTO。
還有另一種方法,就是可以使用一個叫做 FluentValidation(NuGet 中可以下載FluentValidation)的第三方程序庫,他可以構建更強大的驗證場景。在這樣的情況下,我們仍然需在我們的模型中實現 IValidateObject 接口,同時需要依賴於FluentValidation 驗證器,而不是內嵌的驗證邏輯。
小提示 ASP.NET Web API 的驗證行為在跨宿主機上是相同的。
工作原理
為了從 HTTP 請求 Body 中讀取的模型並執行驗證,ASP.NET Web API 依賴於一個 IBodyModelValidator 的服務。接口的大致描述如清單 1-17 所示,然而,他是一個可替代的服務,正常情況下,默認實現(DefaultBodyModelValidator)足夠我們使用,在HttpConfiguration 被設置為自啟動。
清單 1-17. IBodyModelValidator 接口
1 2 3 4 5public
interface
IBodyModelValidator
{
bool
Validate(
object
model, Type type, ModelMetadataProvider metadataProvider,
HttpActionContext actionContext,
string
keyPrefix);
}
有一個叫做FormatrtParameterBinding 的服務,在 HTTP 請求 Body 綁定到 Action 參數的處理請求時,DefaultBodyModelValidator 的 Validate 方法會被調用。對於驗證程序,他會遞歸驗證整個對象圖譜,驗證每一個屬性以及嵌套屬性。Web API 通過使用DataAnnotationModelValidatorProviderr 來支持聲明。如果我們的模型使用WCF 方式的 DataMemberAttribute 聲明,那麼,我們需要使用框架的 DataMemberValidatorProvider。
最後,我們的模型可以實現IValidatableObject 接口,這個接口只暴露了一個簡單的方法如清單1-18所示。如果實現了接口,那就需要我們自己提供額外的驗證邏輯。只要所有的屬性驗證通過,ASP.NET Wwb API 就會調用IValidateableObject接口的 Validate 方法,
清單1-18. IValidateableObject 接口的定義
1 2 3 4public
interface
IValidateableObject
{
IEnumerable<ValidationResult> Validate(ValidationContext validationContext);
}
驗證結果是通過 ASP.NET Web API 的 ModelStateDictionary 形式表示,在這裡 ModelState 也是可以用的。這個和 ASP.NET MVC 中的概念是完全一樣的,但是使用的對象是不同的,因為 Web API 使用自己版本的System.Web.Http.Modelbinding。ModelStateDictionary 暴露了IsValid 屬性,這個屬性可以用來檢查 Action 內Model 驗證的狀態。
聲明的驗證機制也很好的整合到了 ASP.NET Web API Help Page,可以提供對 API 語義上的描述。我們將會在7-11 的時候詳細討論他。
小提示 在 API 中最好的做法是使用不同的模型作為 Request 和Response 實體。例如,實體 ID 一般僅僅是 Response 模型需要的,如果 Request 中需要的話,是可以從 URI 中拿到的。
代碼
清單 1-19 展示了一個模型有多種驗證的情況:
RequiredAttribute,MaxLengthAttribute 和
RangeAttribute。接下來,我們就可以利用 ModelState 來驗證 Controller 中的驗證狀態,同時響應適當的提示信息給調用端。
清單 1-19. 簡單的 Web API 模型驗證
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24public
class
Album
{
public
int
Id {
get
;
set
; }
[Required(ErrorMessage =
"{0} is required"
)]
[MaxLength(30)]
public
string
Artist {
get
;
set
; }
[Required(ErrorMessage =
"{0} is required"
)]
[MaxLength(40)]
public
string
Title {
get
;
set
; }
[Range(0, 10, ErrorMessage =
"{0} in the range of {1}-{2} is required."
)]
public
int
Rating {
get
;
set
; }
}
public
class
AlbumController : ApiController
{
public
HttpResponseMessage Post(Album album)
{
if
(!ModelState.IsValid)
{
throw
new
HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest,
ModelState));
}
//omitted for brevity
}
}
負責處理 ModelState 代碼的一般驗證可以很容易從 Controller 提取到成公共的部分,使其可以被很好的重用,不過這一部分我們將在 5-4 的時候再詳細介紹。
現在,我們考慮一下這個場景,如果我們要在模型上增加增加兩個額外的屬性 Rating 和 Starred,同時擴展模型驗證,驗證的要求是這兩個屬性至少有一個是必填的。雖然,在兩個屬性之間糾纏的驗證很難使用聲明的方式來表示,但是,不要忘記 IValidateableObject 可以幫我們。我們可以使用接口中的 Validata 的方法去檢查整個模型的狀態,同時返回相應的 ValidationResult。我們要做的修改如清單 1-20 所示的代碼。
清單 1-20. 修改 ASP.NET Web API 依賴於 IValidateableObject 的驗證
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23public
class
Album : IValidatableObject
{
public
int
Id {
get
;
set
; }
[Required(ErrorMessage =
"{0} is required"
)]
[MaxLength(30)]
public
string
Artist {
get
;
set
; }
[Required(ErrorMessage =
"{0} is required"
)]
[MaxLength(40)]
public
string
Title {
get
;
set
; }
public
int
? Rating {
get
;
set
; }
public
bool
? Starred {
get
;
set
; }
public
IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if
(!(Rating.HasValue && Rating > 0 && Rating < 10) || (Starred.HasValue && Starred.Value))
{
yield
return
new
ValidationResult(
"You must set either the Rating in the 0-9 range orStarred flag."
);
}
}
}