Introduction
In the previous article I introduced the concept of extensibility, and described where ASP.NET MVC can be extended: With this article I’ll be starting a guided tour around these extensibility points, beginning with the first ones that are encountered when a request hits the application: the routing module.
Before I demonstrate how to extend the routing module, let’s quickly review the way that incoming requests are evaluated and matched with the correct route.
Note: Strictly speaking, the routing engine is part of ASP.NET, and not something specific to ASP.NET MVC, but I’m covering it because it is a fundamental part of the processing pipeline.
How the routing module works
Looking at the routing module from a very high level, what it does is to take in the URL of the incoming request and gives out an instance of the IRouteHandler
that will create, via its GetHandler
method, the IHttpHandler
to process the request.
What happens, in more detail, is that the request is checked against all the routes that are defined in the routing table of the application:
- First the matching is done based on the URL pattern and the default values for the URL parameters
- Then, if the URL pattern matches, the route parameters are populated with the values extracted from the URL segments
- After that, the route parameters are validated against the route constraints: these can either be regular expressions or (and here it comes our first extensibility point) any classes that implement
IRouteConstraint
.
As soon as a match is found, the process of scanning of the global routing table stops, the route handler (our second extensibility point for the article) specified for the route is instantiated, the Http Handler is created and finally executed (via its ProcessRequest
method). If no match is found, a ‘Page Not Found’ error (Http error 404) is returned to the client.
Hopefully I haven’t lost you yet. In case I did, and if some of the terms I used are new to you, I recommend you first go reading an introductory explanation on what routing in ASP.NET is and how it works, such as the ASP.NET Routing page on MSDN or one of the many books on ASP.NET MVC.
How to specify routes
Let’s now see how this workflow works with a code sample, which also shows how to define routes and basic constraints.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
routes.MapRoute("BlogArchive", "{year}/{month}/{day}", new { controller = "Blog", action = "List", month = "1", day = "1" }, new { year = @"\d{2}|\d{4}", month = @"\d{1,2}", day = @"\d{1,2}" } ); routes.MapRoute("Post", "{title}", new { controller = "Blog", action = "Post"} ); routes.MapRoute("Tags", "tags/{tag}", new { controller = "Blog", action = "Tags" } ); routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); |
This is the routing table for a simple blog engine, with three types of routes defined: the single post, the archive by date, and the archive by tag, and the default route at the end.
The first route is matched by any URL with one to three segments, all formed by digits: it will match /2011 but also /2011/11 and /2011/11/25. The second route will match only URLs with just one segment that is not a 2 or 4 digits number. If the URL is instead made by two non-numeric segments, the first of which is “tags”, the third route will be selected. And finally, if none of the specific routes is selected, the default route will be selected.
For example, if the browser sends a request for the URL http://example.com/Authors/List
, the routing module will first try to match it with the “BlogArchive” route: the pattern matches, but not the constraints on the format of the parameters. The rest of the route table is evaluated, but without finding a matching pattern, so the default route definition will be used, and, due to how the default route handler is designed, the processing will move to inside the AuthorsController
class and its List
method will be executed.
You might have noticed that nowhere in the code has the route handler to use been specified: the reason is that the MapRoute
method creates a Route
specifying always MvcRouteHandler
, the default route handler for ASP.NET MVC, as route handler.
Easier debugging of routes
When the route table grows big and complex, then debugging, and understanding why it doesn’t work as expected, can be pretty difficult, especially if the result is a 404 error (which means no route matched). To help you with that, Phil Haack, PM of the ASP.NET MVC team, wrote a simple yet powerful RouteDebugger. To install it into you application all you have to do is get it via Nuget:
1 |
Install-Package RouteDebugger |
When installed it shows all the routes configured in your application, with their parameters, default values, constraints, all the routes that match the current request and the route parameters that will be passed along to the route handler. With the previous example the output is as in Fig 2.
Another interesting debugging tool, not just for routes but for all the pipeline of ASP.NET MVC is Glimpse, also installable via Nuget, with the command:
1 |
Install-Package Glimpse.mvc3 |
Compared to Haack’s RouteDebugger, Glimpse has the disadvantage of not giving any information when there is no match. In the example, its output would have been as in Fig 3.
Let’s now start exploring the extensibility points, starting with custom route constraints.
Custom Route Constraint
A route constraint, as we have seen before, is used when you need more control over the route matching. In the code listing #1 I’ve specified that I wanted to select the BlogArchive route only if the URL represented a date (so, 3 numbers). But something it was not possible with a regular expression was to make sure the date was valid. For that I need something that can access all the URL parameters: a custom route constraint.
The interface to implement
To create a custom Route Constraint, you need to implement the IRouteConstraint
interface, which only has one method: Match
.
1 2 3 4 |
public interface IRouteConstraint { bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) } |
The parameters supplied to the method are:
httpContext
: the http context, which gives access to the"HttpContextBase
class with the complete request, server, application objectsroute
: the route object this constraint belongs toparameterName
: the name of the route parameter being checkedvalues
: all the URL parameters for the routerouteDirection
: whether the check is being performed when looking for the match for an incoming request or URL is being generated.
The method must return true
if the parameter is valid, or false
otherwise.
Sample Implementation
Let’s see how to implement the date validation constraint:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class IsDateValidConstraint : IRouteConstraint { public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { if (routeDirection == RouteDirection.IncomingRequest) { DateTime date; return DateTime.TryParse(String.Format("{0}/{1}/{2}", values["year"], values["month"], values["day"]), CultureInfo.InvariantCulture, DateTimeStyles.None, out date); } return true; } } |
The code itself is pretty easy: it just tries to create a date, and if it fails (for example the browser was asking for the blog post for the 31st of February) it returns false, and the routing module passes to the next route to try and find the right one.
The new constraint is then specified in the route definition just by adding it to the dictionary of constraints:
1 2 3 4 5 6 7 |
routes.MapRoute("BlogArchive", "{year}/{month}/{day}", new { controller = "Blog", action = "List", month = "1", day = "1" }, new { year = @"\d{2}|\d{4}", month = @"\d{1,2}", day = @"\d{1,2}", date = new IsValidDateConstraint() } ); |
As just shown in this example, a custom route constraint doesn’t have to correspond to an actual route parameter (in the example it was specified for a non-existent parameter called “date”). It can also be specified for a fictitious parameter, thus performing the validation also based on information coming from other sources, like, the request object or, as in the case, the other parameters.
Other possible use-cases for custom Route Constraint
An example of a Route Constraint is the HttpMethodConstraint
that comes with ASP.NET 4: it limits a route to be selected to only those that have been requested with a specific HTTP verb, ex GET.
Another possible use-case is that of allowing a certain route to be selected only if the request comes from the local machine, or also if you want to avoid deep linking of resources, such as images or files.
One thing to keep in mind, and this is valid for all the extensibility points of ASP.NET MVC, is that most of the time, you could obtain the same result by putting your custom logic in almost any other extensibility point: for example the last two scenarios could have been handled also as Action Filters (I’ll cover that in a future article) or as RouteHandler.
As a rule of thumb, try to use an extension point for what it has been originally designed: Route Constraints are for validating an URL parameter for the scope of matching a URL with a route. So, if in your application, a different resource should be selected based on the fact that a request comes from the local system or that was not initiated by clicking on a link on your site, then this logic is a good candidate for a constraint, otherwise it’s not. Also consider how far in the processing pipeline you want to go. In the case of a DoS attack prevention system, you want to stop the request as soon as possible: this might be another candidate for a Route Constraint.
Name | Route Constraint |
Area | Routing |
Type | Case-Specific |
Why | When the validation logic is based on more than just the format of the parameter you want to validate |
Interface | IRouteConstraint |
Standard Implementation | HttpMethodConstraint |
Notable implementations | None |
Custom Route Handler
The second extensibility point of this article is the Route Handler, which is the component that is responsible for creating the HttpHandler that will process the request.
There are two reasons why you might want to create your own Route Handler: the first is if you want to manipulate the route data before passing them to MvcHandler
, the default handler for ASP.NET MVC. The second is if you want to exit the ASP.NET MVC pipeline and handle the request with a custom Http Handler.
In both situations you also probably need to create a custom Route
that extends RouteBase
and adds more information, and in the second case you will also have to create your own Http Handler.
The interface to implement
The interface that needs to be implement is IRouteHandler
and its only method GetHttpHandler
:
1 2 3 |
public interface IRouteHandler { IHttpHandler GetHttpHandler(RequestContext requestContext) } |
The requestContext
parameter contains all the information available on the current request: the http context (thus the request, server, application objects) and the route data, which contains the values of parameters and the Route object itself.
The return value is the Http Handler that will process the request.
Sample Implementation
The code that follows is a very simple watermarking system, that superimposes a copyright notice on a requested image.
First we need to write the Http Handler that will process the request; It will look for the image, load it and write the copyright notice on top of it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class WaterMarkHandler: IHttpHandler { private string _watermark; public WaterMarkHandler(string watermark) { _watermark = watermark; } public void ProcessRequest(HttpContext context) { var routeValues = context.Request.RequestContext.RouteData.Values; //Look for the image, load as graphic element, write the watermark context.Response.Write("This is image named " + routeValues["name"] + " (C) "+ _watermark); } public bool IsReusable { get { return false; } } } |
This sample doesn’t really add a watermark on top of the image. Instead it just returns a text string that contains the name of the image and the copyright notice. But the real sample is in the code you can download from here, or from the bottom of the article.
The only problem we’ll face is that this http handler cannot be used with the standard RouteHandler, so a custom route handler is needed. Fortunately, the task of implementing it is pretty trivial: the only thing it has to do is to instantiate the http handler and return it to the caller.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class WaterMarkRouteHandler: IRouteHandler { private string _watermark; public WaterMarkRouteHandler(string watermark) { _watermark = watermark; } public IHttpHandler GetHttpHandler(RequestContext requestContext) { return new WaterMarkHandler(_watermark); } } |
The last step is that of binding all together. The MapRoute
method automatically specifies the MvcRouteHandler
, so we need to create directly with the Route
object, and add it to the route table.
1 2 3 |
Route watermarkRoute = new Route("images/{name}", new WaterMarkRouteHandler("CodeClimber - 2011")); routes.Add("image", watermarkRoute); |
Other cases for Custom Route Handler
This was merely an example that showed how to write your own handler, but in combination with writing http handlers and custom routes, a lot more useful stuff can be done: For example you could implement something that understands multi-lingual routes, or you could implement permanent redirection when you move from a webform site to an ASP.NET MVC one. You could use it to maintain persistent URLs when you change your URL structure even when using ASP.NET MVC.
Yet another resource from Phil Haack is RouteMagic, a collection of utilities for making the routing system a bit more powerful. It also includes a generic HttpHandlerRouteHandler that works with all http handlers that don’t have a constructor with parameters.
Name | Route Handler |
Area | Routing |
Type | Case-Specific, Core |
Why | When you need instantiate a different HttpHandler to process the request (thus not entering the standard ASP.NET MVC pipeline) or you want to manipulate the route parameters before passing them to the MvcRouteHandler |
Interface | IRouteHandler |
Standard Implementation | MvcRouteHandler, StopRoutingHandler |
Notable implementations | RouteDebugger, RouteMagic library |
Conclusions
In this article we have seen how to customize the first part of the pipeline, the one before the moment in which the controller appears: these extension points allow you to branch out from the standard ASP.NET MVC pipeline and fork your own custom processing pipeline.
In the next article I’m going to show how you can tap into the components that create the controllers and execute actions.
The source code for the examples, including Standard Routing and debugging with RouteDebugger, Standard Routing and debugging with Glimpse, Custom Route Constraint and a Custom route handler (watermarking) is available from the speechbubble at the head of the article (To try the samples you have to download the two files from the speechbubble, unzip them in the same folder) or in one package from here as a single package. The free wallchart, ‘ASP.NET MVC Pipeline’, that goes with this article is available as a PDF file, also from the speechbubble or from here. It is best printed on an A3 printer.
Load comments