Parameter validation in ASP .NET MVC 4

Routing in ASP .NET MVC is a great feature. Among other things, it lets you create stateless applications more easily and in a more elegant way and break away from practices that rely on cookies/session variables. If you have a controller action like the following:

public class SiteController : Controller
{
	public ActionResult News(int id)
	{
		// Do some interesting stuff with that id parameter
	}
}

And if you leave the default route that comes with most MVC project templates, you will have that action automatically respond to a route (read URL) like /Site/News/1. How cool is that?

However, there’s a small catch in the route-matching mechanism. If the route is requested with an invalid id parameter (as in /Site/News/blah) or a missing one (/Site/News) you will get an awful error:

System.ArgumentException: The parameters dictionary contains a null entry for parameter ‘id’ of non-nullable type ‘System.Int32’ for method ‘System.Web.Mvc.ActionResult News(Int32)’ in ‘Tbt.Controllers.SiteController’. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.

Yep, that’s a 500 (Internal Server Error) page. That’s basically because the routing engine could find the controller and the action but had trouble parsing the parameter (that happens to be a value type). That might not sound like a big deal (and probably isn’t in most projects); after all, who will be using our web app’s URLs in ways they’re not intended to be used? Well, in my experience, search engines’ crawlers seem to do that. And if good SEO is a concern, you might not want to make the search engines “think” your app crashes for no apparent reason.

Of course, there’s an easy, straightforward way to fix that. You guessed right: make the parameters nullable.

public class SiteController : Controller
{
	public ActionResult News(int? id)
	{
		if (!id.HasValue)
		{
			// Redirect or return the appropriate HTTP code here
		}
		int realId = id.Value;
		// Do some interesting stuff with the realId variable
	}
}

However, depending on the amount of actions the application may have, this can become a cumbersome, tedious task and can distract us from our main goal: write awesome code. That’s what made me look for a better solution, and this is what I came up with.

Using the ActionMethodSelectorAttribute class

According to the MSDN documentation, the ActionMethodSelectorAttributeRepresents an attribute that is used to influence the selection of an action method“. And that’s exactly what we will do: intercept the request and make sure it fulfills all the requirements (in this case, having valid values for all non-nullable parameters); if not, the app will automatically issue a 404 (Not found) HTTP status code and our action method will not be called. Here’s the code:

Update (2015-05-27): Added default value check as suggested by Steven Sproat.

using System;
using System.Reflection;
using System.Web.Mvc;

public class NonNullableParametersAttribute : ActionMethodSelectorAttribute
{
	public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
	{
		var methodParams = methodInfo.GetParameters();

		foreach (var parameterInfo in methodParams)
		{
			if (parameterInfo.HasDefaultValue)
			{
				continue;
			}

			var paramType = parameterInfo.ParameterType;
			if (!IsSimpleType(paramType))
			{
				continue;
			}

			var value = controllerContext.Controller.ValueProvider.GetValue(parameterInfo.Name);
			if (value == null || value.AttemptedValue == null || !CanParse(value.AttemptedValue, paramType, value.Culture))
			{
				return false;
			}
		}

		return true;
	}

	private static bool IsSimpleType(Type t)
	{
		return t.IsPrimitive || t.IsEnum || t == typeof(Decimal) || t == typeof(DateTime) || t == typeof(Guid);
	}

	private static bool CanParse(object rawValue, Type destinationType, IFormatProvider formatProvider)
	{
		try
		{
			var test = Convert.ChangeType(rawValue, destinationType, formatProvider);
			return test != null;
		}
		catch
		{
			return false;
		}
	}
}

Then, we decorate our action methods like so:

[NonNullableParameters]
public ActionResult News(int id)
{
	// Do your stuff using that id parameter
}

The NonNullableParametersAttribute will handle any number of parameters of primitive type (int, double, etc) and some non-primitive value types (decimal, DateTime, Guid) for you.

5 thoughts on “Parameter validation in ASP .NET MVC 4

    • That’s a different approach that would work for some cases. The problem is that it will handle any NullReferenceException, regardless of whether it was caused because you tried to access the Value property of a nullable parameter or the exception occurred down the stack in your business logic. Furthermore, to mimic what this attribute does, you would also have to create a special implementation of the HandleErrorAttribute to redirect to a Not Found page (feedback for the user) and issue a 404 status code (feedback for search engines), as opposed to showing a default error page with a 500 or 200 status code.

      • You are right.. I was actually contemplating my options…, The HandleError is merely a generic catch-all error handler which isn’t very flexible.

        I haven’t use either solution for now but I will be definitely use your solution should the need arise.

        Thanks!

  1. I had to add the following code at the top of the ‘foreach’ loop, because if an Action method parameter was given a default value, not providing a value to that parameter through a URL would trigger a 404 error:

                if (parameterInfo.HasDefaultValue)
                    continue;
    

    Great post, thanks!
    Steven

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s