ASP.NET MVC默認采用基於標准特性的Model驗證機制,但是只有應用在Model類型及其屬性上的ValidationAttribute才有效。如果我們能夠將ValidationAttribute特性直接應用到參數上,我們不但可以實現簡單類型(比如int、double等)數據的Model驗證,還能夠實現“一個Model類型,多種驗證規則”,本篇文章將為你提供相關的解決方案(源代碼從這裡下載)。
一、ValidationAttribute本身是可以應用到參數上的
如果你夠細心應該會發現我們常用的驗證特性都可以直接應用到方法的參數上。以如下所示的RangeAttribute的定義為例,應用在該類型上的AttributeUsageAttribute的定義表明可以標注該特性的目標元素包括參數、字段和屬性。
1: [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property,AllowMultiple=false)]
2: public class RangeAttribute : ValidationAttribute
3: {
4: //省略成員
5: }
但是對於ASP.NET MVC的Model驗證來說,應用在Action方法參數上的驗證特性起不到任何作用,原因很簡單:用於進行Model驗證的ModelValidator對象是通過基於參數類型的Model元數據來創建的,根本不會去解析應用在參數本身上的驗證特性。
二、為什麼需要基於參數的Model驗證?
但是在我看到,直接針對Action參數的Model驗證具有很高的實用意義:
有些情況下我們不能對作為Model的數據類型進行修改(比如像int、double和字符串這樣的原生類型);
相同的Model類型在不同的Action方法調用中需要采用不同的驗證規則。
如果我們可以直接將驗證特性應用到參數上面,這兩個問題在一定程度上都可以得到解決。
三、如何得到應用在參數上的ValidationAttribute?
到目前為止,我們對ASP.NET MVC的可擴展的Model驗證系統已經有了一個全面的了解,現在我們通過對它進行相應的擴展使直接應用到參數上的驗證特性能夠生效。我們需要自定義一個ModelValidatorProvider將提供基於應用到參數上的驗證特性的ModelValidator,但在這之前需要解決的另一個問題是如何將應用於參數的特性提供給我們自定義的ModelValidatorProvider。在這裡我們將當前ControllerContext作為這些特性的載體。
Action方法的執行通過ActionInvoker來實現,默認的ControllerActionInvoker和AsyncControllerActionInvoker都定義了一個受保護的虛方法GetParameterValue根據用於描述參數的ParameterDescriptor對象和當前的Controller上下文來綁定對應的參數值。那麼我們就可以通過繼承ControllerActionInvoker/AsyncControllerActionInvoker以重寫該方法的方式將ParameterDescriptor保存當前的Controller上下文中。
為此我們定義了一個具有如下定義的兩個自定義的ActionInvoker。ParameterValidationActionInvoker和ParameterValidationAsyncActionInvoker分別繼承自ControllerActionInvoker和AsyncControllerActionInvoker。在重寫的GetParameterValue方法中,我們在調用基類的同名方法之前將作為參數的ParameterDescriptor對象保存到當前Controller上下文中,具體來說是放到了表示當前路由數據的RouteDataDictionary對象的DataTokens集合中。在方法調用之後我們將它從Controller上下文中移除。
1: public class ParameterValidationActionInvoker : ControllerActionInvoker
2: {
3: protected override object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
4: {
5: try
6: {
7: controllerContext.RouteData.DataTokens.Add("ParameterDescriptor",parameterDescriptor);
8: return base.GetParameterValue(controllerContext, parameterDescriptor);
9: }
10: finally
11: {
12: controllerContext.RouteData.DataTokens.Remove("ParameterDescriptor");
13: }
14: }
15: }
16:
17: public class ParameterValidationAsyncActionInvoker : AsyncControllerActionInvoker
18: {
19: protected override object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
20: {
21: try
22: {
23: controllerContext.RouteData.DataTokens.Add("ParameterDescriptor", parameterDescriptor);
24: return base.GetParameterValue(controllerContext, parameterDescriptor);
25: }
26: finally
27: {
28: controllerContext.RouteData.DataTokens.Remove("ParameterDescriptor");
29: }
30: }
31: }