improve.dk
Just another mindless drone looking for the perfect stack
posts - 227, comments - 489

How to do URL rewriting on IIS 7 properly

Written on October 14, 2009 by Mark S. Rasmussen in Development: .NET, Sysadmin: IIS

One of my earlier blog posts, and the all time most popular one, was about how to make URL rewriting on IIS 7 work like IIS 6. While my method did provide a means to the goal, it's humiliatingly far from what I should've done. Since the old post is still the most visited post on my blog I feel obligated to write a followup on how to do proper url rewriting in IIS 7.

The scenario

I'll assume a completely vanilla IIS 7 setup, contrary to the old post, there's no IIS tampering required.

I've setup a simple web application solution structure like so:

iis7urlrewritingdoneproperly_1

As in the original post my goal is to accept a URL like http://localhost/blog/2006/12/08/missing-windows-mobile-device-center and map it to the BlogPost.aspx file in the root of my application. During the rewrite process I want to make the year, month, day and title available for the BlogPost.aspx file in an easily accessible way.

Rewriting using Global.asax

The easiest way of rewriting URL's is to add a new Global.asax file to the root of your solution. Now paste in the following code:

using System;
using System.Text.RegularExpressions;
using System.Web;

namespace IIS7UrlRewritingDoneProperly
{
	public class Global : HttpApplication
	{
		// Runs at the beginning of each request to the server
		protected void Application_BeginRequest(object sender, EventArgs e)
		{
			// Match the specific blog post URL path as well as pull out variables in regex groups
			Match m = Regex.Match(Request.Url.LocalPath, @"^/blog/(?<year>\d{4})/(?<month>\d{2})/(?<day>\d{2})/(?<title>.*)/?$");

			// If we match a blog posts URL, save the URL variables in Context.Items and rewrite to /BlogPost.aspx
			if (m.Success)
			{
				Context.Items["Title"] = m.Groups["title"].Value;
				Context.Items["Year"] = m.Groups["year"].Value;
				Context.Items["Month"] = m.Groups["month"].Value;
				Context.Items["Day"] = m.Groups["day"].Value;

				HttpContext.Current.RewritePath("/BlogPost.aspx");
			}
		}
	}
}

Now all you need is a single change in your web.config file:

<configuration>
	<system.webServer>
		<modules runAllManagedModulesForAllRequests="true">
	</system.webServer>
</configuration>

The web.config change basically does the same as adding the wildcard map in IIS6. It ensures ASP.NET will run our Application_BeginRequest function for all requests - both those that match .aspx files as well as those for static files.

Rewriting using an HttpModule

As an alternative to putting the rewriting logic into Global.asax, you might want to write it into a distributable HttpModule. If your URL rewriting functionality is common for multiple sites, generic or for any other reason may be usable on multiple sites, we don't want to replicate the functionality in Global.asax.

If you added the Global.asax file from before, make sure you remove it again so it doesn't conflict with the HttpModule we're about to write. Add a new class project to the solution - I've called mine MyUrlRewriter. Add a reference to System.Web and add a single new class file to the project called UrlRewriter. Your solution should look like this:

iis7urlrewritingdoneproperly_2

Now paste the following code into the UrlRewriter.cs class file:

using System;
using System.Text.RegularExpressions;
using System.Web;

namespace MyUrlRewriter
{
	public class UrlRewriter : IHttpModule
	{
		// We've got nothing to dispose in this module
		public void Dispose()
		{ }

		// In here we can hook up to any of the ASP.NET events we use in Global.asax
		public void Init(HttpApplication context)
		{
			context.BeginRequest += new EventHandler(context_BeginRequest);
		}

		// This method does exactly the same as in Global.asax
		private void context_BeginRequest(object sender, EventArgs e)
		{
			// Match the specific blog post URL path as well as pull out variables in regex groups
			Match m = Regex.Match(HttpContext.Current.Request.Url.LocalPath, @"^/blog/(?<year>\d{4})/(?<month>\d{2})/(?<day>\d{2})/(?<title>.*)/?$");

			// If we match a blog posts URL, save the URL variables in Context.Items and rewrite to /BlogPost.aspx
			if (m.Success)
			{
				HttpContext.Current.Items["Title"] = m.Groups["title"].Value;
				HttpContext.Current.Items["Year"] = m.Groups["year"].Value;
				HttpContext.Current.Items["Month"] = m.Groups["month"].Value;
				HttpContext.Current.Items["Day"] = m.Groups["day"].Value;

				HttpContext.Current.RewritePath("/BlogPost.aspx");
			}
		}
	}
}

Notice that the context_BeginRequest function is identical to the one we had in Global.asax, except we have to reference HttpContext.Current explicitly since it's not implicitly available as in Global.asax.

