How ASP.NET MVC Routing Works and its Impact on the Performance of Static Requests

Recently a number of people have asked how MVC and ASP.NET routing impacts the performance of static requests (HTML, JPG, GIF, CSS, JS, etc). I'll answer that question below while explaining how routing is implemented in the ASP.NET pipeline on IIS 6 and IIS 7. This applies to both ASP.NET 3.5 and 4.0, unless otherwise stated. I'll also talk about the new extensionless URL routing feature in ASP.NET 4.0.

How ASP.NET Routing Works

URLs are mapped to handlers by IIS when the request is initially received. I'll refer to the ASP.NET handlers as managed handlers, and everything else as unmanaged handlers. A request with a managed handler will enter ASP.NET and be processed by the various managed modules in the pipeline. On IIS 6, a request with an unmanaged handler won't be processed by managed code. On IIS 7, a request with an unmanaged handler, also, won't be processed by managed code, at least not by default.

The first step to make routing work for requests with unmanaged handlers is to direct these requests into managed code. On IIS 6 this is done with a wildcard mapping to aspnet_isapi.dll. On IIS 7 this is done either by setting runAllManagedModulesForAllRequests=”true” or removing the "managedHandler" preCondition for the UrlRoutingModule. Ok, so now the requests are coming into managed code, what happens next? Well, the UrlRoutingModule inspects the requests and will change the handler mapping according to the route table. If it doesn't change the handler mapping, then the original handler mapping will be used--the one set by IIS. It's actually a bit more complicated on IIS 6 because of the wildcard mapping. On IIS 6, if the UrlRoutingModule doesn't change the handler, ASP.NET will issue a child request that allows the request to execute with the handler mapping that would have been applied originally if the wildcard mapping didn't exist. Make sense?

Impact on Performance of Static Requests

Okay, so what about the performance of static requests (HTML, JPG, GIF, CSS, JS, etc)? If a request enters managed code, the throughput for that request will be reduced to *approximately* 1/3 of what it would have been, assuming that no caching is involved. Note that I said *approximately*, because the actual result will depend upon hardware, software, the size of the file, etc. Fortunately kernel caching comes to the rescue on IIS 7. If you have an application that uses routing in such a way that *all* requests enter managed code, as long as you don’t change the handler for static requests, they will be kernel cached on IIS 7 (if they’re hot, and meet kernel cache requirements).  What about kernel caching on IIS 6? Unfortunately kernel caching is disabled for child requests on IIS 6, which means enabling ASP.NET routing on IIS 6 effectively disables kernel caching of static requests.

So does that mean I should put my static content in an application that isn’t using routing, if I'm concerned about the performance of static requests? On IIS 6, the kernel cache is effectively disabled for static content when ASP.NET routing is enabled; so yes, you proably should partition your site. On IIS 7 it's only the first couple requests to the static file that miss the kernel cache, assuming the request meets kernel caching requirements. However, there is a better way to enable ASP.NET routing on IIS 7, and ASP.NET 4.0 makes use of it by default. Keep reading and I'll also tell you how to enable this on ASP.NET 3.5.

ASP.NET 4.0 Enables Routing of Extensionless URLs

In ASP.NET v4.0, there is a better way to enable routing. Normally you're only interested in routing extensionless URLs, and have no need to route static requests (HTML, JPG, GIF, CSS, JS, etc). In v4.0 there is a new feature that allows extensionless URLs to be directed into managed code, without impacting static requests (HTML, JPG, GIF, CSS, JS, etc). Because of this feature, on IIS 6 you no longer need a wildcard mapping and on IIS 7 you no longer need to set runAllManagedModulesForAllRequests=”true” or remove the "managedHandler" precondition for the UrlRoutingModule. It works by default on both IIS 6 and IIS 7, except that you need a QFE from the IIS team to make this work on Windows Vista SP2, Windows Server 2008 SP2, Windows Server 2008 R2, and Windows 7. Once you obtain the IIS 7 QFE and install v4.0 ASP.NET, you’ll be able to route extensionless URLs without impacting static requests. The QFE enables a new “*.” handler mapping—the notation may seem weird, but all you care about is the fact that this maps to URLs without an extension. ASP.NET registers a “*.” handler mapping when v4.0 is installed. If you don’t have the IIS 7 QFE, that handler mapping does nothing. If you have the IIS 7 QFE, extensionless URLs are mapped to our handler, which enables them to be routed by the UrlRoutingModule. Information about the IIS 7 QFE and steps to download it can be found at https://support.microsoft.com/kb/980368. For the record, the implementation of this feature on IIS 6 is done quite differently, and I won't go into that here.

