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

And*_*tan 5 c# asp.net-mvc .net-3.5 asp.net-mvc-2

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

我正在使用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"));
}
Run Code Online (Sandbox Code Playgroud)

就像是:

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

我知道这正是MVC可以实现的那种交叉.

在我看来,基本控制器覆盖OnActionExecuting或自定义ActionFilter是最有可能的方式.

我还希望能够扩展系统,以便它自动获取XML模式验证错误(在自定义值提供程序绑定期间添加到ModelState),从而阻止操作方法继续,如果任何参数值不能正确加载,因为XML请求格式错误.

And*_*tan 3

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

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

我希望这对于一个如此答案来说不是太多代码(!);我在那里有大量的文档注释,我将其删除以使其更短。

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

  • 用于构造参数值的 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));
  }
}
Run Code Online (Sandbox Code Playgroud)

有了这个我就可以这样做:

public void ActionMethod([RequiredParameter]MyModel p1){ /* code here */ }
Run Code Online (Sandbox Code Playgroud)

但这本身当然不会做任何事情,所以现在我们需要一些东西来实际触发验证,获取模型错误并将它们添加到模型状态。

输入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);
  }
}
Run Code Online (Sandbox Code Playgroud)

哇!现在已经快到了...

所以,现在我们可以这样做:

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

为了完成这个难题,我们需要一些可以调查模型错误并自动响应(如果有)的东西。这是最不整洁的类(我讨厌使用的名称和参数类型),我可能会在我的项目中更改它,但它有效,所以我无论如何都会发布它:

[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);
}
Run Code Online (Sandbox Code Playgroud)

在我的应用程序中,我有一个 XmlResult,它采用 Model 实例并使用 DataContractSerializer 或 XmlSerializer 序列化为响应 - 因此我创建了RespondWithXmlModelErrorsAttribute从最后一个类型继承的 XmlResult,以将模型中的其中一个制定为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.
}
Run Code Online (Sandbox Code Playgroud)

对于网页来说,不一定需要最后一个阶段,因为模型错误通常包含在首先发送数据的页面的重新渲染中,而现有的 MVC 方法适合这种情况。

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