Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Serving URLs with File Extensions in an ASP.NET MVC Application


:P
On this page:

Today I was working on a blog plug-in for an existing application. The application is an ASP.NET MVC application so the application uses MVC’s routing to handle file access for the most part. One of the requirements for the blogging component is that it has to integrate with Windows Live Writer handling posting and updates to posts. One of the requirements for Live Writer to supported extended post properties when uploading new posts is a Live Writer Manifest file.

Serving the wlwmanifest.xml File from a Subfolder

The issue is that the manifest file has to live at a very specific location in the blog's root folder using an explicit filename.

If your site is running from a Web root, that's not a problem – it's easy to link to static files in the Web root, because MVC is not managing the root folder for routes (other than the default empty ("") route). So if you reference wlwmanifest.xml in the root of an MVC application by default that just works as IIS can serve the file directly as a static file.

Problems arise however if you need to serve a 'virtual file' from a path that MVC is managing with a route. In my case the blog is running in a subfolder that is a MVC managed routed path – /blog. Live writer now expects the wlwmanifest.xml file to exist in the /blog folder, which amounts to a URL like the following:

http://mysite.com/blog/wlwmanifest.xml

Sounds simple enough, but it turns out mapping this very specific and explicit file path in an MVC application can be tricky.

MVC works with Extensionless URLs only (by default)

ASP.NET MVC automatically handles routing to extensionless urls via the IIS ExtensionlessRouteHandler which is defined in applicationhost.config:

<system.webServer> <handlers> <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*."
verb="GET,HEAD,POST,DEBUG"
type="System.Web.Handlers.TransferRequestHandler"
preCondition="integratedMode,runtimeVersionv4.0" responseBufferLimit="0" /> </handlers>
</system.webServer>

Note the path="*." which effectively routes any extensionless URLs to the TransferRequestHandler which is MVC's entrypoint.

This handler routes any extensionless URLs to the MVC Routing engine which then picks up and uses the routing framework to route requests to your controller methods – either via default routes of controller/action or via custom routes defined with [Route()] attributes. This works great for MVC style extensionless routes that are typically used in MVC applications.

Static File Locations in Routed directories

However, things get tricky when you need to access static files in a directory that MVC routes to. For the Live Write scenario particularly I need to route to:

http://mysite.com/blog/wlwmanifest.xml

The problem is:

  • There’s no physical blog folder (wlwmanifest.xml resides in the root folder)
  • /blog/ is routed to by MVC
  • /blog/ is a valid and desirable MVC route
  • wlwmanifest.xml can’t be physically placed in this location

And that makes it rather difficult to handle the specific URL Live Writer expects in order to fine the manifest file.

There are a couple of workarounds.

Skip Routing use UrlRewrite

After futzing around with a bunch of different solutions inside of MVC and the routing setup, I instead decided to use the IIS UrlRewrite module to handle this. In retrospect this is the most efficient solution since IIS handles this routing at a very low level.

To make this work make sure you have the IIS Rewrite Module installed – it’s an optional component and has to be installed via the IIS Platform installer.

Then add the following to your web.config file:

<system.webServer>
  <rewrite>
    <rules>
      <rule name="Live Writer Manifest">
        <match url="wlwmanifest.xml"/>
        <action type="Rewrite" url="blog/manifest"/>
      </rule>
    </rules>
  </rewrite>
</system.webServer>

This effectively routes any request to wlwmanifest.xml on any path to a custom MVC Controller Method I have set up for this. Here’s what the controller method looks like:

[AllowAnonymous]        
[Route("blog/manifest")]
public ActionResult LiveWriterManifest()
{            
    return File(Server.MapPath("~/wlwmanifest.xml"), "text/xml");
}

This is an efficient and clean solution that is fixed essentially through configuration settings. You simply redirect the physical file URL into an extensionless URL that ASP.NET can route as usual and that code then simply returns the file as part of the Response. The only downside to this solution is that it explicitly relies on IIS and on an optionally installed component.

Custom Path to TransferRequestHandler

Another, perhaps slightly safer solution is to map your file(s) to the TransferRequestHandler Http handler, that is used to route requests into MVC. I already showed you that the default path for this handler is path="*." but you can add another handler instance into your web.config for the specific wildcard path your want to handle. Perhaps you want to handle all .xml files (path="*.xml") or in my case only a single file (path="wlwmanifest.xml").

Here's what the configuration looks like to make the single wlwmanifest.xml file work:

<system.webServer>
  <handlers>
    <add name="Windows Live Writer Xml File Handler"
      path="wlwmanifest.xml"
      verb="GET" type="System.Web.Handlers.TransferRequestHandler"
      preCondition="integratedMode,runtimeVersionv4.0" responseBufferLimit="0"  />
  </handlers>
</system.webServer>

Once you do this, you can now route to this file by using an Attribute Route:

[Route("blog/wlwmanifest.xml")]
public ActionResult LiveWriterManifest()
{            
    return File(Server.MapPath("~/wlwmanifest.xml"), "text/xml");
}

or by configuring an explicit route in your route config.

Enable runAllManagedModulesForAllRequests

If you really want to route files with extensions using only MVC you can do that by forcing IIS to pass non-Extensionless Urls into your MVC application. You can do this by enabling the  runAllManagedModulesForAllRequests option on the <modules> section in the IIS configuration for your site/virtual:

<system.webServer>
    <modules runAllManagedModulesForAllRequests="true" />
</system.webServer> 

While this works to hit the custom route handler, it’s not really something I typically want to enable as it routes every type of document – including static files like images, css, javascript – through the MVC pipeline which adds overhead. Unless you’re already doing this to perform special manipulation of static files, I wouldn’t recommend enabling this option.

