ASP.NET MVC Routing Extensibility

You develop an ASP.NET MVC application by extending it; customising any default logic that you wish to change with your own implementation. Simone starts a tour of the extensibility points of ASP.NET MVC, by looking at the beginning of the pipeline, the Routing Module, and gives a practical example of writing an extension, with source code: a way of 'watermaking' images 'on the fly'.

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.

1407-img92.jpg

Fig 1. Workflow inside the UrlRoutingModule

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.

Code List 1: routing table

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:

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.

1407-RouteDebugger.jpg

Fig 2. Debugging information in RouteDebugger for the URL http://example.com/Authors/List

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:

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.

1407-Glimpse.jpg

Fig 3: Route information from Glimpse.

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.

The parameters supplied to the method are:

  • httpContext: the http context, which gives access to the "HttpContextBase class with the complete request, server, application objects
  • route: the route object this constraint belongs to
  • parameterName: the name of the route parameter being checked
  • values: all the URL parameters for the route
  • routeDirection: 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:

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:

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:

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.

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.

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.

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.

Downloads

Tags: , , , , ,

  • 59465 views

  • Rate
    [Total: 2    Average: 5/5]
  • Anonymous

    Great
    great article…

  • Anonymous

    Nice article
    Simple and effective explanation of what otherwise seemed to be a complex topic.

  • labilbe

    Thanks
    Thank you for all the details, great explanation!

  • Gurjeet

    Nice article
    Its really bvery useful.Thanks!

  • A

    nICE article
    good it was wat i expected..

  • beton

    Very usefule article.
    Great post!
    Any chance to continutre through MVC pipeline extensibility with similiar atricles? 🙂

    Cheers