ASP.NET MVC Routing - Intercepting file requests like Index.html, and what it teaches about how Routing works
Easy: Including an HTML file in an ASP.NET MVC site
I got an interesting question recently on routing, which lead to an even more interesting question on how to do the opposite. This turns out to offer a pretty good look at some important details of how routing works under the hood.
First, here's the question:
I have a scenario where I’ve got an MVC application and I need to put a static HTML file in the root of the site (to work with a third party). Is there a way that I can specify in an MVC app that I want to not reroute requests to the html page? i.e. suppose I have myfile.html in the root of my MVC app, how do I make it so the MVC magic doesn’t try to redirect my request?
Note that while the question was about a file with a .html extension, this applies to other files that are normally mapped to static content, like XML or SWF or what have you.
Fortunately, this is pretty easy:
The easiest way is to do nothing. MVC routing won't touch requests with an extension it hasn't been mapped to, and the IIS static file handler will pick it up. Try creating a new MVC project and drop myfile.html in the root and browse to it, you’ll see the HTML file is served. If you think about it, this is kind of necessary to allow for an MVC site to be able to serve up images, JavaScript, favicon, etc. files without trying to route those requests.
If you really want to make sure that routing doesn’t handle it if it happens to see it, you can add an IgnoreRoute rule to your RegisterRoutes method (used to be in Global.asax.cs, now in App_Start/RouteConfig.cs):
routes.IgnoreRoute("{file}.html");
A little tougher: Routing .HTML Requests To An MVC Route
This made me want to show an example of how to do the opposite - intercept a request to myfile.html even if there's a myfile.html on disk. That turns out to be a bit more work. It's not rocket science, but there are a few steps.
Step 1. Mapping the file extension to TransferRequestHandler
IIS 7 Integrated mode uses HTTP Handler mappings which point path / verb combinations to an HTTP Handler. For example, there's a default handler mapping which points path="*.axd" verb="GET,HEAD,POST,DEBUG" to the appropriate ISAPI module for the .NET runtime version the site's running under. The easiest way to see the default handlers under IIS Express is to run a site under IIS Express, right-click the IIS Express icon in the system tray, click "show all applications", and click on a site. The applicationhost.config link at the bottom is linked, so you can just click on it and it should load in Visual Studio.
If you scroll to the bottom, you'll see that there's a catchall StaticFile mapping for path="*" verb="*" which points to "StaticFileModule,DefaultDocumentModule,DirectoryListingModule". That's what will handle your .html request if you do nothing. So the first step is to add a handler in your web.config that will point *.html requests to the TransferRequestHandler. TransferRequestHandler is the handler that takes care of the extensionless URLs you're used to seeing in MVC routes, e.g. /store/details/5.
Adding a handler mapping is really easy - just open up your web.config and add it to the <system.webServer/handlers> node.
<add name="HtmlFileHandler" path="*.html" verb="GET" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
Note that you can make the path more specific if you'd like. For instance, if you only wanted to intercept one specific request, you could use path="sample.html"
Step 2. Configuring the route
Next, you'll need a new route. Open up App_Start/RouteConfig.cs and it to the RegisterRoutes call. My complete RegisterRoutes looks like this:
routes.MapRoute( name: "CrazyPants", url: "{page}.html", defaults: new { controller = "Home", action = "Html", page = UrlParameter.Optional } );
You can make your route as complex as you want. For instance, here's one by pilotbob which uses a constraint to map several legacy filetypes to a controller:
routes.MapRoute( "Legacy" , "Html/{*src}" , new { Controller = "Home", action = "Html" } , new { src = @"(.*?)\.(html|htm|aspx|asp)" } // URL constraints , new [] { "WebApp.Controllers" } );
Step 3. Route Existing Files
That almost covers it, but there's one more thing to take care of - overriding requests that match an existing file. If you've got an actual file called myfile.html, the routing system won't allow your route to run. I forgot about this, ended up with an HTTP 500 error (recursion overflow) and had to ask Eilon Lipton for help.
Anyways, that's easy to fix - just add routes.RouteExistingFiles = true to your route registration. My completed RegisterRoutes call looks like this:
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.RouteExistingFiles = true; routes.MapRoute( name: "CrazyPants", url: "{page}.html", defaults: new { controller = "Home", action = "Html", page = UrlParameter.Optional } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
That's it.
I tested by adding this controller action:
public ActionResult Html(string page) { ViewBag.Page = page; return View(); }
and I threw in this view:
@{ ViewBag.Title = "Html"; } <h2>Yo yo yo, I'm the HTML Controller!</h2> <p>Requested page: @(ViewBag.Page).html</p>