Click here to Skip to main content
Click here to Skip to main content
Go to top

ASP.NET MVC bundles internals

, 16 Feb 2014
Rate this:
Please Sign up or sign in to vote.
The idea of minimizing and combining multiple script and style files into one file has been popular among web developers for quite some time. With the 4th version of ASP.NET MVC Microsoft introduced a mechanism (called bundles) that allow .NET developers to automate and control this process. Althoug

The idea of minimizing and combining multiple script and style files into one file has been popular among web developers for quite some time. With the 4th version of ASP.NET MVC Microsoft introduced a mechanism (called bundles) that allow .NET developers to automate and control this process. Although bundles are quite easy to configure and use they might sometimes do not behave as expected. In this post I’m going to acquaint you with bundles internals and present you ways to troubleshoot problems they may generate.

Bundles architecture

To examine bundles let’s create a default ASP.NET MVC project in Visual Studio 2013. This project should have a BundleConfig.cs file in App_Start folder with some bundle routes defined, eg.:

public class BundleConfig
{
    // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862
    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                    "~/Scripts/jquery-{version}.js"));
        ...
    }
}

After the above code is called from Global.asax on Application_Start event a new route will be created and a request to http://localhost:8080/bundles/jquery.js?v=JzhfglzUfmVF2qo-weTo-kvXJ9AJvIRBLmu11PgpbVY1 will render a minimized version of jquery (unless the <compilation> tag does not have the debug attribute set to true). To understand how it works let’s have a look how bundles interact with the ASP.NET pipeline. As we know requests coming to an ASP.NET application need to be served by a handler. At first a default handler is assigned by IIS based on a mask (handlers tag in applicationhost.config). Then the request is processed by all the HTTP modules defined in the configuration files (in the integrated mode a precondition must be also fulfilled). Each module has a chance to change the already assigned handler. Finally the chosen handler processes the request. Starting from .NET4 there is also a possibility to inject HTTP modules into the ASP.NET pipeline dynamically from our application code. For this purpose we need to add a PreApplicationStartMethodAttribute attribute to our assembly. When HTTP runtime detects an assembly with such an attribute it will execute a method the attribute defines before the application start. As we are examining bundles let’s take as an example System.Web.Optimization.dll assembly. It has the following attribute set:

[assembly: PreApplicationStartMethod(typeof (PreApplicationStartCode), "Start")]

And the PreApplicationStartCode class looks as follows:

[EditorBrowsable(EditorBrowsableState.Never)]
public static class PreApplicationStartCode
{
  private static bool _startWasCalled;

  /// <summary>
  /// Hooks up the BundleModule
  /// </summary>
  public static void Start()
  {
    if (PreApplicationStartCode._startWasCalled)
      return;
    PreApplicationStartCode._startWasCalled = true;
    DynamicModuleUtility.RegisterModule(typeof (BundleModule));
  }
}

Notice that the above code registers a new BundleModule in the ASP.NET pipeline:

    public class BundleModule : IHttpModule
    {
      ...
      private void OnApplicationPostResolveRequestCache(object sender, EventArgs e)
      {
        HttpApplication app = (HttpApplication) sender;
        if (BundleTable.Bundles.Count <= 0)
          return;
        BundleHandler.RemapHandlerForBundleRequests(app);
      }
      ...
    }

Remapping happens only if a static file with a name equal to our bundle does not exist:

internal static bool RemapHandlerForBundleRequests(HttpApplication app)
{
  HttpContextBase context = (HttpContextBase) new HttpContextWrapper(app.Context);
  string executionFilePath = context.Request.AppRelativeCurrentExecutionFilePath;
  VirtualPathProvider virtualPathProvider = HostingEnvironment.VirtualPathProvider;
  if (virtualPathProvider.FileExists(executionFilePath) || virtualPathProvider.DirectoryExists(executionFilePath))
    return false;
  string bundleUrlFromContext = BundleHandler.GetBundleUrlFromContext(context);
  Bundle bundleFor = BundleTable.Bundles.GetBundleFor(bundleUrlFromContext);
  if (bundleFor == null)
    return false;
  context.RemapHandler((IHttpHandler) new BundleHandler(bundleFor, bundleUrlFromContext));
  return true;
}

After a BundleHandler is chosen to process a given request it creates a context for bundle operations and examines the BundleTable in search for a bundle that should be sent to the browser. Bundles are cached by their hash so subsequent calls for the same bundle perform much faster than the first one.

IIS configuration for bundles

