Using ASP.NET MVC on IIS 6 without the .MVC Extension

One awesome features of ASP.NET MVC is the clean URLs.  If you aren't familiar with them, here is a sample:

/products/list

(versus a typical /ProductList.aspx)

The default route for ASP.NET MVC looks like this:

/{controller}/{action}

Using this route you can map most of the URLs that your application will need.  There is a problem, however, when running ASP.NET MVC on IIS6 (and 5).  The URL above doesn't have an extension, so IIS will assume that it is a virtual directory on the server.  We have a couple of options we can do to get around this:

  • We could create wild card mappings and pass every request through ASP.NET, however this is not recommended for performance reasons
  • We can add an extension that maps to ASP.NET (.mvc)

In the latter option your routes must change to something like this:

/{controller}.mvc/{action}

So URLs generated with this route will look like:

/products.mvc/list

...which basically reverts our clean urls into, well... less clean ones.

So if you want to deploy to Windows Server 2003 & IIS 6, you're out of luck.  Well, mostly...

URL Rewriting to the Rescue

We can leverage an ISAPI filter to rewrite URLs which will allow us to have the nice URLs while still on IIS 6.  There are 2 major products that do this for IIS:

ISAPI Rewrite is the more mature of the two, however it's not free.  They do provide a free version, but it doesn't support regular expressions, so it's pretty useless.

Jeff Atwood has a nice write-up of these two utilities.  In it he notes that the syntax for Ionic's ISAPI Rewrite is a little stranger and doesn't support the regex that we're all used to (since it uses a C regex library, not a .NET one).

I decided to choose Ionic's because it was free, however there was a lack of information on how to structure URLs, and it required a lot of trial and error to get it working.  Luckily they have an automatic logging facility that tells you how the rules are matching up.

For ASP.NET MVC, I needed to cover these cases:

  • A request for / should be redirected to /home
    This isn't really required, as .NET can do this for me, however I wanted to ensure that all entry points are at the same URL to avoid Page Rank issues.  (See the same Atwood post for information on how important this is)
  • A request for /something should be rewritten to /something.mvc
    That is, the user will request it without an extension, but the filter will rewrite it without the user's address bar ever changing.
  • A request for /something/index should be rewritten to /something.mvc/index
    Just making sure that URLs with actions get the extension only on the first part
  • The content directory that contains our javascript, CSS, and images should be excluded from the above rule
    Otherwise we'd have /content.mvc/styles.css which would be interpreted as a controller, rather than a direct file request.

I found a good resource that outlined how to do this with ISAPI Rewrite, however the rules were quite different with Ionic's.

Here are my rules for Ionic's ISAPI Rewrite that works for ASP.NET MVC:

# empty URL gets mapped to home controller 
RewriteRule  ^/$                 /home [R] # map controller parts of urls to .mvc, ignoring the content directory
RewriteRule  ^(?!/Content)(/[A-Za-z0-9_-]+)(/.*)?$          $1.mvc$2  [I]

It turns out that I only need 2 rules to satisfy the above requirements.  The first rule forces the browser to redirect, which will aid in making sure that I only have 1 entry point to my website.

The 2nd rule takes the first part of the ULR and adds .mvc to it, appending the remaining verbatim (if any).  It also excludes anything beginning with "content," so I'm free to put images, javascript, css, and other literal file resources there.

The last requirement is to define my routes with both .mvc and regular formats.  The trick is to define them in the right order.

routes.MapRoute("basic", "{controller}/{action}", new { ... } );

routes.MapRoute("basic_mvc", "{controller}.mvc/{action}", new {...});

Doing this ensures that our .MVC routes will actually function, but when we ask the framework for a URL (such as with Url.Action() or Html.ActionLink) we are handed the extension-less route (since it is defined first).

Route Testing is IMPORTANT

I've said it before and I'll say it again.  Route testing is important.  A single tiny change can break an entire application.  Applying automated tests is critical for any MVC application.  Since deployment of an ASP.NET MVC application needs to be flexible, your application should function with either type of route (extensions or not) so that you have flexibility of deployment.

Hard coding a route in 1 single location will prevent you from doing this.  Did I mention route testing is important?

#1 Tim avatar
Tim
6.07.2008
11:58 AM

Which release of Ionic's ISAPI Rewrite did you use to do this?I've been trying to do the same but no matter what version i've tried, i've recieved a heap corruption error when i've configured the ISAPI Filter in IIS.


#2 Tim Barcz avatar
Tim Barcz
7.07.2008
3:00 PM

Yep, Heap corruption error for me too....


#3 Ben Scheirman avatar
Ben Scheirman
7.07.2008
9:23 PM

That's a bummer.Does it happen immediately?Did you examine the ionic logs and see what the issue is?I used the latest version of Ionic ISAPI Rewrite as of the post date, and I didn't notice any problems, though that application isn't production-ready...I only did some tests myself.


#4 Brian LeGros avatar
Brian LeGros
7.09.2008
10:25 AM

I was running into issues with the contents of the INI file, but found a solution.I was able to get your INI file working by using [R,I] instead of [I] on the 2nd ReWrite rule.I'm now running into an issue where if I leave a trailing slash off of the path (e.g. - http://localhost/Home) it appends $2 to the query string (e.g. - http://localhost/Home$2).Any ideas on what I could add to overcome this? I'm terrible at regex. Html.ActionLink() links to controllers w/o the trailing slash so anyone using it would probably run into this.Also, I added the following to my Globals.asax.cs file so that I didn't have to add routes and when I move to IIS7 can just remove the Ionic ISAPI filter:HttpApplication app = sender as HttpApplication;if (app != null){ if (app.Request.AppRelativeCurrentExecutionFilePath.Contains(".mvc")) {app.Context.RewritePath(app.Request.Url.PathAndQuery.Replace(".mvc", "")); }}I can't take credit though, I borrowed the idea from http://blog.codeville.net/2008/07/04/options-for-deploying-aspnet-mvc-to-iis-6/.


#5 Brian LeGros avatar
Brian LeGros
7.09.2008
11:22 AM

Ugh, I guess I'm gonna spam your blog today.I just tried to execute the following line against www.fileformat.info/.../regex.htm which can verify PCRE statments:RewriteRule ^(?!/Content)(/[A-Za-z0-9_-]+)(/.*)?$ $1.mvc$2 [R,I]The experession works correctly whether there is a trailing slash or not ... so it looks like it may be the Ionic engine that isn't playing nice.I checked on CodePlex for a known bug, but didn't have too much luck sifting through everything.For now I'll have to use Url.Action() and append a "/" I guess.Thanks for all the help.


#6 Brian LeGros avatar
Brian LeGros
7.09.2008
1:37 PM

Alright, last time into my idiocy, I promise.The original line you wrote is now working for me.The [R,I] flags together were causing my POSTs to fail because I was redirecting each time and losing the HTTP request body ... doh.The problem with escaping $2 still exists, so I'm still having to use trailing "/" for URLs from Html.ActionLink(), but otherwise, pretty URLs seem to working correctly now.Thanks again for letting me work through this on your blog.Have a good one.


#7 Ben Scheirman avatar
Ben Scheirman
7.10.2008
12:17 PM

Sorry you're having so much trouble!The [I] flag is definitely needed, as that allows case insensitive rules.I'm not sure about the [R] though...Hopefully it will work for you without any crazy workarounds.


Leave a Comment