2010-09-08 116 views
5

我想到了這樣做的幾種方法,但我想獲得社區的視圖。我有一種感覺,答案是令人生畏的簡單 - 我不害怕看起來很愚蠢(我的孩子很久以前就把這種恐懼從我身上帶走了)如果參數爲null,則防止執行MVC Action方法

我正在使用MVC2編寫XML REST Web服務。 Web服務的使用者將接收和發送的所有XML類型都由簡單而廣泛的XSD進行管理,並且這些參數將通過自定義默認模型綁定器和值提供程序從請求主體中的xml綁定。我有很好的控制器,每個控制器都有很好的動作方法(不是過多 - 只是'很好')) - 幾乎在所有情況下,這些動作方法都將接受所有的模型類型參考類型。

實際上在每種情況下,調用者都不會提供這些參數值,因此可以發回"The parameter {name} type:{ns:type} is required"等標準錯誤消息。

我想要做的是能夠在執行操作方法之前驗證參數不爲null;然後返回一個表示錯誤的ActionResult給客戶端(爲此我已有一個XMLResult類型),而操作方法本身不必驗證參數本身。

因此,而不是:

public ActionResult ActionMethod(RefType model) 
{ 
    if(model == null) 
     return new Xml(new Error("'model' must be provided")); 
} 

喜歡的東西:

public ActionResult ActionMethod([NotNull]RefType model) 
{ 
    //model now guaranteed not to be null. 
} 

我知道這正是那種交叉,可以在MVC實現的。

這在我看來,無論是的OnActionExecuting基本控制器覆蓋或自定義ActionFilter是這樣做的最有可能的方式。

我也希望能夠擴展系統,以便它自動拾取XML模式驗證錯誤(在綁定期間通過自定義值提供程序添加到ModelState中),從而防止繼續執行操作方法(如果有任何參數由於XML請求格式不正確,無法正確加載值。

回答

2

下面是我想出來的(在等待更好的想法:))

這是一個通用的辦法,我認爲執行是相當可擴展性 - 允許希望類似在提供模型驗證的同時提供我期待的錯誤自動響應功能(當模型狀態包含一個或多個錯誤時)時,可以進行參數驗證的深度。

我希望這不是太多代碼的SO回答(!);我在那裏有大量的文檔評論,我已經把它縮短了。

所以,在我的情況我有兩種類型的模型錯誤的,如果他們出現的,應該阻止該操作方法的執行:

    從參數值將被構建的XML的
  • 失敗架構驗證模型中結合當前正在執行
  • 失蹤(空)參數值

模式驗證,並自動添加模型錯誤的ModelState中 - 所以這是很大的。所以我需要一種方法來執行自動空檢查。

最後,我創建了兩個班的收官驗證:

[AttributeUsage(AttributeTargets.Parameter, 
AllowMultiple = false, Inherited = false)] 
public abstract class ValidateParameterAttribute : Attribute 
{ 
    private bool _continueValidation = false; 

    public bool ContinueValidation 
    { get { return _continueValidation; } set { _continueValidation = value; } } 

    private int _order = -1; 
    public int Order { get { return _order; } set { _order = value; } } 

    public abstract bool Validate 
    (ControllerContext context, ParameterDescriptor parameter, object value); 

    public abstract ModelError CreateModelError 
    (ControllerContext context, ParameterDescriptor parameter, object value); 

    public virtual ModelError GetModelError 
    (ControllerContext context, ParameterDescriptor parameter, object value) 
    { 
    if (!Validate(context, parameter, value)) 
     return CreateModelError(context, parameter, value); 
    return null; 
    } 
} 

[AttributeUsage(AttributeTargets.Parameter, 
AllowMultiple = false, Inherited = false)] 
public class RequiredParameterAttribute : ValidateParameterAttribute 
{ 
    private object _missing = null; 

    public object MissingValue 
    { get { return _missing; } set { _missing = value; } } 

    public virtual object GetMissingValue 
    (ControllerContext context, ParameterDescriptor parameter) 
    { 
    //using a virtual method so that a missing value could be selected based 
    //on the current controller's state. 
    return MissingValue; 
    } 

    public override bool Validate 
    (ControllerContext context, ParameterDescriptor parameter, object value) 
    { 
    return !object.Equals(value, GetMissingValue(context, parameter)); 
    } 

    public override ModelError CreateModelError 
    (ControllerContext context, ParameterDescriptor parameter, object value) 
    { 
    return new ModelError(
     string.Format("Parameter {0} is required", parameter.ParameterName)); 
    } 
} 

有了這個話,我可以這樣做:

public void ActionMethod([RequiredParameter]MyModel p1){ /* code here */ } 

但是,這對自己沒有做任何事情當然,所以現在我們需要一些實際觸發驗證,以獲得模型錯誤並將它們添加到模型狀態。

輸入ParameterValidationAttribute

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
       Inherited = false)] 