Just in case you need to disable the feature, I'll tell you how.  On IIS 6, you can disable the feature by setting a DWORD registry key at HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ASP.NET\4.0.30319\EnableExtensionlessUrls = 0 (the default is 1, even when the key does not exist).  On IIS 7, you can disable the feature by removing the "*." handler mappings. There are three of them (1 for 32-bit classic mode, 1 for 64-bit classic mode, and 1 for integrated mode) and the names of these handler mappings in applicationHost.config are: "ExtensionlessUrl-ISAPI-4.0_32bit", "ExtensionlessUrl-ISAPI-4.0_64bit", and "ExtensionlessUrl-Integrated-4.0".

How to Enable Extensionless URL Routing on ASP.NET 3.5 for MVC

Similarly to the way it is done in ASP.NET 4.0, you can enable routing of extensionless URLs on ASP.NET 3.5 (and even ASP.NET 2.0) without setting runAllManagedModulesForAllRequests="true" or configuring a wildcard script handler.  To do this, you need one of the following operating systems:  Windows Vista SP2, Windows Server 2008 SP2, Windows Server 2008 R2, or Windows 7.  And you need to patch it with the hotfix available at https://support.microsoft.com/kb/980368 (this hotfix will eventually be rolled into a future service pack, of course).  Once you have the hotfix installed, simply add the following web.config to your application and take the source code for MyTransferRequestHandler (also below) and put it in a CS file located in the App_Code folder of your application.  Alternatively you can compile MyTransferRequestHandler into a DLL and place it in the bin, or better yet, give it a strong name, install it in the GAC, and NGEN it.  After doing this, extensionless URLs will be directed into managed code, and you can then route them to a different handler.  If you choose not to route them to a different handler, they will be directed back to IIS so that they can be executed by the handler that would have served them if our extensionless URL handler was not installed.

"web.config":

 <configuration>
  <system.webServer>
    <handlers>
      <!-- THESE HANDLER MAPPINGS SHOULD GO BEFORE 
           ANY OTHER HANDLER MAPPINGS -->
      <add name="EURL-ISAPI-2.0_32bit" 
        path="*." 
        verb="GET,HEAD,POST,DEBUG" 
        modules="IsapiModule"
        scriptProcessor="%SystemRoot%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" 
        preCondition="classicMode,runtimeVersionv2.0,bitness32" 
        responseBufferLimit="0"/>
      <add name="EURL-ISAPI-2.0_64bit" 
        path="*." verb="GET,HEAD,POST,DEBUG"
        modules="IsapiModule" 
        scriptProcessor="%SystemRoot%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" 
        preCondition="classicMode,runtimeVersionv2.0,bitness64" 
        responseBufferLimit="0"/>
      <add name="EURL-integrated-2.0" 
        path="*." 
        verb="GET,HEAD,POST,DEBUG" 
        type="MySample.MyTransferRequestHandler" 
        preCondition="integratedMode,runtimeVersionv2.0"/>
      <!-- PLACE ADDITIONAL HANDLER MAPPINGS BELOW HERE -->
    </handlers>
    <modules runAllManagedModulesForAllRequests="false">
      <!-- BE CERTAIN TO SET runAllManagedModulesForAllRequests="false" -->
      <!-- AND IF YOU ADDED A WILDCARD SCRIPT MAPPING, REMOVE THAT TOO -->
    </modules>
  </system.webServer>
</configuration>

 

