Keyvan Nayyeri is a Ph.D. student in Computer Science and previously held a B.Sc. degree in Applied Mathematics. He was born in Kermanshah, Kurdistan, Iran in 1984, and is currently living in San Antonio, TX. His primary research interests are Programming Languages & Compilers and Software Engineering.  He’s also a software architect and developer with focus on Microsoft development technologies as well as Open Source platforms. Keyvan is an avid community leader and contributor who has written four books for Wiley/Wrox and several articles for prominent community websites. Also he has contributed to many Open Source projects. As a result of his long-time contributions to the community, he has received several recognitions and awards from Microsoft, its partners, and community websites. Keyvan is a continues learner who loves to study, learn, and discover new technologies everyday, and is enthusiast for serving to the humankind through his research and contributions. For a long time he has been blogging about various topics on his blog that has become a rich resource for software developers. His blog is available at www.nayyeri.net. Keyvan has posted 36 posts at DZone. View Full User Profile

Custom Route Constraint in ASP.NET MVC

04.16.2009
| 7284 views |
  • submit to reddit

In a recent blog post Simone did a great job by listing all the 13 major extensibility points available in ASP.NET MVC with a short description and references for further reading.

It’s been a while that I wanted to write more about ASP.NET MVC but like all other topics, it has been a decision only! However, I thought that it’s a good start point to go over these extensibility points with some introductory posts as starter guides. So here is the first part covering custom route constraints in ASP.NET MVC.

As you may know, ASP.NET MVC (and hopefully the next versions of ASP.NET WebForms) has a powerful routing mechanism to route requests to their corresponding controller and action methods with appropriate parameters with some basic constraint declaration mechanisms.

While there is a simple way to add routes to the routing engine, it’s also possible to validate requests based on your business scenarios and more complex constraints. This is an extensibility point in ASP.NET MVC routing mechanism that is called route constraint in which you define custom constraints that check the validity of requests, and notify the routing engine if it should route this request with this route object and pattern or not.

Let me clarify this with a very common example that you’ve seen in many sites. Many of the sites and blogs have an archive calendar where you can find posts for a specific year, month or day by navigating to a unique URL that includes the year, month, and day numbers. Of course, these numbers are integer but their range should be limited somehow. For instance, you don’t expect a year number to be smaller than 1900 or larger than 2100, or a month number to be smaller than 1 or larger than 12. In a more complex case, the range of day numbers can vary by month and there is a dependency between the month and day numbers. You would agree that it should be very good to check these ranges and validate incoming requests to make sure they contain valid values.

You can add a route constraint by implementing the IRouteConstraint interface in ASP.NET MVC, and adding it to your routes through a few steps that I’ll write in a moment. IRouteConstraint has a single Match function that returns a Boolean value which determines whether this request should be processed by the route object or not. It also has some parameters:

  • httpContext: The current HttpContext object in which this request is being processed.
  • route: The route object that is going to process this request.
  • parameterName: The name of the parameter in route pattern that is being validated by this constraint.
  • values: The key/value pair of request parameters and their corresponding values extracted from request URL based on the route pattern.
  • routeDirection: A RouteDirection enumeration value that determines if the route is requested by client or is created based on the route definition.

Here I write a simple ASP.NET MVC application that simulates such an archive system but only displays the year, month, and day numbers in the view. I write some constraints to check the abovementioned ranges and exclude invalid requests from routing engine.

First, I create an ArchiveController with an Index action method to display the values of year, month, and day parameters.

using System.Web.Mvc;

namespace RouteConstraintSample.Controllers
{
public class ArchiveController : Controller
{
public ActionResult Index(int year, int month, int day)
{
ViewData["Year"] = year;
ViewData["Month"] = month;
ViewData["Day"] = day;

return View();
}
}
}

I also create a view to display these values.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Archive
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>
Archive</h2>
<p>
Here are the information extracted from URL:
</p>
<br />
Year:
<%= ViewData["Year"] %>
<br />
Month:
<%= ViewData["Month"]%>
<br />
Day:
<%= ViewData["Day"]%>
<br />
</asp:Content>

In the main step I need to create three separate constraints for year, month, and day validation that later I will apply in my routing definitions.

I start with a simple constraint for year values called YearRouteConstraint.