For simplicity’s sake, I will focus only on Integrated Pipie in IIS7+. You need to be sure that ASP.NET handler is called for your bundle requests, otherwise they won’t be served. If you are using urls in the form of /bundlename?v=bundlehash the default handler configuration in IIS (presented below) should be good.

<handlers>
    ...
    <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
    <add name="StaticFile" path="*" verb="*" modules="StaticFileModule,DefaultDocumentModule,DirectoryListingModule" resourceType="Either" requireAccess="Read" />
</handlers>

And in the IIS Failed Request Trace you should see the following events (I marked in red the ones that are related to bundles):

iis bundle request trace

Notice that the ExtensionlessUrlHandler-Integrated-4.0 handler assigned at first by IIS is then replaced by System.Web.Optimization.BundleHandler. We already know that this replacement is ordered by System.Web.Optimization.BundleModule on the RESOLVE_REQUEST_CACHE notification (marked in red on the image).

Troubleshooting problems

So far we examined bundles internals and their correct interaction with the ASP.NET (IIS) pipeline. But what if things go wrong and instead of seeing nicely compacted javascript you receive 404 HTTP response? We had such a problem in production in one of our applications. Just after deploying a new version of this application, bundles were never working (returning 404 code). The only fix we found was to restart the application pool after a deploy. As you can imagine it was less than desirable solution so I started investigating the root cause of our problem. During tests I found out that this problem was appearing only when the application was interrupted with requests during deployment (by, for example, our load balancer which was checking if the application is responding). Example javascript bundle in our application had the following path: bundle/Site.js?v=77xGE3nvrvjxqAXxBT1RWdlpxJyptHaSWsO7rRkN_KU1. Did you notice a subtle difference between this url and the one from the ASP.NET example application? Yes, the .js EXTENSION! This small part of the url changed dramatically the way IIS handled requests for bundles. Till application was ready (fully deployed) IIS tried to serve them using StaticFileHandler (which was in accordance to its handlers mask configuration). Also it appears that IIS caches which modules were run for a given url. Thus, even when our application was ready to serve the bundle requests IIS didn’t run System.Web.Optimization.BundleModule on them. We eventually removed the .js extension from the bundles url. Another solution might have been to change the mask for the ExtensionlessUrlHandler-Integrated-4.0 to *. This would force IIS to run the managed module for all the requests to the application.

If you would like to check which files were included into a bundle you may tamper the request (using for example fiddler) by modifying the User-Agent header to Eureka/1, example request:

GET http://localhost:8080/Content/css?v=WMr-pvK-ldSbNXHT-cT0d9QF2pqi7sqz_4MtKl04wlw1 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/css,*/*;q=0.1
If-Modified-Since: Sat, 15 Feb 2014 15:52:46 GMT
User-Agent: Eureka/1
Referer: http://localhost:8080/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8,pl;q=0.6,fr-FR;q=0.4,fr;q=0.2

and the response:

HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/css; charset=utf-8
Vary: Accept-Encoding
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?YzpcdGVtcFxidW5kbGUtdGVzdFxDb250ZW50XGNzcw==?=
X-Powered-By: ASP.NET
Date: Sat, 15 Feb 2014 22:12:52 GMT
Content-Length: 14076

/* Bundle=System.Web.Optimization.Bundle;Boundary=MgAwADcANgAwADIAMwAyADUA; */
/* MgAwADcANgAwADIAMwAyADUA "~/Content/site.css" */
html {
    background-color: #e2e2e2;
    margin: 0;
    padding: 0;
}
...

Summary

I hope that this post helped you better understand ASP.NET bundles. They are a great mechanism to automatically group and minimize script and style files in your application. And if you ever encounter any problems with them remember about IIS Failed Request Trace and Eureka/1 user agent :)


Filed under: CodeProject, Diagnosing ASP.NET

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Sebastian Solnica
Software Developer (Senior)
Poland Poland
Interested in tracing, debugging and performance tuning of the .NET applications (especially ASP.NET).
 
If you find this article interesting, maybe you would like to pay me a visit: http://lowleveldesign.wordpress.com? Smile | :)

Comments and Discussions

 
QuestionPreApplicationStartCode PinmemberPeter Frazer8-Apr-14 0:48 
AnswerRe: PreApplicationStartCode PinmemberSebastian Solnica8-Apr-14 1:56 
GeneralRe: PreApplicationStartCode thank you PinmemberPeter Frazer8-Apr-14 2:05 
GeneralMy vote of 5 PinmemberHumayun Kabir Mamun16-Feb-14 18:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140916.1 | Last Updated 16 Feb 2014
Article Copyright 2014 by Sebastian Solnica
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid