Twitter About Home

Deploying ASP.NET MVC to IIS 6

Deploying ASP.NET MVC applications to IIS 6 always causes confusion at first. You’ve been coding in Visual Studio 2008, seeing your lovely clean URLs work nicely in the built-in web server, you stick the code on some Windows Server 2003 machine, and then wham! It’s all like 404 Not found  and you’re like hey dude that’s not cool.

Published Jul 4, 2008

image

This happens because IIS 6 only invokes ASP.NET when it sees a “filename extension” in the URL that’s mapped to aspnet_isapi.dll (which is a C/C++ ISAPI filter responsible for invoking ASP.NET). Since routing is a .NET IHttpModule called UrlRoutingModule, it doesn’t get invoked unless ASP.NET itself gets invoked, which only happens when aspnet_isapi.dll gets invoked, which only happens when there’s a .aspx in the URL. So, no .aspx, no UrlRoutingModule, hence the 404.

I’d say you’ve got four ways around this:

Option 1: Use a wildcard mapping for aspnet_isapi.dll

This tells IIS 6 to process all *requests using ASP.NET, so routing is always invoked, and there’s no problem. It’s dead easy to set up: open IIS manager, right-click your app, go to *Properties, then Home Directory *tab, then click *Configuration. Under Wildcard application maps, click Insert (not Add, which is confusingly just above),  then enter C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll for “Executable”, and uncheck Verify that file exists.

Done! Routing now just behaves as it always did in VS2008′s built-in server.

Unfortunately, this also tells IIS to use ASP.NET to serve all requests, including for static files. It will work, because ASP.NET has a built-in DefaultHttpHandler that does it, but depending on what you do during the request, it might use StaticFileHandler to serve the request. StaticFileHandler is much less efficient than IIS natively. You see, it always reads the files from disk for every request, not caching them in memory. It doesn’t send Cache-Control headers that you might have configured in IIS, so browsers won’t cache it properly. It doesn’t do HTTP compression. However, if you can avoid interfering with the request, DefaultHttpHandler will pass control back to IIS for native processing, which is much better.

For small intranet applications, wildcard mappings are probably the best choice. Yes, it impacts performance slightly, but that might not be a problem for you. Perhaps you have better things to worry about.

For larger public internet applications, you may need a solution that delivers better performance.

Option 2: Put .aspx in all your route entries’ URL patterns

If you don’t mind having .aspx in your URLs, just go through your routing config, adding .aspx before a forward-slash in each pattern. For example, use {controller}.aspx/{action}/{id} or myapp.aspx/{controller}/{action}/{id}. *Don’t *put .aspx inside the curly-bracket parameter names, or into the ‘default’ values, because it isn’t really part of the controller name – it’s just in the URL to satisfy IIS.

Now your application will be invoked just like a traditional ASP.NET app. IIS still handles static files. This is probably the easiest solution in shared hosting scenarios. Unfortunately, you’ve spoiled your otherwise clean URL schema.

Options 3: Use a custom filename extension in all your URL patterns

This is the same as the above, except substituting something like .mvc instead of .aspx. It doesn’t really create any advantage, other than showing off that you’re using ASP.NET MVC.

Just update your route entries as described above, except putting .mvc instead of .aspx. Next, register a new ISAPI mapping: Open IIS manager, right-click your app, go to Properties, then Home Directory *tab, then click *Configuration. *On the *Mappings tab, click Add, then enter C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll for “Executable”, .mvc (or whatever extension you’re using) for “Extension”, and uncheck Verify that file exists. Leave Script engine checked (unless your app has Execute permission) and leave All verbs selected unless you specifically want to filter HTTP methods.

That’s it – you’re now using a custom extension. Unfortunately, it’s still a bit of an eyesore on your otherwise clean URL schema.

Option 4: Use URL rewriting

This is a trick to make IIS think there’s a filename extension in the URL, even though there isn’t. It’s the hardest solution to implement, but the only one that gives totally clean URLs without any significant drain on performance.

Ben Scheirman came up with a great post on this subject, but I’m adapting the technique slightly so as to avoid needing to change my routing configuration in any way. Here’s how it works for me:

  1. As an extensionless request arrives, we have a 3rd-party ISAPI filter that rewrites the request to add a known extension: .aspx.

  2. IIS sees the extension, and maps it to aspnet_isapi.dll, and hence into ASP.NET

  3. Before routing sees the request, we have an Application_BeginRequest() handler that rewrites the URL back to its original, extensionless form

  4. Routing sees the extensionless URL and behaves normally.

Since the URL gets un-rewritten in step 3, you don’t have to do anything funny to make outbound URL generation work.

How to do it

First, download and install Helicon’s ISAPI_Rewrite. You can use the freeware edition, version 2, though beware this will affect all the sites on your server. If you need to localize the rewriting to a particular app or virtual directory, you’ll need one of the paid-for editions.

Now edit ISAPI_Rewrite’s configuration (Start -> All programs -> Helicon -> ISAPI_Rewrite -> httpd.ini), and add:

# If you're hosting in a virtual directory, enable these lines,
# entering the path of your virtual directory.
#UriMatchPrefix /myvirtdir
#UriFormatPrefix /myvirtdir

# Add extensions to this rule to avoid them being processed by ASP.NET
RewriteRule (.*)\.(css|gif|png|jpeg|jpg|js|zip) $1.$2 [I,L]

# Normalizes the homepage URL to /
RewriteRule /home(\?.*)? /$1 [I,RP,L]
RewriteRule / /home [I]

# Prefixes URLs with "rewritten.aspx/", so that ASP.NET handles them
RewriteRule /(.*) /rewritten.aspx/$1 [I]

This excludes known, static files (CSS, GIF etc.), but for the rest, it prefixes the URL with /rewritten.aspx, making ASP.NET kick in. As a bonus, it normalizes any requests for /home to simply / via a 301 redirection, helping out with your SEO. Save this file, and restart IIS (run iisreset.exe).

That’s implemented step 1. Now, to implement step 3, add the following handler to your Global.asax.cs file:

protected void Application_BeginRequest(Object sender, EventArgs e)
{
    HttpApplication app = sender as HttpApplication;
    if (app != null)
        if (app.Request.AppRelativeCurrentExecutionFilePath == "~/rewritten.aspx")
            app.Context.RewritePath(
                app.Request.Url.PathAndQuery.Replace("/rewritten.aspx", "")
            );
}

This detects rewritten URLs, and un-rewrites them. That does it! (Or at least it works on my machine – please share your experiences.)

Now you’ve got clean, extensionless URLs on IIS 6 (and probably on IIS5, though I haven’t tried), without using a wildcard map, and without interfering with IIS’s efficient handling of static files.

Bonus option 5: Upgrade to Windows Server 2008 and IIS 7

Of course, it’s much easier with IIS 7, because it natively supports .NET IHttpModules, so by default you’ll have UrlRoutingModule plugged right into the server, and you don’t have to do anything weird to make it work perfectly.

READ NEXT

ASP.NET MVC Preview 3 is available

Looks like it went up earlier today…

Published May 27, 2008