public class ParameterValidationAttribute : ActionFilterAttribute 
{ 
    public override void OnActionExecuting(ActionExecutingContext filterContext) 
    { 
    var paramDescriptors = filterContext.ActionDescriptor.GetParameters(); 
    if (paramDescriptors == null || paramDescriptors.Length == 0) 
     return; 

    var parameters = filterContext.ActionParameters; 
    object paramvalue = null; 
    ModelStateDictionary modelState 
     = filterContext.Controller.ViewData.ModelState; 
    ModelState paramState = null; 
    ModelError modelError = null; 

    foreach (var paramDescriptor in paramDescriptors) 
    { 
     paramState = modelState[paramDescriptor.ParameterName]; 
     //fetch the parameter value, if this fails we simply end up with null 
     parameters.TryGetValue(paramDescriptor.ParameterName, out paramvalue); 

     foreach (var validator in paramDescriptor.GetCustomAttributes 
       (typeof(ValidateParameterAttribute), false) 
       .Cast<ValidateParameterAttribute>().OrderBy(a => a.Order) 
      ) 
     { 
     modelError = 
      validator.GetModelError(filterContext, paramDescriptor, paramvalue); 

     if(modelError!=null) 
     { 
      //create model state for this parameter if not already present 
      if (paramState == null) 
      modelState[paramDescriptor.ParameterName] = 
       paramState = new ModelState(); 

      paramState.Errors.Add(modelError); 
      //break if no more validation should be performed 
      if (validator.ContinueValidation == false) 
      break; 
     } 
     } 
    } 

    base.OnActionExecuting(filterContext); 
    } 
} 

呼!近有現在......

所以,現在我們可以這樣做:

[ParameterValidation] 
public ActionResult([RequiredParameter]MyModel p1) 
{ 
    //ViewData.ModelState["p1"] will now contain an error if null when called 
} 

爲了完成我們需要的東西,可以調查模型的錯誤並自動,如果有任何迴應了這個難題。這是班中最整潔(我討厭的名稱和使用的參數類型),我可能會改變它在我的項目,但它的作品,所以我會反正張貼:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
       Inherited = false)] 
public abstract class RespondWithModelErrorsAttribute : ActionFilterAttribute 
{ 
    public override void OnActionExecuting(ActionExecutingContext filterContext) 
    { 
    ModelStateDictionary modelState = 
     filterContext.Controller.ViewData.ModelState; 

    if (modelState.Any(kvp => kvp.Value.Errors.Count > 0)) 
     filterContext.Result = CreateResult(filterContext, 
        modelState.Where(kvp => kvp.Value.Errors.Count > 0)); 

    base.OnActionExecuting(filterContext); 
    } 

    public abstract ActionResult CreateResult(
    ActionExecutingContext filterContext, 
    IEnumerable<KeyValuePair<string, ModelState>> modelStateWithErrors); 
} 

在我應用程序我有一個XmlResult,它接受一個Model實例並使用DataContractSerializer或XmlSerializer序列化到響應 - 所以我創建了RespondWithXmlModelErrorsAttribute,它繼承了這個最後一個類型,將其中的一個與該模型一起構建爲Errors類,該類僅包含每個類作爲字符串的模型錯誤。響應代碼也會自動設置爲400錯誤請求。

因此,現在我可以這樣做:

[ParameterValidation] 
[RespondWithXmlModelErrors(Order = int.MaxValue)] 
public ActionResult([RequiredParameter]MyModel p1) 
{ 
    //now if p1 is null, the method won't even be called. 
} 

在這最後階段不一定會被要求在網頁的情況下,由於模型誤差通常包括在頁面的重新渲染髮送首先是數據,現有的MVC方法適合這種情況。

但對於Web服務(XML或JSON)能夠卸載錯誤報告的東西別的使得編寫實際的操作方法容易得多 - 並且更富於表現力,我覺得。

+0

接受這個答案,因爲它對我來說真的很好,我認爲它是適當的可擴展性是一個非常有效的補充我們的內部Mvc擴展庫。 – 2010-09-15 21:39:24

+0

這是一個很好的解決方案,我之後做了什麼..乾杯安德拉斯。我的第一次嘗試幾乎在那裏,但我看到我失蹤了。 – horHAY 2016-11-25 15:58:55

1

那麼你可以添加使用正則表達式個別路線值的約束。然後,如果這些約束不堅持,行動方法不會被打:

routes.MapRoute ("SomeWebService", "service/{userId}", 
       new { controller = "Service", action = "UserService" }, 
       new { userId = @"\d+" }); 

另外,您可以創建自定義的約束條件來驗證路由值一起作爲一個包。這對你來說可能是一個更好的策略。看看這裏:Creating a Custom Route Constraint

+0

感謝,不幸的是這是行不通的,因爲這些參數在請求主體不那麼容易獲得的路由引擎XML綁定;我不想在路由級別檢查請求xml來執行此驗證。這也會讓它很難返回一個很好的錯誤信息。 – 2010-09-08 08:14:00

+0

已編輯,以突出這些參數將來自請求機構,因此這必須在模型綁定 – 2010-09-08 08:18:35