Routing File Requests to an ASP.Net MVC Route

Routing File Requests MVC

Recently I converted a small restaurant website over to use ASP.Net MVC. The original site was one that I created probably about 10 years ago and was a mix of mostly static HTML pages along with a few ASPX WebForms pages.

One thing to keep in mind when doing a conversion like this is to make sure that the old URLs indexed by Google or bookmarked by your visitors aren’t left completely broken. For instance, in the old restaurant website there was a page named locations.html. The equivalent of that page in the new ASP.Net MVC website can be found at the route /Locations/. If I had just uploaded the new website to my hosting company it would have worked fine, but anybody trying to access the locations page from a Google search or an old bookmark would have received a 404 error because it would still be pointing to that locations.html file that no longer exists. I needed to find a quick and dirty way of routing file requests from the old .html and .aspx pages over to their new ASP.Net MVC routes.

The solution that I ended up using involves setting up HTTP handler mappings, mapping routes and creating a controller action.

Step 1 – HTTP Handler Mappings

If you aren’t familiar with HTTP handler mappings, it is a pretty simple concept. IIS7 uses HTTP handler mappings to point a path and verb combination to a specific module to handle a request. For instance, the default mapping that would handle “*.aspx” requests for a site running on the .Net 4.0 Framework in Integrated mode looks like this.

<add name="PageHandlerFactory-Integrated-4.0" path="*.aspx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode,runtimeVersionv4.0" />

If you are curious to see what the default HTTP handler mappings are, you can find them by looking in the applicationhost.config file used for IIS Express on your development machine. This file can be found at \Users\\Documents\IISExpress\config\applicationhost.config. The HTTP handler mappings are located at the bottom of the file.

The handler that is used for extensionless URLs (like ASP.Net MVC routes) is the TransferRequestHandler. If you open the applicationhost.config file and search for “ExtensionlessUrl-Integrated-4.0” you will see how this mapping is setup.

For my scenario, I just needed to add two HTTP Handler Mappings to the web.config of my website to override the default behavior from the applicationhost.config and send *.html and *.aspx GET requests to that same TransferRequestHandler used for the ASP.NET MVC Extensionless URLs.

Note that HTTP Handler Mappings are added to the system.webServer / handlers node of the web.config file.

<system.webServer>
  <handlers>
    <add name="HtmlFileHandler" path="*.html" verb="GET" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />

    <add name="AspxlFileHandler" path="*.aspx" verb="GET" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
  </handlers>
</system.webServer>

Step 2 – Mapping the routes

Once the HTTP handler mappings were setup to treat *.html and *.aspx GET requests the same as ASP.Net MVC routes, I needed to create a couple new routes in RouteConfig.cs to map those incoming requests to a controller action. I setup an action named “LegacyPage” on the Home controller that takes a parameter containing the requested page’s name.

routes.MapRoute(
    name: "LegacyHtml",
    url: "{page}.html",
    defaults: new { controller = "Home", action = "LegacyPage", page = UrlParameter.Optional }
);

routes.MapRoute(
    name: "LegacyAspx",
    url: "{page}.aspx",
    defaults: new { controller = "Home", action = "LegacyPage", page = UrlParameter.Optional }
);

Step 3 – Create Controller Action

The last step that required was to create the controller action that was specified when setting up the new route(s) in RouteConfig.cs. This action will take in the name of the page that was requested and redirect the request to an MVC route. So if someone had the locations.html page from the old restaurant website bookmarked, this action would take the request for locations.html and redirect them to /Locations/. As mentioned earlier, this was a pretty simple website with 20 or less pages overall which allowed me to easily map the old pages over to the equivalent new routes one by one. If the site were more complex and had many more pages, I probably would have went with a different solution for mapping the old page names to the routes in the new website.

Below is an abbreviated example of what my controller action looked like. If I couldn’t find a match for the passed in “page” then I was redirecting them to a custom 404 error page. I also had it sending me an email for a while whenever it couldn’t find a match in case I happened to miss mapping something.

public ActionResult LegacyPage(String page)
{
    if (string.Compare(page, "locations", true) == 0)
    {
        return RedirectToActionPermanent("Locations");
    }

    if (string.Compare(page, "onlineordering", true) == 0)
    {
        return RedirectToActionPermanent("OrderOnline",);
    }

    if (string.Compare(page, "coupons", true) == 0)
    {
        return RedirectToActionPermanent("Coupons");
    }

    if (string.Compare(page, "history", true) == 0)
    {
        return RedirectToActionPermanent("History");
    }

    return RedirectToAction("Error404");
}