Other Attempts

As is often the case, all this looks straight forward in a blog post like this but it took a while to actually track down what was happening and realizing that IIS was short-circuiting the request processing for the .xml file.

Before I realized this though I went down the path of creating a custom Route handler in an attempt to capture the XML file:

public class CustomRoutesHandler : RouteBase
{
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        var url = httpContext.Request.Url.ToString();

        if (url.ToLower().Contains("wlwmanifest.xml"))
        {
            httpContext.Response.ContentType = "text/xml";
            httpContext.Response.TransmitFile("~/wlwmanifest.xml");
            httpContext.Response.End();
        }        
        return null;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext,
                RouteValueDictionary values)
    {
        return null;
    }
}

To hook up a custom route handler:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.IgnoreRoute("{resource}.ashx/{*pathInfo}");            
            
    routes.Add(new CustomRoutesHandler());

    routes.MapMvcAttributeRoutes();

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

The route handler explicitly checks each request coming in and then overrides the behavior to load the static file.

But alas,  this also doesn’t work by default because just like a route that tries to look at an XML file, the file is never actually passed to the MVC app because IIS handles it.

Nevertheless it's good to know t that MVC has the ability to allow you to look at every request it does handle and customize the route or processing along the way which allows for short circuiting of requests which can be useful for special use cases. Irrelevant to my initial problem, but useful and worthwhile to mention in this context :-)

Summary

File based URL access is one of those cases that should be super simple and obvious, but is not. It requires a relatively simple but non-obvious workaround to ensure that you can handle a Url with an extension by either using UrlRewrite or adding an explicit file mapping to the TransferRequestHandler.

Incidentally in ASP.NET 5 (MVC 6) – assumes you’re handling all requests anyway as you are expected to build up the entire request pipeline – including static file handling from scratch. So I suspect in future versions of MVC this sort of thing will be more natural, as long as the host Web server stays out of the way…

Posted in ASP.NET  MVC  

The Voices of Reason


 

Kevin Pirkl
November 13, 2015

# re: Serving URLs with File Extensions in an ASP.NET MVC Application

I had a use case scenario for running runAllManagedModulesForAllRequests=true..

The requirement on the project was the need for security and logging for every single asset request to a Windows Domain Users. Four server web farm sat behind load balancer that routed traffic to each of the four web heads in something akin to a round-robin process (not exactly that simple) and did not employ any kind of sticky bit bindings so your. In true distributed fashion the web page could come from web-farm-server #1 and then the very next page asset could be delivered from web-farm-server #2, etc..

In a farm scenario like this if you try turning on Windows Authentication against everything delivered through the integrated pipeline it creates an insane number of 401 authentication requests that are quite expensive. Win Auth under the covers bouncing around with their tokens between servers triggering re-auth user the cover was not pretty.. I resolved all that by taking the site to be only SSL, making all site assets virtual or physical require that user be authenticated, and enabling a hybrid Windows Form Authentication pointing to an MVC App running under a single directory that required Windows Authentication.

When a user hits the site and is not logged in then Windows Forms Authentication picks up the original requested url and redirects to the folder requiring Windows Authentication. MVC code runs and write out a Forms Authentication session token and finally redirects the user back to the original requested item. After that the session cookie login token just travels around with all subsequent URI requests.

Of course to allow the authentication token to decrypt across the farm you have to set the machine.config machinekey element attributes of decryptionKey, validationKey and validation algorithm the same on each server in the farm. This worked great and via IIS Advanced logging we tracked an crazy amount of detail. There were metrics being collected on everything..

It worked perfect. To ensure good performance we did not allow the developers to use view or session state.

Legacy ASP, static HTML pages, PHP, etc through the integrated pipeline all properly decrypted and showed the users login id.. That about sums up the the use case where it worked for me.


Well last I just want to say thanks for your latest blog post and as always they make for some great reading. Cheers

Rick Strahl
November 13, 2015

# re: Serving URLs with File Extensions in an ASP.NET MVC Application

@Kevin - yeah, runAllManagedModulesForAllRequests can be very useful - in special occasions. Especially when interop'ing between different platforms. But... and that's a big but, it does have perf considerations because running static content through the ASP.NET pipeline definitely slows down requests in terms of raw request throughput especially for static resources. As long as you can live with that, that's fine. Regardless unless there's a specific need - like your use case, it's generally not a good idea to turn this on.

Ironically in early versions of MVC it was a requirement that this flag was set (I think before a native IIS module was available)...

Paul
December 20, 2015

# re: Serving URLs with File Extensions in an ASP.NET MVC Application

If you are using a CDN for static assets (js,css,images etc) then I assume that there is no issue with using runAllManagedModulesForAllRequests?

Rick Strahl
December 20, 2015

# re: Serving URLs with File Extensions in an ASP.NET MVC Application

@Paul - no of course not since those requests are not actually hitting your server. You don't even need a CDN for that - you can just use a separate virtual or subdomain (ie. IIS Application) to isolate out the non-processing requests.

Arun
January 21, 2021

# re: Serving URLs with File Extensions in an ASP.NET MVC Application

Nice Article. I am trying to use the method you mentioned here in one of my project. Instead of xml files, in my case I would like to serve .pdf files in MVC pipeline. I have created a handler like below

And I have created a folder named CommonFiles inside the route directory and copied few pdf files there for testing. But the issue is when ever I try to access those files I am getting an internal server error , Its not even going through the MVC pipeline. If I just give a path which is not exists with a .pdf then its trying to go via MVC pipeline. Can you please hep me here ?


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024