In ASP.NET MVC, the default signature for a Details action includes an Int32 method argument. The system works fine when the expected data is entered, and the happy path is followed, but put in an invalid value or no value at all, and an exception explodes all over the user.
The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.Web.Mvc.ActionResult Details(Int32)' in 'MvcSampleApplication.Controllers.WidgetController'.
The following solution applies to the first version of the ASP.NET MVC framework. If you are looking for a solution in the ASP.NET MVC 2 framework, try Validating ASP.NET MVC 2 Route Values with DefaultValueAttribute.
By default, the third section of an ASP.NET MVC Route is the Id, passed as a method argument to a controller action. This value is a string, defaulting to String.Empty, but it can be parsed to other types such as an integer for a default Details action. The problem stems from when invalid values or missing values are passed to these integer-expecting actions; MVC handles the inability to parse the value into an integer, but then throws an exception trying to pass a null value to a Controller Action expecting a value type for a method argument.
A common solution is to convert the method argument to a nullable integer, which will automatically cause the argument to be null when the route value specified in the URL is an empty or non-integer value. The solution works fine, but seems a little lame to me; I want to avoid having to check HasValue within every action. I have to check for invalid identity values anyway (a user with an id of –1 isn’t going to exist in my system), so I would much rather default these invalid integers to one of these known, invalid values.
Using a custom ActionFilterAttribute, I can accomplish my goal. Prior to executing my Controller Action, an ActionFilterAttribute can validate that the specified route value meets an expected type, and correct the value to a default value if this validation fails; this will allowing me to keep that method argument as a value type integer and avoid Nullable<int>.HasValue.
ForceIntegerRouteValueAttribute class
public sealed class ForceIntegerRouteValueAttribute : ActionFilterAttribute
{
private readonly int _defaultValue;
private readonly string _routeValueName;
public ForceIntegerRouteValueAttribute(string valueName, int defaultValue)
{
_routeValueName = valueName;
_defaultValue = defaultValue;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
int testValue;
if (!context.ActionParameters.ContainsKey(_routeValueName) ||
!int.TryParse(context.RouteData.Values[_routeValueName].ToString(),
out testValue))
{
context.RouteData.Values[_routeValueName] = _defaultValue;
context.Result = new RedirectToRouteResult(context.RouteData.Values);
}
}
}
The attribute accepts a string matching the name of the route value to analyze and a default value to assign to the route value should the validation fail. The override on OnActionExecuting will cause the validation to fire immediately prior to the Action being executed. The method attempts to parse the route value to an integer, and if it fails it will set the route value to the default value and restart processing of the route. RedirectToRouteResult restarts the route processing over again, since to get to this point, the method must have changed a route value, and without the redirect, the action would continue on with the original value. This will also redirect the browser to route matching the new route value, such as redirecting ~/Widgets/Details/Foo to ~/Widgets/Details/0.
Usage
public class WidgetsController : Controller
{
[ForceIntegerRouteValue("id", 0)]
public ActionResult Details(int id)
{
return View();
}
}
By simply adding the attribute to the Action, I can now validate that my identity is an integer, allowing for some level of trust into user input values, and do so without having to laden my code with unnecessary value checks. This same idea can be used for any sort of pre-filtering, which could also include checking that the number contains no more than two decimal places. Just don’t go too far with this idea. Familiarize yourself with Route Constraints, too, as there will be times that a constraint can better serve your business needs than an Action Filter. But for areas where this solution does serve well, such as eliminating Nullable<int> in Action arguments, this option just seems cleaner to me. And in the ASP.NET MVC World of keeping Controllers as light as possible, I like clean.