Prevent execution of the MVC Action method if the parameter is zero

I thought of several ways to do this, but I want to get a community view. I have a feeling that the answer is extremely simple - I'm not afraid to look stupid (my children have long left me!)

I am writing an XML REST web service using MVC2. All types of XML that web service consumers receive and send are controlled by a simple but extensive XSD, and these parameters will be bound from the xml in the request body using a custom binding provider and default provider.

I have a large number of controllers, each of which has a good number of action methods (not excessive - just "good";)), and in almost every case, these action methods will take on model types that are reference types.

In almost every case, it will be an error for the caller not to provide these parameter values, and as such a standard error message, such as "The parameter {name} type:{ns:type} is required", may be sent back.

What I want to do is to verify that the parameters are non-zero before the action method is executed; and then return an ActionResult that represents the error to the client (for this I already have a type XMLResult), without the action method itself having to validate the parameters themselves.

So, instead of:

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

Sort of:

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

I know that this is exactly the kind of cross cutting that can be achieved in MVC.

It seems to me that the most likely way to do this is to either override the base controller OnActionExecutingor a custom ActionFilter.

, XML ( ModelState ), , - , XML- .

+5
2

, ( :))

, , , , ( ), .

, SO- (!); , , .

, , , , :

  • 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 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, , a 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), , - , .

+2

, , . , , :

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

. , . :

+1

All Articles