"App_Code/MyTransferRequestHandler":

 namespace MySample {
    using System;
    using System.Web;
    // This handler only works on IIS 7 in integrated mode and is
    // designed to be used as a "*." handler mapping.  We will refer
    // to "*." as the extensionless handler mapping.  There is a bug
    // in Windows Vista, Windows Server 2008, and Windows Server 2008 R2
    // that prevents the IIS 7 extensionless handler mapping from working
    // correctly, and if you have not already patched your machine you 
    // will need to install this hotfix: https://support.microsoft.com/kb/980368.
    // 
    // WARNING: You may be wondering how this handler works, since it passes
    // the original URL to the TransferRequest method.  Why doesn't it do the
    // same thing the second time the URL is requested?  Well, TransferRequest
    // invokes the IIS 7 API IHttpContext::ExecuteRequest, and includes the
    // EXECUTE_FLAG_IGNORE_CURRENT_INTERCEPTOR flag in the third argument to
    // ExecuteRequest.  This causes the "*.", or extensionless, handler mapping
    // to be skipped, and allows the handler that would have executed originally
    // to serve this request. If you attempt to use this handler without an
    // extensionless handler mapping, it will probably result in recursion.
    // This recurssion will eventually be stopped by IIS once the loop iterates
    // about 12 times, and then IIS will respond with a 500 status, a message 
    // that says "HTTP Error 500.0 - Internal Server Error", and an HRESULT  
    // value of 0x800703e9.  The system error message for this HRESULT is
    // "Recursion too deep; the stack overflowed.".
    public class MyTransferRequestHandler : IHttpHandler {        
        public void ProcessRequest(HttpContext context) {
            context.Server.TransferRequest(context.Request.RawUrl, 
                                           true /*preserveForm*/);
        }       
        public bool IsReusable {
            get {
                return true;
            }
        }
    }
}

 

Wondering how the extensionless URL handler mappings work? Wondering why it works for both classic and integrated mode? In classic mode, the "*." handler mapping is to our aspnet_isapi.dll. Extensionless URLs will be mapped to the DefaultHttpHandler in classic mode. The DefaultHttpHandler is listed in the <httpHandlers> section in the root web.config file with path="*", so it catches everything that is not caught by something else. If you don't change the handler before the MapRequestHandler event, the DefaultHttpHandler will issue a child request to the original URL with the HSE_EXEC_URL_IGNORE_CURRENT_INTERCEPTOR flag. This tells IIS to skip our extensionless URL handler and map the request to the handler that would normally serve this type of request. If on the hand you want ASP.NET to handle it, you can change the handler by calling HttpContext.RemapHandler before the MapRequestHandler event. This is what ASP.NET routing does in v4.0--in v3.5, routing essentially does the same, but in a much more round about manner. So that's how the extensionless URL handler works in classic mode. In integrated mode, it's essentially the same, but instead of going through the ISAPI path we're able to call the IIS API IHttpContext::ExecuteRequest directly, and similarly we pass a flag to ignore the current interceptor, but the flag is called EXECUTE_FLAG_IGNORE_CURRENT_INTERCEPTOR. The ISAPI code path actually uses these same APIs, but not directly.

Troubleshooting

Having trouble with the MyTransferRequestHandler shown above? That code only directs extensionless URLs into managed code. You need to use routing or something else in order to handle them. If you just want to see if extensionless requests are entering managed code, you can add the following to your web.config and put the MyInterceptorModule source code in a file named MyInterceptorModule.cs within your App_Code directory. The MyInterceptorModule intercepts all requests that enter managed code and displays information about the URL and the values of several server variables.

 

"web.config additions":

 <system.web>
    <httpModules>
      <add 
        name="MyInterceptorModule" 
        type="MySample.MyInterceptorModule"/>
    </httpModules>
  </system.web>
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false"/>
    <modules runAllManagedModulesForAllRequests="false">
      <add name="MyInterceptorModule" 
        type="MySample.MyInterceptorModule" 
        preCondition="managedHandler"/>
   </modules>
  </system.webServer>

 

"App_Code/MyInterceptor":

 namespace MySample {
    using System;
    using System.Web;
    public class MyInterceptorModule : IHttpModule {
        public void Dispose() {}
        public void Init( HttpApplication app ) {
            app.BeginRequest += new EventHandler( OnBeginRequest );
        }
        private void OnBeginRequest(object sender, EventArgs e) {
            HttpApplication app = sender as HttpApplication;
            HttpResponse response = app.Context.Response;
            HttpRequest request = app.Context.Request;
            response.Output.WriteLine("<html><head><title>intercepted</title></head><body><pre>");
            response.Output.WriteLine("Path={0}", request.Path);
            response.Output.WriteLine("PathInfo={0}", request.PathInfo);
            response.Output.WriteLine("QueryString={0}", request.QueryString);
            response.Output.WriteLine("RawUrl={0}", request.RawUrl);
            foreach(string key in request.ServerVariables.AllKeys) {
                response.Output.WriteLine(
                       "{0} = {1}", key, request.ServerVariables[key]);
            }
            response.Output.WriteLine("</pre></body></html>");
            app.CompleteRequest();
        } 
    }
}

 

 

