代码之家  ›  专栏  ›  技术社区  ›  Andras Zoltan

如果参数为null,则阻止执行MVC操作方法

  •  5
  • Andras Zoltan  · 技术社区  · 14 年前

    我想了一些方法,但我想得到社区的意见。我有一种感觉,答案很简单——我不怕显得愚蠢(我的孩子很久以前就把这种恐惧从我身边带走了!)

    我正在使用MVC2编写一个XMLRESTWeb服务。web服务的使用者将接收和发送的所有XML类型都由简单但广泛的XSD控制,这些参数将通过自定义的默认模型绑定器和值提供程序从请求体中的XML绑定。

    我有相当数量的控制器,每个控制器都有相当数量的动作方法(不是过多的-只是“goodly”;)——在几乎所有情况下,这些动作方法都将接受所有引用类型的模型类型。

    实际上,在每种情况下,调用者不提供这些参数值都将是一个错误,因此标准错误消息 "The parameter {name} type:{ns:type} is required"

    我想做的是在执行action方法之前验证参数是否为非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 回复  |  直到 14 年前
        1
  •  2
  •   Andras Zoltan    14 年前

    下面是我提出的实现(在等待更好的想法时:)

    这是一种通用的方法,而且我认为它具有很强的可扩展性——在提供我正在寻找的错误自动响应功能(当模型状态包含一个或多个错误时)的同时,希望允许类似于模型验证的参数深度验证。

    我希望这不是太多的代码,所以答案(!);我有一大堆的文档注释,我把它们取出来以缩短它。

    • 对将从中构造参数值的XML进行架构验证失败
    • 缺少(null)参数值

    模式验证目前是在模型绑定期间执行的,并且会自动将模型错误添加到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,它接受一个模型实例,并使用DataContractSerializer或XmlSerializer序列化响应—因此我创建了 RespondWithXmlModelErrorsAttribute Errors

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

    但是对于web服务(XML或JSON)来说,能够将错误报告转移到其他东西上,使得编写实际操作方法变得更加容易,而且更具表现力,我觉得。

        2
  •  1
  •   user151323 user151323    14 年前

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

    或者,可以创建自定义约束,将路由值作为一个包一起验证。这对你来说可能是个更好的策略。请看这里: Creating a Custom Route Constraint