Now add a reference from the original web application project to the MyUrlRewriter class project. Once this is done we just need to ensure our HttpModule is included in our web application by modifying the web.config:

<configuration>
    <system.webServer>
        <modules runAllManagedModulesForAllRequests="true">
            <add name="UrlRewriter" type="MyUrlRewriter.UrlRewriter, MyUrlRewriter"/>
        </modules>
    </system.webServer>
</configuration>

At this point you should be able to run the website with the exact same URL rewriting functionality as we had before - though this time in a redistributable assembly called MyUrlRewriter.dll which can easily be included into any website by adding a single line to the section of the web.config file.

Not Invented Here Syndrome

If you have basic requirements to your URL rewriting solution you may often be able to settle with one of the many readymade HttpModules that you can simply plug into your application. IIS 7 also has a URL Rewrite Module that you can install and easily configure through the IIS manager.

Feedback

Gravatar

Brian Holmgård Kristensen wrote on 10/19/2009 3:50 PM

Mark, you ROCK!

Setting runAllManagedModulesForAllRequests="true" solved all my issues with NHibernate and Unit Of Work :-)
Gravatar

Chris wrote on 12/19/2009 12:58 AM

IS there any IIS 7.0 setup required here? I've tried both methods you mentioned and can't get either working. I have URL Rewriting working in IIS 6.0 but our production server just switched to 7.0.

Thanks,
Chris
Gravatar

Mark S. Rasmussen wrote on 12/21/2009 9:20 AM

@Chris

There shouldn't be any IIS7 setup required as all is configured through the web.config file. Has there been any IIS7 configurations on the machine level that might affect your particular site?

Is the BeginRequest method being fired in either method?

/ Mark
Gravatar

Andrew wrote on 1/14/2010 11:23 PM

Excellent follow up article. Great info.

Thanks,
Andrew
Gravatar

Andrew wrote on 1/14/2010 11:37 PM

This method available in IIS7 is easy to implement. But I don't believe it is as efficient as the "hack" method you outlined in:
www.improve.dk/...
The reason is, I noticed that global.asax is called for every request on the page - i.e. .net code is run for request for .css, .js, .gif, .png files etc which is not as good as having the final "catch all" method you outlined in the previous article.

Using the old method, you can set up the wildcard to use a Managed Handler: System.Web.UI.PageHandlerFactory which allows us to keep IIS7 in Integrated mode. The only catch, which I am trying to find a simple solution for, is the default pages for folders will have to be managed by the handler instead of relying on the Default Pages specified in IIS7, which gets overridden by System.Web.UI.PageHandlerFactory.

Thanks,
Andrew
Gravatar

Mark wrote on 3/19/2010 10:51 PM

You are my new god Mark. You totally just saved my ass - My URL rewriting was set up fine and working on my localhost, but porting to IIS 7 presented an issue. The URL rewriting can also get screwed up if you have your application pool in integrated mode rather than classic - but that was not the case for me this time around - setting runAllManagedModulesForAllRequests="true" worked beautifully.

Interestingly enough actually, my problems with my URL rewriting module came up today after I implemented a custom 404 error page. It worked fine before hand. Not sure why that made a difference though??
Gravatar

Ophedian wrote on 3/31/2010 8:23 PM

Dude, after 6 hours trying to figure out what the heck is going on, finding this answer was like finding a chilled Deer Park water bottle after wondering in the Sahara desert. You Rock!!!
Gravatar

Kvarc wrote on 5/9/2010 7:34 PM

Thanks for the info, this enabled my UrlRewriter to work in Windows 7.

Only two changes were necessary:
<modules> -> <modules runAllManagedModulesForAllRequests="true">
and adding:
<add name="UrlRewriter" type="Intelligencia.UrlRewriter.RewriterHttpModule, Intelligencia.UrlRewriter"

Regards!
Gravatar

Jones wrote on 11/19/2011 9:39 AM

Thnak you, thank you. thank you!!!!!
Gravatar

Jiss wrote on 2/15/2012 5:49 AM

Mark,
Nice article.
But when I use 'Rewriting using Global.asax', then we need to specify '.aspx' extension also with virtual urls. I could not run virtual paths without .aspx on an iis7 hosted server. But could run if extension specified like '192.168.0.9:8044/.../sample-title-of-artcle.aspx'.

Else we have to use httpmodule based rewriting.
Gravatar

Mark S. Rasmussen wrote on 2/15/2012 10:36 AM

Jiss,
If you follow the instructions on mapping all requests through the aspnet_isapi.dll file as described here: improve.dk/..., you'll be able to map non-.aspx virtual URLs too.

I do however recommend that you use an HttpModule as it's the cleanest solution by far.

Post Comment

Name  
Email
Url
Comment
Please add 7 and 2 and type the answer here: