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

时间:2010-09-08 07:55:06

标签: c# asp.net-mvc asp.net-mvc-2 .net-3.5

我想过几种方法,但我希望得到社区的看法。我有一种感觉,答案非常简单 - 我不怕看起来很愚蠢(我的孩子很久以前就把这种恐惧从我身上带走了!)

我正在使用MVC2编写XML REST Web服务。 Web服务的使用者将接收和发送的所有XML类型都由简单但广泛的XSD管理,并且这些参数将通过自定义默认模型绑定器和值提供程序从请求正文中的xml绑定。

我有很多控制器,每个都有很多动作方法(不过量 - 只是'很好';)) - 几乎在所有情况下,这些动作方法都将接受所有参考的模型类型类型。

在几乎所有情况下,调用者都不会提供这些参数值,因此可能会发回"The parameter {name} type:{ns:type} is required"这样的标准错误消息。

我想要做的是能够在执行操作方法之前验证参数是否为空;然后返回一个表示客户端错误的ActionResult(为此我已经有XMLResult类型),而action方法本身不必验证参数本身。

所以,而不是:

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 个答案:

答案 0 :(得分:2)

这是我提出的实现(等待任何更好的想法:))

这是一种通用的方法,我认为它具有相当的可扩展性 - 在提供错误自动响应功能的同时(模型状态包含一个或多个)时,可以获得与模型验证相同的参数验证深度我正在寻找的更多错误。

我希望这不是一个SO答案的代码太多(!);我在那里有大量的文档评论,我已经把它缩短了。

所以,在我的场景中,我有两种类型的模型错误,如果它们发生,应该阻止执行action方法:

  • 将构造参数值的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,它接受一个Model实例并使用DataContractSerializer或XmlSerializer序列化到响应 - 所以我创建了RespondWithXmlModelErrorsAttribute继承自这最后一个类型以制定其中一个model作为Errors类,它只是将每个模型错误包含在字符串中。响应代码也会自动设置为400 Bad Request。

因此,现在我可以这样做:

[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),使得编写实际的动作方法变得更容易 - 而且更具表现力,我觉得。

答案 1 :(得分:1)

您可以使用正则表达式将约束添加到单个路径值。然后,如果不支持这些约束,则不会触发操作方法:

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

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