views:

61

answers:

2

I've thought of a few ways of doing this but I want to get the community's view. I have a feeling that the answer is cringeworthily simple - I'm not afraid to look stupid (my kids took that fear away from me long ago!)

I'm writing an XML REST web service using MVC2. All XML Types that consumers of the web service will receive and send are governed by simple but extensive XSD, and these parameters will be bound from xml in the request body via a custom default model binder and value provider.

I have a goodly amount of controllers, each with a goodly amount of action methods (not excessive - just 'goodly' ;) ) - and in nearly every case these action methods are going to be accepting model types that are all reference types.

In practically every case it's going to 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" can be sent back.

What I want to do is to be able to validate parameters are not null before an action method is executed; and then to return an ActionResult that represents the Error to the client (for this I already have an XMLResult type) without the action method itself having to validdate the parameters itself.

So, instead of:

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

Something like:

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

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

It seems to me that either a base controller override of OnActionExecuting or a custom ActionFilter is the most likely way of doing this.

I'd also like to be able to extend the system so that it automatically picks up XML schema validation errors (added to ModelState during binding by a custom value provider) thus preventing the action method from continuing if any of the parameter values can't be loaded correctly because the XML request is badly formed.

+1  A: 

Well you could add constraints using regular expressions to individual route values. Then, if these constraints are not upheld, the action method will not be hit:

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

Alternatively you could create custom constraints to validate route values together as a pack. This would probably be a better strategy for you. Have a look here: Creating a Custom Route Constraint

Developer Art
Thanks-unfortunately this won't work because these parameters are bound from xml in the request body so not easily available to the routing engine; I don't want to be inspecting the request xml at the routing level to do this validation. This also would make it harder to return a nice error message.
Andras Zoltan
Have edited to highlight that these params will come from request body and that therefore this has to be done after model binding
Andras Zoltan
A: 

Here's the implementation that I've come up with (while waiting for any better ideas :) )

It's a generic approach and I think is pretty scalable - allowing for hopefully a similar kind of depth to parameter validation as you get with model validation at the same time as providing the error auto-respond functionality (when model state contains one or more errors) that I was looking for.

I hope this isn't too much code for an SO answer(!); I had a load of documentation comments in there that I've taken out to keep it shorter.

So, in my scenario I have two types of model error that, if they occur, should block execution of the action method:

  • Failed schema validation of the XML from which a parameter value will be constructed
  • Missing (null) parameter value

Schema validation is currently performed during model binding, and automatically adds model errors to the ModelState - so that's great. So I need a way to perform the auto-null check.

In the end I created two classes to wrap up the validation:

[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));
  }
}

With this I can then do this:

public void ActionMethod([RequiredParameter]MyModel p1){ /* code here */ }

But this on its own doesn't do anything of course, so now we need something to actually trigger the validation, to get the model errors and add them to model state.

Enter the 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);
  }
}

Whew! Nearly there now...

So, now we can do this:

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

To complete the puzzle we need something that can investigate the model errors and automatically respond if there are any. This is the least tidy of the classes (I hate the name and the parameter type used) and I'll probably change it in my project, but it works so I'll post it anyway:

[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);
}

In my application I have an XmlResult that takes a Model instance and serializes to the response using either DataContractSerializer or XmlSerializer - so I've then created RespondWithXmlModelErrorsAttribute that inherits from this last type to formulate one of those with the model as an Errors class that simply contains each of the model errors as strings. The Response Code is also automatically set to 400 Bad Request.

Thus, now I can do this:

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

In the case of web pages this last stage won't necessarily be required, since model errors are typically included in a re-rendering of page that sent the data in the first place, and the existing MVC approach suits this fine.

But for web services (either XML or JSON) being able to offload error reporting to something else makes writing the actual action method a lot easier - and much more expressive, I feel.

Andras Zoltan
accepting this answer as it's worked really well for me, and I think it's suitably extensible to be a really valid addition to our in-house Mvc extensions library.
Andras Zoltan