If you are running an ASP.NET MVC application under IIS6 you will need an extension in the URL in order for the request to be handled by the ASP.NET runtime. You can configure IIS so that all requests, no matter what extension, is handled by the aspnet_isapi filter but if you do this your application must handle content request as well (like css and image files). The best way to handle extensionless urls under IIS6 is to use Helicon Tech’s ISAPI_Rewrite 3. This isapi filter will rewrite the url before it is being processed, in effect it takes an extensionless url and rewrites it into an url with an extension.
ISAPI_Rewrite is a commercial product, however there is a free lite version that works really well (it has some limitations). In ISAPI_Rewrite you write the rewrite rules using regular expressions:
RewriteEngine on RewriteBase / RewriteRule ^Home/(.*?)$ Home.mvc/$1
Charles Vallance has written an excellent post on extensionless urls with ASP.NET MVC, the problem with his solution is that it requires that each route rule be duplicated, one with extension and one without.
routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" } ); routes.MapRoute( "Default", "{controller}.mvc/{action}/{id}", new { controller = "Home", action = "Index", id = "" } );
The reason why you need two rules is that one is used for inbound urls (which after isapi_rewrite have an extension), the other is for outbound urls so that generated links from helpers are extensionless. This duplication of route rules might not be a big problem if you only use the default routing schema but if you use a lot of specific routes you want a better way to declare your routes so you do not need to duplicate them.
I have blogged previously about the route fluent interface I created for CodeSaga. I extended this further to handle this route duplication. In the url definition I only need to place a marker where the extension is going to be:
SagaRoute .MappUrl("admin$/repository/edit/{reposName}") .ToDefaultAction<RepositoryAdminController>(x => x.Edit(null)) .AddWithName(RouteName.EditRepository, routes);
The actual route duplication is handled by the AddWithName function:
public SagaRoute AddWithName(string routeName, RouteCollection routes) { var clone = this.Clone(); Url = Url.Replace("$", ""); if (ShouldAddExtensionlessRoute()) { routes.Add(routeName, this); routes.Add(routeName + ".mvc", clone); } else { routes.Add(routeName, clone); } return this; } public SagaRoute Clone() { var clone = new SagaRoute(Url.Replace("$", ".mvc")); foreach (var pair in Defaults) clone.Defaults.Add(pair.Key, pair.Value); foreach (var pair in Constraints) clone.Constraints.Add(pair.Key, pair.Value); return clone; } private bool ShouldAddExtensionlessRoute() { return RuntimeContext.Config.UrlExtensionMode == UrlExtensionMode.WithoutExtension; }
In the code above I clone the route, replace the dollar sign with ".mvc" and remove the dollar from the original route. The extensionless route must be added before the one with the extension, this is because it needs precedence when generating the outbound urls. There is also a setting that controls if the extensionless route is added at all, this is for IIS6 users that don't want to bother with isapi_rewrite.
I hope this comes in handy if you are working on an ASP.NET MVC application that needs to support IIS6 and IIS7 in extension and extenionless modes.