0121 31 45 374
Qoute Icon

Beware! Context.RewritePath does not end the current execution path

Tim

We've recently been working on an inherited project that's got some interesting "features", one of which is how they handle URL rewriting.

There are a number of HTTPHandlers that can be plugged into your application or ISAPI filters to enable URL rewriting in IIS (and now routes in ASP.Net 4 etc) but a fairly classic/old method was to handle it within your Global.asax's Application_BeginRequest method.

A Simple Example

The example below rewrites a path such as "/article/123.aspx" and transforms it to "/articledisplay.aspx?id=132"

<%@ Application Language="C#" %>
<script RunAt="server">
    /// <summary>
    /// Begins the application request
    /// </summary>
    /// <param name="sender">The source of the event</param>
    /// <param name="e">A System.EventArgs that contains the event data</param>
    void Application_BeginRequest(Object sender, EventArgs e)
    {
        // Get the current path (this will be an root relative link e.g. /article/123.aspx)
        string path = Request.Path.ToLower();

        // Work out if we want to transform it
        if (path.Contains("/article/"))
        {
            string id = path.Replace("/article/", String.Empty).Replace(".aspx", String.Empty);
            Context.RewritePath(String.Concat("~/articledisplay.aspx?id=", id), false);
        }
    }
</script>

Ignoring the pro's and cons of using this method to rewrite the paths, one thing you should be aware of is that Context.RewritePath does not end the code execution.

Why's that important to know?

The problem with this is simple, if you have additional rules later in the code, they will also run -which could end up causing quite a lot of confusion.

Consider the following example Application_BeginRequest method (overlook the semantics of the code):

// Get the current path (this will be an root relative link e.g. /category/123.aspx)
string path = Request.Path.ToLower();

// It's a category request
if (path.Contains("/category/"))
{
    // Use the id to perform some form of database lookup i.e. the category
    int id = Category.GetIdByUrl(path);

    // Redirect the user to the page to display the category
    Context.RewritePath(String.Concat("~/rewritepath.aspx?type=category&id=", id), false);
}

// It's a product request
if (path.Contains("/product/"))
{
    string id = path.Replace("/product/", String.Empty).Replace(".aspx", String.Empty);

    // Redirect the user to the page to display the category
    Context.RewritePath(String.Concat("~/rewritepath.aspx?type=product&id=", id), false);
}

Now think about where the visitor would end up if they go to: /category/product/123.aspx is it /rewritepath.aspx?type=category&id=123 or /rewritepath.aspx?type=product&id=123?

Due to the way Context.RewritePath works, it executes /rewritepath.aspx?type=product&id=123 so where's the issue?

The problem is that although the pages behind it don't actually get executed until the end of the Application_BeginRequest method, because the url satisfies both the url rules, the database call will happen -even though in reality, it's not needed. The result is that you could end up with a massive (and unnecessary) overhead to every request hitting the ASP.Net engine (and if you have wildcard mapping enabled bare in mind that's every request -including images/css/javascript).

So if you feel the need to include additional rules (or additional processing e.g. database calls), consider the order of your rules and return from the method as soon as you know your rules are getting fulfilled, the above example would then be written as:

// Get the current path (this will be an root relative link e.g. /category/123.aspx)
string path = Request.Path.ToLower();

// It's a product request
if (path.Contains("/product/"))
{
    string id = path.Replace("/product/", String.Empty).Replace(".aspx", String.Empty);

    // Redirect the user to the page to display the category
    Context.RewritePath(String.Concat("~/rewritepath.aspx?type=product&id=", id), false);

    // We know that the user is going to the right place so no more rules need to be executed -return
    return;
}

// It's a category request
if (path.Contains("/category/"))
{
    // Use the id to perform some form of database lookup i.e. the category
    int id = Category.GetIdByUrl(path);

    // Redirect the user to the page to display the category
    Context.RewritePath(String.Concat("~/rewritepath.aspx?type=category&id=", id), false);

    // There are no more rules so no need to return
}

Other things to note

Using my somewhat simplistic example, there are a number of other things you could do to improve the code's maintainability and performance:

  • Consider using "StartsWith" instead of contains to make the match more specific (the code we had used "IndexOf() != -1" throughout -but then also had a couple of "IndexOf() == -1" to spice it up). There may be a minor overhead doing this however it makes it a lot easier to understand what you want to achieve.
  • Try not to do anything other than "forward" the request on as quickly as possible i.e. no database calls!
  • Add known exclusions at the start of the code where you know you don't need to rewrite the path i.e. the folders for CSS, images and JavaScript. You could also approach this by wrapping all rules in an inclusion i.e. if(path.StartsWith("category") || path.StartsWith("product"))
  • You should write the rules not only in order of importance/process but while doing so, give consideration to which will be processed most frequently e.g. if you have a rule that's only use by 1 in 1000 calls as the first rule, this will be checked before the relevant rules 999 times in 1000 -so what may be a very small overhead could become a large bottleneck as soon as your site starts to grow in popularity
  • Document each rule clearly i.e. explaining what url formats should trigger it
  • Where rules are mutually exclusive, use "else if" rather than just "if" to make it clear that you don't expect the others to be run if the first is valid.

Liked this post? Got a suggestion? Leave a comment