using System;
using System.Globalization;
using System.Web;
using System.Web.Routing;

namespace RouteConstraintSample.Routes
{
public class YearRouteConstraint : IRouteConstraint
{
#region IRouteConstraint Members

public bool Match(HttpContextBase httpContext, Route route,
string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if ((routeDirection == RouteDirection.IncomingRequest) &&
(parameterName.ToLower(CultureInfo.InvariantCulture) == "year"))
{
try
{
int year = Convert.ToInt32(values["year"]);
if ((year >= 1900) && (year <= 2100))
return true;
}
catch
{
return false;
}
}
return false;
}

#endregion
}
}

The logic behind this implementation is simple. I check to make sure that this request is coming from a client, and also check that the parameter name is equal to my expected value. Of course, this may not be a big deal because you can map the correct parameter to this constraint, but it’s always better to close doors to avoid unexpected issues in the lower levels of code. In the main body of the Match function I extract the value of the year parameter and check its range. If it falls in the range then I return the true value to process the request by the route otherwise ignore it.

The MonthRouteConstraint has a very similar logic that checks the value of the month to be between 1 and 12.

using System;
using System.Globalization;
using System.Web;
using System.Web.Routing;

namespace RouteConstraintSample.Routes
{
public class MonthRouteConstraint : IRouteConstraint
{
#region IRouteConstraint Members

public bool Match(HttpContextBase httpContext, Route route,
string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if ((routeDirection == RouteDirection.IncomingRequest) &&
(parameterName.ToLower(CultureInfo.InvariantCulture) == "month"))
{
try
{
int month = Convert.ToInt32(values["month"]);
if ((month >= 1) && (month <= 12))
return true;
}
catch
{
return false;
}
}
return false;
}

#endregion
}
}

The last constraint is a good example of constraints that validate the value of a parameter that depends on other parameter.

using System;
using System.Globalization;
using System.Web;
using System.Web.Routing;

namespace RouteConstraintSample.Routes
{
public class DayRouteConstraint : IRouteConstraint
{
#region IRouteConstraint Members

public bool Match(HttpContextBase httpContext, Route route,
string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if ((routeDirection == RouteDirection.IncomingRequest) &&
(parameterName.ToLower(CultureInfo.InvariantCulture) == "day"))
{
try
{
int month = Convert.ToInt32(values["month"]);
int day = Convert.ToInt32(values["day"]);

if (day < 1)
return false;

switch (month)
{
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
if (day <= 31) return true;
break;
case 2:
if (day <= 28) return true;
break;
case 4:
case 6:
case 9:
case 11:
if (day <= 30) return true;
break;
}
}
catch
{
return false;
}
}
return false;
}

#endregion
}
}

DayRouteConstraint checks the value of day parameter based on the value of month, so it can’t exceed the valid value of the number of days in that months. To do this, it extracts the value of month from the RouteValueDictionary along with the value of day.

The last step is to connect all these things together so ASP.NET MVC can run the application. This step is nothing but defining the routes.

using System.Web.Mvc;
using System.Web.Routing;
using RouteConstraintSample.Routes;

namespace RouteConstraintSample
{
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);

routes.MapRoute(
"Archive",
"archive/{year}/{month}/{day}",
new
{
controller = "Archive",
action = "Index",
year = "",
month = "",
day = ""
},
new
{
year = new YearRouteConstraint(),
month = new MonthRouteConstraint(),
day = new DayRouteConstraint()
}
);
}

protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
}

I add a new route to my route collection called Archive with “archive/{year}/{month}/{day}” pattern that everybody can understand. Like the regular routes I also set the default values for the route, but add a constraint object to it. This constraint object maps each parameter in the pattern to its constraint instance, so these values can be validated.

Now I run the application with a valid request pattern to get the result.

Output

I also can send a request for the 31st day of April that is not valid and I get an error. Obviously, the better implementation had to display a custom page with appropriate error message, but this is just a sample application!

Output

As the last note, you may wonder why I wrote three separate constraints for these three values while I could extract all the values in a single constraint and validate them. While this is possible, I avoided it because it’s not the proposed practice and usage, and also it’s not always the case.

You can download the sample code package for this post from here.

 

References
AttachmentSize
RouteConstraintSample.zip247.31 KB
Published at DZone with permission of its author, Keyvan Nayyeri. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)