Note that if you're experimenting and want to see what is kernel cached, there are a number of things that prevent a resource from being kernel cached. Be sure to include an Accept-Encoding header, for example most browsers include "Accept-Encoding: gzip,deflate". Also don't issue a conditional GET, for example, make sure your browser is not using "If-Modified-Since". Request the page a couple times in order to kernel cache it. To see if it is served from the kernel cache, start Performance Monitor (Start|Run|type perfmon.msc) and add the following performance counters: "Web Service Cache\Kernel: URI Cache Hits", "Web Service Cache\Kernel: URI Cache Misses", and "Web Service Cache\Kernel: Current URIs Cached".

Regards,
Thomas

Comments

  • Anonymous
    April 20, 2010
    Question about not needing the wildcard mapping on iis6 w/ asp.net 4 I tried it out and it indeed did work for my mvc2 based application..but it works only about 70% of the time. It will 404 the other 30%.  If I readd the wildcard mapping it works 100% of the time.. Any ideas?

  • Anonymous
    April 20, 2010
    The comment has been removed

  • Anonymous
    June 17, 2010
    "In v4.0 there is a new feature that allows extensionless URLs to be directed into managed code, without impacting static requests (HTML, JPG, GIF, CSS, JS, etc)." This feature seems to have broken some URL rewriters, specifically the Ionics ISAPI Rewriter Filter (IIRF), on IIS 6 + ASP.NET 4.0 combinations. If I have a rewrite rule for http://example.com/ to redirect to http://www.example.com/, and if I hit "http://example.com" in my browser, then I get redirected to "http://www.example.com/eurl.axd/GUID/", which results in a 404. If I go to "example.com/default.aspx" then the redirection works properly; I get redirected to "www.example.com/default.aspx". This is astonishing because the behavior should be the same for both requests, since IIS 6 is finding the default.aspx default document and executing that. But "something" is seeing that the first attempt (to "/" instead of "default.aspx") is "extensionless" and bringing this "eurl.axd" thing out of nowhere. If I switch to using my IHttpModule instead of IIRF, it seems to be fine. I don't understand where this ASP.NET 4.0 extensionless magic on IIS 6 is living in the request pipeline, and why it is breaking the rewriter. (Under ASP.NET 2.0 it was fine.) Any insight is appreciated!

  • Anonymous
    June 18, 2010
    Hi Nicholas, You can disable the v4.0 ASP.NET extensionless URL feature on IIS6 by setting a DWORD at HKEY_LOCAL_MACHINESOFTWAREMicrosoftASP.NET4.0.30319.0EnableExtensionlessUrls = 0.  After changing the value, you will need to restart IIS in order for us to pick up the change, because it is only read once when IIS starts. Here’s how the v4.0 ASP.NET extensionless URL features works on IIS 6.  We have an ISAPI Filter named aspnet_filter.dll that appends “/eurl.axd/GUID” to extensionless URLs.  This happens early on in the request processing.  We also have a script mapping so that “.axd” requests are handled by our ISAPI, aspnet_isapi.dll.  When we append “/eurl.axd/GUID” to extensionless URLs, it causes them to be mapped to our aspnet_isapi.dll, as long as the script map exists as expected.  These requests then enter ASP.NET where we remove “/eurl.axd/GUID” from the URL, that is, we restore the original URL.  The restoration of the original URL happens very early.  Now the URL is extensionless again, and if no further changes are made and you’re using the default <httpHandlers> section in web.config, this will be assigned to the DefaultHttpHandler, since it has path=”” and is the first handler to match the extensionless URL.  The DefaultHttpHandler will then redirect this request back to IIS, but this time our filter will not append “/eurl.axd/GUID” and the request will be handled as it would be normally.  To do anything interesting with the v4.0 ASP.NET extensionless URL feature, you must change the handler from DefaultHttpHandler to something else, before the MapRequestHandler event fires. It sounds like our aspnet_filter is appending “/eurl.axd/GUID”, but we are not finding the “*.axd” script map, and so the request is not mapped to aspnet_isapi.dll, and therefore never enters ASP.NET and we never restore the original URL.  

  • Anonymous
    July 12, 2010
    I'm trying to combine ASP.Net 3.5 WebFroms with MVC 2 running on IIS 6 and I am concerned about performance of static content, what steps should I follow?

  • Anonymous
    July 12, 2010
    Hi Frank, ASP.NET 3.5 with MVC 2 requires a wildcard script map on IIS 6, which will incur a performance hit on every request to static content.  Whether that performance hit is significant is something you'll have to decide.  Roughly speaking, it will be about 3 times slower, but you should measure that yourself as so many things impact the actual numbers. I recommend that you upgrade to ASP.NET 4.0, which has functionality built-in for directing only extensionless URLs into managed code.  Also, if you move from IIS 6 to IIS 7 it has a better way of directing only extensionless URLs into managed code, and that is described in the post at blogs.msdn.com/.../how-extensionless-urls-are-handled-by-asp-net-v4.aspx. Thanks, Thomas

  • Anonymous
    August 29, 2010
    The comment has been removed

  • Anonymous
    August 29, 2010
    The comment has been removed

  • Anonymous
    August 29, 2010
    If you're going to take a dependency on v4, then I think you should also take a dependency on KB980368.  It will be released in the next Windows service pack, although I don't know when that will be.   IIS 6 does not have the feature used in 7 and 7.5, so we implemented the solution differently.  The implementation is discussed a little bit in blogs.msdn.com/.../how-to-disable-the-asp-net-v4-0-extensionless-url-feature-on-iis-6-0.aspx.  The IIS 6 implementation is not as efficient and has usability issues that the IIS 7 and 7.5 implementation does not have. Yes, on IIS 7 and 7.5, you need KB980368 for both classic and integrated mode.  If you don't want a dependency on KB980368, then you should find a different solution for extensionless URLs.  That pretty much means you will have to use an IIS wildcard mapping. Medium trust applications can change the value of runAllManagedModulesForAllRequests or remove the "managedHandler" precondition.   ASP.NET is very dependent on config, and I think you should use config in your ASP.NET apps.

  • Anonymous
    August 30, 2010
    Thomas, thanks for the answer. Actually I needed this feature for a config-free web control. I usually don't avoid web.config settings in standalone ASP.NET applications. I thought I would be able to use new Routing features (dynamic route registration) to get rid of HttpHandler definitions in web.config so that the control would work auto-magically when included in an ASP.NET project. I guess I need to keep HttpHandler definitions in web.config.

  • Anonymous
    September 24, 2010
    In my site, I have a Controller specifically for serving controlled file resources (FileController) that makes sure the user is authenticated.  So the url would be like mysite.com/.../myControlledFile.zip.  Running it from VS 2010 worked fine, but I keep getting 404 not founds when i publish, upload to my IIS 6 server and then try to access the resource.  You think it is because IIS is seeing the .zip and not passing it to managed handlers?  If I disable that registry entry like you suggest, won't it bring all static content into managed handlers?  Is there a way in IIS 6 to serve my controlled file resources with managed handlers while static stuff like images don't get passed to managed handlers?

  • Anonymous
    September 27, 2010
    On IIS 6, the ASP.NET ISAPI extension does not include a script map for the ZIP file extension. If you want ASP.NET to serve URLs that end in ".zip", you will need to add a script map for that extension to aspnet_isapi.dll. Setting the registry key EnableExtensionlessUrls = 0 on IIS 6 will prevent extensionless URLs from being directed into ASP.NET.  This has nothing to do with serving URLs with the ZIP file extension.

  • Anonymous
    October 21, 2010
    IIS6 + .Net 4 + MVC 2 I can see the extensionless filter installed as an ISAPI Filter on the IIS config, but when I browse to my app folder it says 'Directory Listing Denied' if I add a wildcard handler to the appllication config, the app works. note that the server also has ISAPI_rewrite filter installed, but I tried removing it and that didn't help.

  • Anonymous
    October 28, 2010
    The extensionless URL feature will direct URLS into ASP.NET if:

  1. v4.0 aspnet_filter.dll is registered as an ISAPI filter at the web server level.
  2. ".axd" is script mapped to the v4.0 aspnet_isapi.dll at the web site level.  To register ASP.NET script maps you would typically run aspnet_regiis.exe -s W3SVC/N/ROOT, where N is the siteID of the web site you want to change and aspnet_regiis.exe is in the install directory of the ASP.NET version you want to script map.  Backup your metabase before making changes like this.
  3. the web site has read and script permission.
  4. "EnableExtensionlessUrls" is not set or is set to 1. If the above checks out, the only other thing you need is a routing table or some ASP.NET code that remaps the URL to a managed handler.
  • Anonymous
    December 15, 2010
    The comment has been removed

  • Anonymous
    December 02, 2013
    why is it that my .cshtml show only text and does'nt show the style?..

  • Anonymous
    January 11, 2014
    The comment has been removed

  • Anonymous
    July 27, 2015
    This just saved what was left of my hair from being pulled out. THANKS!