How to add request validation errors to ModelStateDictionary in ASP.NET MVC?
Investi开发者_运维问答gating the security of a system I'm building with ASP.NET MVC 2 led me to discover the request validation feature of ASP.NET - a very neat feature, indeed. But obviously I don't just want to present the users with the Yellow Screen of Death when they enter data with HTML in, so I'm out to find a better solution.
My idea is to find all the fields that have invalid data and add them to the ModelStateDictionary
before invoking the action such that they automatically appear in the UI as error messages. After googling this a bit it appears that no one have implemented this before which I find puzzling since it seems so obvious. Does anyone here have a suggestion on how to do this? My own idea is to supply a custom ControllerActionInvoker
to the controller, as described here, that somehow checks for this and modifies the ModelStateDictionary
but I'm stuck on how to do this last bit.
Just catching HttpRequestValidationException
exceptions does not seem a useful approach since it does not actually contain all the information I need.
I've answered the question myself but I'd still be very interested to hear of any solutions which are more elegant/robust.
Having looked a bit at how MVC does the model binding I've come up with a solution myself. I extend the Controller
class with a custom implementation that overrides the Execute
method like so:
public abstract class ExtendedController : Controller
{
protected override void Execute(RequestContext requestContext)
{
ActionInvoker = new ExtendedActionInvoker(ModelState);
ValidateRequest = false;
base.Execute(requestContext);
}
}
For me to control when request validation occurs I've added the following to the web.config
:
<httpRuntime requestValidationMode="2.0"/>
The meat of the action happens in the custom implementation of the ControllerActionInvoker
class:
public class ExtendedActionInvoker : ControllerActionInvoker
{
private ModelStateDictionary _modelState;
private const string _requestValidationErrorKey = "RequestValidationError";
public ExtendedActionInvoker(ModelStateDictionary modelState)
{
_modelState = modelState;
}
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
{
var action = base.FindAction(controllerContext, controllerDescriptor, actionName);
controllerContext.RequestContext.HttpContext.Request.ValidateInput();
return action;
}
protected override object GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
{
try
{
return base.GetParameterValue(controllerContext, parameterDescriptor);
}
catch (HttpRequestValidationException)
{
var fieldName = parameterDescriptor.ParameterName;
_modelState.AddModelError(fieldName, ModelRes.Shared.ValidationRequestErrorMessage);
_modelState.AddModelError(_requestValidationErrorKey, ModelRes.Shared.ValidationRequestErrorMessage);
var parameterType = parameterDescriptor.ParameterType;
if (parameterType.IsPrimitive || parameterType == typeof(string))
{
return GetValueFromInput(parameterDescriptor.ParameterName, parameterType, controllerContext);
}
var complexActionParameter = Activator.CreateInstance(parameterType);
foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(complexActionParameter))
{
object propertyValue = GetValueFromInput(descriptor.Name, descriptor.PropertyType, controllerContext);
if (propertyValue != null)
{
descriptor.SetValue(complexActionParameter, propertyValue);
}
}
return complexActionParameter;
}
}
private object GetValueFromInput(string parameterName, Type parameterType, ControllerContext controllerContext)
{
object propertyValue;
controllerContext.RouteData.Values.TryGetValue(parameterName, out propertyValue);
if (propertyValue == null)
{
propertyValue = controllerContext.HttpContext.Request.Params[parameterName];
}
if (propertyValue == null)
return null;
else
return TypeDescriptor.GetConverter(parameterType).ConvertFrom(propertyValue);
}
}
What this does is to perform request validation after the action has been found. This will not immediate cause an error if the request is invalid but when GetParameterValue
is called it will throw an exception. To avoid this I override this method and wrap the base call in a try-catch. If an exception is caught I basically re-implement the model binding (I make no promises about the quality of this code) and add an error to the ModelStateDictionary
object for the value.
As a bonus, as I wanted to return an error in a standard format for my ajax methods I also added a custom implementation of the InvokeActionMethod
.
protected override ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters)
{
if (_modelState.ContainsKey(_requestValidationErrorKey))
{
var errorResult = new ErrorResult(_modelState[_requestValidationErrorKey].Errors[0].ErrorMessage, _modelState);
var type = controllerContext.Controller.GetType();
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
if (methods.Where(m => m.Name == actionDescriptor.ActionName).First().ReturnType == typeof(JsonResult))
return (controllerContext.Controller as ExtendedControllerBase).GetJson(errorResult);
}
return base.InvokeActionMethod(controllerContext, actionDescriptor, parameters);
}
精彩评论