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.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read