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

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

我已经想到了几种实现此目的的方法,但我想听取社区的意见。我觉得答案简单得令人畏缩——我不害怕看起来很愚蠢(我的 child 很久以前就把这种恐惧从我身上带走了!)

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

我有大量的 Controller ,每个 Controller 都有大量的 Action 方法(不过分 - 只是'很好';)) - 几乎在每种情况下,这些 Action 方法都将接受所有引用的模型类型类型。

在几乎所有情况下,调用者不提供这些参数值都会出错,因此会出现标准错误消息,例如 "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 的基本 Controller 覆盖或自定义 ActionFilter 是最可能的方法。

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

最佳答案

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

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

我希望这不是一个 SO 答案的太多代码(!);我已经删除了其中的大量文档注释以使其更短。

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

  • 将构造参数值的 XML 的模式验证失败
  • 缺少(空)参数值

架构验证目前在模型绑定(bind)期间执行,并自动将模型错误添加到 ModelState - 这非常棒。所以我需要一种方法来执行自动 null 检查。

最后我创建了两个类来完成验证:

[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 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 方法适合这种情况。

但是对于网络服务(XML 或 JSON)来说,能够将错误报告卸载到其他东西使得编写实际的操作方法变得容易得多 - 我觉得也更具表现力。

关于c# - 如果参数为 null,则阻止 MVC Action 方法执行,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3665655/

相关文章:

c# - 更改 Web Api 2 Post 方法以接受修改后的正文

c# - 在 Windows XP 中使用 C# 在登录屏幕上显示窗口

c# - 自定义字符串作为 Entity Framework 的主键

c# - 系统.BadImageFormatException : 64bit MVC 2 project - with step by step instruction to reproduce the error

asp.net-mvc - ASP.NET MVC : Why can't I set ShowForEdit model metadata with an attribute?

c# - 是否可以在没有外键的情况下设置导航属性?

c# - 正则表达式中的可选组返回太多匹配项

c# - GridMvc 错误地在列中显示文本

asp.net-mvc - 在 IISExpress 上通过计算机名称访问 ASP.net Web api 时出现 400 错误请求

asp.net - MVC 2 AntiForgeryToken - 为什么是对称加密 + IPrinciple?