Creating Route Constraints to Restrict Requests

Introduction

In ASP.NET MVC applications, the requests sent by the client browser are actually "routes". Depending on the route definitions defined by the developer the requests then reach a particular controller. It is possible that requests may contain invalid route parameters, for example, a string where an integer is expected. In such cases errors will occur when the control reaches the controller. Wouldn't it be nice to trap such errors at the route level itself? That is what route constraints allow you to do. In this article you will learn to use regular expressions to avoid improper route parameters. You will also learn to create a custom route constraint mechanism for advanced validation of route parameters.

Defining Routes

In order to understand how route constraints work you will develop a sample ASP.NET MVC web application that displays a list of blog posts and allows you to view an individual post. The data required by the application is stored in a SQL Server database table named Posts. The schema of the Posts table is shown below:

The schema of the posts
Figure 1: The schema of the Posts table

The individual posts can be accessed via routes as shown below:

http://localhost:1034/posts/2010/12/13/1

Notice the above route carefully, especially the last four parameters of the route URL. As you might have guessed 2010, 12 and 13 are year, month and day respectively. Together they represent PublishDate of a particular post. The last parameter (1) is the PostId.

Now consider the following route URLs.

http://localhost:1034/posts/200/72/0/1

http://localhost:1034/posts/aaa/bbb/ccc/d

http://localhost:1034/posts/1a2b/3c/4d/567

Without any route constraints in place the above routes are also valid routes from an ASP.NET MVC framework point of view. However, from an application logic point of view they are clearly invalid because valid PublishDate and PostId values cannot be constructed from them. Using route constraints you can ensure that only valid values are passed to the controller.

Now that you have a basic idea of what route constraints are, let's see how to use them. To begin, create a new empty ASP.NET MVC 3 application. Open the Global.asax file and define a route (in addition to the Default route) to accept the URLs as discussed earlier. You can register a new route in the RegisterRoutes() method.

routes.MapRoute(
"BlogPosts",
"posts/{year}/{month}/{day}/{postid}",
new { controller = "BlogPosts", action = "ShowPost"}
);

The above piece of code defines a new route named BlogPosts. The format of the route is posts/{year}/{month}/{day}/{postid}. The ShowPost method of the BlogPosts controller will handle this route. You will create the BlogPosts controller later.

Creating the Controller and Views

Now add a new LINQ to SQL class to the MVC application and drag and drop the Posts table from Server Explorer onto the design surface of the .dbml file. Doing so will create a LINQ to SQL class - Post.

LINQ to SQL class - Post
Figure 2: LINQ to SQL class - Post

Next, add a new class to the Models folder and name it 'PostData'. The PostData class is used to carry post related information (PostId for example) to the ShowPost() action you will create later.

public class PostData
{
    public int Year { get; set; }
    public int Month { get; set; }
    public int Day { get; set; }
    public int PostId { get; set; }
}

The PostData class simply contains four public properties viz. Year, Month, Day and PostId. Notice that these properties correspond to the route parameters you defined earlier.

Add a new controller - BlogPostsController - and write two action methods viz. Index() and ShowPost() as shown below :

public ActionResult Index()
{
    DataClasses1DataContext db = new DataClasses1DataContext();
    var results = from rows in db.Posts
                    select rows;
    return View(results);
}
 
public ActionResult ShowPost(PostData data)
{
    DataClasses1DataContext db = new DataClasses1DataContext();
    var results = from rows in db.Posts
                    where rows.PostId == data.PostId
                    select rows;
    return View(results);
}

The Index() action method fetches all the blog posts from the Posts table using a LINQ to SQL query and renders the Index view by passing the results as a model. The ShowPost() action method accepts a parameter of type PostData and fetches just one blog post matching a specific PostId. The ShowPost view is then rendered.

The Index view renders the model data in a table. The HTML markup of the Index view is shown below :

<h1>List of Posts</h1>
<table border="1" cellpadding="3">
<%
foreach (var row in Model){
%>
<tr>
<td>
<%= row.PostId %>
</td>
<td>
<%= row.Title %>
</td>
<td>
<%= row.PublishDate %>
</td>
<td>
<a href='<%= "posts/" + row.PublishDate.Year + "/" + row.PublishDate.Month.ToString("00") + "/" + row.PublishDate.Day.ToString("00") + "/" + row.PostId %>'>Show</a>
</td>
</tr>
<%}%>
</table>

Notice how the "Show" hyperlink is rendered in the form posts/<yyyy>/<mm>/<dd>/<postid>.

The ShowPost view displays just one post and is shown below :

<body>
<%
int postid=0;
foreach (var row in Model){
postid = row.PostId;
%>
<h1><%: row.Title %></h1>
<%: row.Content %>
<hr />
<p><em>Published on :<%: row.PublishDate %></em></p>
<%}%>
</body>

Before you proceed further, run the application and ensure that the Index and ShowPost views are working as expected. The following figures show them in action :

List of Posts
Figure 3: List of Posts

Show Post View
Figure 4: Show Post View

After ensuring that everything we developed so far is working as expected, deliberately enter some invalid route, say posts/aaaa/bb/cc/d and observe the data parameter of the ShowPost action method. The following figure shows the data parameter in the Visual Studio Watch window.

Visual Studio Watch Window
Figure 5: Visual Studio Watch Window

Notice how the Year, Month, Day and PostId parameters are passed as 0 because the corresponding values (aaaa, bb, cc and d) cannot be converted to integer types. As a result the ShowPost view will render a blank page since no data has been returned. In our example the ShowPost() action method contains little amount of code. Imagine a case where a lot more processing is happening only to generate errors at a later stage. By putting route constraints you can prevent invalid route parameters at the route level itself. You can add route constraints in two ways:

  • By using regular expressions.
  • By creating and using custom route constraint classes.

Let's look at both of the techniques one at a time.

Using Regular Expressions to Restrict Routes

Open the Global.asax file again and modify the route definition you added earlier.

routes.MapRoute(
"BlogPosts",
"posts/{year}/{month}/{day}/{postid}",
new { controller = "BlogPosts", action = "ShowPost" },
new { year = @"^dddd$", month = @"^dd$", day = @"^dd$", postid = @"d+" }
);

Notice the last parameter of the MapRoute() method. It represents route constraints in the form of regular expressions. Regular expressions define the pattern for year, month, day and postid route parameters. In all the regular expressions used in the above piece of code, d stands for a digit. The regular expressions starting with ^ and end with $ ensure that only certain number of digits ( 4 for year, 2 for month and day) are allowed. The + sign used for the postid parameter indicates that it can take one or more number of digits.

Now, run the application again and try to enter some invalid route. This time before the application control reaches the ShowPost() action method, an HTTP 404 error is displayed as shown below :

An HTTP 404 Error
Figure 6: An HTTP 404 Error

Creating Custom Route Constraints

The route constraint technique you used in the preceding case ensures that only integers are entered as route parameters. However, it doesn't check whether the specified year, month and day belong to the postid under consideration (nor whether given year, month and day has a valid postid). To take care of this sort of validation you will need to create a custom route constraint class. Let's see how.

To create a custom route constraint, you need to create a class that implements the IRouteConstraint interface. Then you need to write implementation for the Match() method. For example, YearConstraint class that validates the year parameter of the route is shown below :

public class YearConstraint : IRouteConstraint
{
    string strRegEx = string.Empty;
 
    public YearConstraint(string regex)
    {
        strRegEx = regex;
    }
 
    public bool Match(HttpContextBase httpContext, Route route, 
           string parameterName, 
           RouteValueDictionary values, 
           RouteDirection routeDirection)
    {
        try{
        int value = Convert.ToInt32(values[parameterName]);
        int postId=Convert.ToInt32(values["postid"]);
        DataClasses1DataContext db = new DataClasses1DataContext();
        var item = from items in db.Posts
                    where items.PostId == postId
                    select items;
        Post p = item.SingleOrDefault();
        if (p == null)
        {
            return false;
        }
        if (p.PublishDate.Value.Year == value)
        {
            return Regex.IsMatch(value.ToString("0000"), strRegEx);
        }
        else
        {
            return false;
        }
        }
        catch
        {
          return false;
        }
    }
}

The class accepts a regular expression in the constructor for pattern matching. The Match() method has five parameters viz. HttpContext, Route, string, RouteValueDictionary and RouteDirection. Inside the Match() method we retrieve the value of a route parameter (year in this case) using values dictionary and parameterName. We also retrieve post ID from the dictionary. The code then fetches a post with matching postId and checks if the year, as supplied in the route and the year from PublishDate, match. It also checks the pattern using Regex class. The Match() method returns true if the route parameter matches our expectations otherwise it returns false.

You will need to implement similar logic for MonthConstraint, DayConstraint and PostIdConstraint classes (we won't discuss them here.)

Once all four custom route constraint classes are defined, you need to modify the route definition from Global.asax as shown below :

routes.MapRoute(
"BlogPosts",
"posts/{year}/{month}/{day}/{postid}",
new { controller = "BlogPosts", action = "ShowPost" },
new { year = new YearConstraint(@"^dddd$"), 
month = new MonthConstraint(@"^dd$"), 
day = new DayConstraint(@"^dd$"), 
postid = new PostIdConstraint(@"d+") }
);

Notice the last parameter of the MapRoute() method. This time we instantiate custom route constraints (YearConstraint, MonthConstraint, DayConstraint and PostIdConstraint) by passing regular expressions for pattern matching. When you run the application the Match() method of all the custom route constraint classes is executed to ensure that all route parameters are as expected and only then control reaches the ShowPost() method. If any of the Match() method returns false, an HTTP 404 error is thrown as discussed before.

Summary

In ASP.NET MVC applications, the client browser sends requests in the form of "routes". At times you may need to ensure that route parameters follow a certain predefined pattern or condition. The MVC framework allows you to define route constraints to restrict the route parameter values. The route constraints can be defined either by regular expressions or by custom route constraint classes. A custom route constraint is a class that implements IRouteConstraint interface. The Match() method of the custom route constraint is responsible for deciding whether a route parameter value is acceptable or not. Using route constraints also ensures that control reaches an action method only after validating the route parameters.



About the Author

Bipin Joshi

Bipin Joshi is a blogger and writes about apparently unrelated topics - Yoga & technology! A former Software Consultant by profession, Bipin has been programming since 1995 and has been working with the .NET framework ever since its inception. He has authored or co-authored half a dozen books and numerous articles on .NET technologies. He has also penned a few books on Yoga. He was a well known technology author, trainer and an active member of Microsoft developer community before he decided to take a backseat from the mainstream IT circle and dedicate himself completely to spiritual path. Having embraced Yoga way of life he now codes for fun and writes on his blogs. He can also be reached there.

Related Articles

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

  • Live Event Date: November 13, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT APIs can be a great source of competitive advantage. The practice of exposing backend services as APIs has become pervasive, however their use varies widely across companies and industries. Some companies leverage APIs to create internal, operational and development efficiencies, while others use them to drive ancillary revenue channels. Many companies successfully support both public and private programs from the same API by varying levels …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds