Click here to Skip to main content
15,885,309 members
Articles / Web Development / ASP.NET

ASP.NET MVC Bundles Internals

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
16 Feb 2014CPOL5 min read 45.1K   7   5
ASP.NET MVC Bundles internals

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 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, e.g.:

C#
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 at 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 also be fulfilled). Each module has a chance to change the already assigned handler. Finally, the chosen handler processes the request. Starting from .NET 4, 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 starts. As we are examining bundles, let’s take as an example System.Web.Optimization.dll assembly. It has the following attribute set:

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

And the PreApplicationStartCode class looks as follows:

C#
[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:

C#
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:

C#
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.

XML
<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
Image 3 Image 4

License

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


Written By
Software Developer (Senior)
Poland Poland
Interested in tracing, debugging and performance tuning of the .NET applications.

My twitter: @lowleveldesign
My website: http://www.lowleveldesign.org

Comments and Discussions

 
GeneralAbout the Article Pin
Member 111491734-Nov-14 20:00
Member 111491734-Nov-14 20:00 
QuestionPreApplicationStartCode Pin
Peter Frazer8-Apr-14 0:48
Peter Frazer8-Apr-14 0:48 
AnswerRe: PreApplicationStartCode Pin
Sebastian Solnica8-Apr-14 1:56
Sebastian Solnica8-Apr-14 1:56 
GeneralRe: PreApplicationStartCode thank you Pin
Peter Frazer8-Apr-14 2:05
Peter Frazer8-Apr-14 2:05 
GeneralMy vote of 5 Pin
Humayun Kabir Mamun16-Feb-14 18:54
Humayun Kabir Mamun16-Feb-14 18:54 

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

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