Introduction to BundlerForBower for ASP.NET MVC5

Last Updated: October 16, 2016 | Created: February 9, 2016

I have recently changed over my ASP.NET MVC5 application to use Bower and Grunt for handling all my web packages, e.g. JavaScript and CSS libraries. You can read about this in another of my articles called Converting your ASP.NET MVC5 application to use Bower, Grunt and Gulp. In changing over that I tried to follow the approach that ASP.NET Core 1 uses with web packages. Most things, like its use of Grunt/Gulp for web build tasks was fine, but when it came to the the inclusion of JavaScript and CSS files in a HTML web page, then I had to find a new way.

This article is about my solution called BundlerForBower, or B4B for short, which is available in an open-source project. My B4B solution provides similar features to MVC5’s BundleConfig approach, but is designed specifically to work with Bower and the new Grunt/Gulp build process.

UPDATE: BundlerForBower now available on NuGet

BundlerForBower is available on NuGet – see https://www.nuget.org/packages/Bundler4Bower/ 

ASP.NET Core 1 approach

When swapping over to Bower and Grunt/Gulp I wanted to follow as closely as possible the approach that ASP.NET Core 1 uses, as I expect to change over to using ASP.NET Core 1 some time soon. As you will see from my other article it wasn’t hard to follow the same approach for most things, but for delivering JavaScript and CSS files in a HTML web page I had two problems.

The first was a technical problem, in that ASP.NET Core 1 uses new <link> and <script> tags which MVC5 does not have access to. The second was a design problem in that ASP.NET Core 1 approach was to have the user list the files to be delivered in TWO places: one in the Gulp file for the build process and the second in the HTML views that needed them. That isn’t DRY (Don’t Repeat Yourself)!

These two things spurred me on to create a better solution. In fact I have made B4B configurable to work in both an ASP.NET MVC5 application AND in the new ASP.NET Core 1 application as I think the design of B4B, which is DRY, is useful in ASP.NET Core 1 too.

The parts of BundlerForBower

I am going to describe the various part of B4B, but as a start let me introduce the three main parts.

  1. A extension class called BowerBundlerHelper which needs to be placed in you MVC application so that it has access to various MVC features.
  2. A BowerBundles.json file that contains the list of bundles and their files. This is used both by Grunt/Gulp to prepare the files and by B4B to deliver the correct files at run time. Note: This file should be placed in the App_Data directory of a MVC5 project (not sure where to put it in ASP.NET Core 1).
  3. A class library called B4BCore which the BowerBundlerHelper class uses to handling bundling. This class library also contains a useful class called CheckBundles that is useful for checking all your bundles are up to date before your release anything to production.

As you can see from point 2 this approach is DRY, as the same file that is used for building the concatenated and minified production files is also used by B4B to delivery the individual files in Debug mode and the production files in non-Debug mode.

Using BowerBundlerHelper to deliver bundles

The BowerBundlerHelper extension class has two Html helper methods that are very similar to MVC5’s Styles and Scripts classes, but applied as extension methods on the HtmlHelper class. They are:

  1. @Html.HtmlCssCached("bundleName"), which is equivalent to @Styles.Render("~/Content/bundleName")
  2. @Html.HtmlScriptsCached("bundleName"), which is equivalent to @Scripts.Render("~/bundles/bundleName")

B4B makes a decision to delivers individual files or the single minfied file that Grunt produced is defined by whether the code was compiled in DEBUG mode or not. This feature can be overridden on each method by the optional ‘forceState’ parameter.

Using BowerBundleHelper to deliver static files with cachebuster added

When delivering static files, e.g. images, you need to think about what happens if you change the file content. The problem is if you change the file content but not its name then if caching is turned on the user’s browser will use the old file content, not the new file content.

The BowerBundlerHelper has a command to turn a normal file reference into one containing a cache  busting value. For instance for an image you would use something like this in your razor view:

<img src='@Html.AddCacheBusterCached("~/images/my-cat-image.jpg")' />

There is a more detailed section on this later in this article.

BowerBundles.json file format

The BowerBundles.json file holds the data on what files are in reach bundle. This is equivalent MVC5′ s BundleConfig.cs in that you define your bundles here. I happen to think its a bit simpler than BundleConfig.cs, mainly because Grunt/Gulp is doing much of the hard work.

The key thing is that this file is the single source of what files are in what bundles. This file is used both by Grunt/Gulp to concatenate and minify the files and by B4B to deliver the right files at run time. It is also used by B4B’s CheckBundles (see later) class to check that your bundles are all correct.

The file is, by default, must be called BowerBundles.json and should be placed in MVC5’s App_Data directory. The file format is a json object which can contain a mixture of two formats: one for files delivered from your web application and a second format for using a Content Delivery Network (CDN) to delivery standard libraries

1. Delivery of files from your application

For delivering bundles of files from your application then each bundle is property that contains an array of strings holding the relative file references to each file you want in a bundle. Here is a simple example:

{
  "mainCss": [
    "lib/bootstrap/dist/css/bootstrap.css",
    "Content/site.css"
  ],
  "standardLibsJs": [
    "lib/jquery/dist/jquery.js",
    "lib/bootstrap/dist/js/bootstrap.js"
  ],
  "appLibsJs": [
    "Scripts/MyScripts/*.js"
  ]
}

The name of the property, e.g. mainCss is the name of the bundle and the array is the list of files in order that should be included. So to include the mainCss bundle you would include the command @Html.HtmlCssCached("mainCss") in your _Layout.cshtml file, or whatever View that needed it.

As you can see you can specify an exact file, or add a search string like "Scripts/MyScripts/*.js", but the order is then dependant on the name and some (many) files need to be loaded in a specific order. Directory searches can include file searches as well, e.g "Scripts/*/*.js", but at the moment I have not implemented the Grunt/Gulp’s /**/ search all directories and subdirectories feature.

Please see the section in the ReadMe file which gives you the steps to add a new file bundle.

2. Delivery of files from Content Delivery Network (CDN), with fallback

B4B can also handle the delivery of JavaScript via a Content Delivery Network (CDN). You can define a CDN url, with fallback in the BowerBundles.json file using the following syntax:

  "standardLibsCndJs": [
    {
      "development": "lib/jquery/dist/jquery.js",
      "production": "jquery.min.js",
      "cdnUrl": "https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.min.js",
      "cdnSuccessTest": "window.jQuery"
    },
    {
      "development": "lib/bootstrap/dist/js/bootstrap.js",
      "production": "bootstrap.min.js",
      "cdnUrl": "https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.5/bootstrap.min.js",
      "cdnSuccessTest": "window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
    }
  ]

The individual properties are explained in the B4B ReadMe CDN section but you can see from the data above we have both a ‘development’ file, which is delivered in debug mode, and a ‘production’ file that is delivered if the ‘cdnSuccessTest’ fails.

Using the above CDN bundle, standardLibsCdnJs, in your application will insert two <script> loads, each with a JavaScript section which used the {cdnSuccessTest} code to check that the CDN had loaded properly. If the test was false then the CDN worked and nothing else happens. However if it fails then the JavaScript inserts a extra <script> load to pull in the file given by the {production} property. The code output by the first CDN definition would look like this:

<script src='https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.min.js'></script>;
<script>
   (window.jQuery||document.write(
   "\x3Cscript 'src=~/js/jquery.min.js?v=SnW8SeyCxQMkwmWggnI6zdSJoIVYPkVYHyM4jpW3jaQ\x3C/script>'));
</script>

Please see the section in the ReadMe file which gives you the steps to add a new CND bundle.

NOTE: At the moment I have not implemented CSS CDN support. The testing code is quite complex and I left it out for now. If someone wants to implement that then please let me know.

Adding a cachebuster to other static files

As well as bundles B4B can help with individual static files, e.g. images. These is a command called @Html.AddCacheBusterCached(“~/images/my-cat-image.jpg”). In this case a checksum of the file will be calculated based on its content and added as a cachebuster value.

In the case where you can pre-calculate the cachebsuter value then there is a second version which looks like this @Html.AddCacheBusterCached(“~/js/jquery.js”, “2.1.4”).

The way that the cachebuster is applied is set by the `StaticFileCaching` property in the B4B config. This means you can use different ways of applying caching busting by adding your own `BundlerForBower.json` file with a different cache busting scheme (see next section).

By default B4B uses the standard ASP.NET approach of adding a suffix, e.g. the command @Html.AddCacheBusterCached(“~/images/my-cat-image.jpg”) would produce the following html.

http://localhost:61427/images/my-cat-image.jpg?v=xKyBfWHW-GTt8h8i8iy9p5h4Gx9EszkidtaUrkwVwvY

Note: I use the SHA256 Hash which produces a hash which is related to the content. However this does take some time on large files, so I cache the SHA256 Hash to improve later access times.

B4B’s options: BundlerForBower.json

I have tried to make B4B flexible so I have put some of the key setting is a json file so that you can override them if you want to change things. I is also useful for me as ASP.NET Core 1 will need different settings. Below is the default setting for B4B, held in the file defaultConfig.json.

{
 "BundlesFileName": "BowerBundles.json",
 "StaticFileCaching": "{fileUrl}?v={cachebuster}",
 "JsDirectory": "js/",
 "JsDebugHtmlFormatString": "<script src='{fileUrl}'></script>",
 "JsNonDebugHtmlFormatString": "<script src='{fileUrl}?v={cachebuster}'></script>",
 //see http://stackoverflow.com/a/236106/1434764 about why we need to escape the document.write()
 "JsCdnHtmlFormatString": "<script src='{cdnUrl}'></script><script>({cdnSuccessTest}||document.write(\"\\x3Cscript src='{fileUrl}?v={cachebuster}'\\x3C/script>\"));</script>",
 "CssDirectory": "css/",
 "CssDebugHtmlFormatString": "<link href='{fileUrl}' rel='stylesheet'/>",
 "CssNonDebugHtmlFormatString": "<link href='{fileUrl}?v={cachebuster}' rel='stylesheet'/>",
 "CssCdnHtmlFormatString": "" //I have not currently implemented CDN for CSS files. Doable, but complicated.
}

I have used meaningful names to make the setting more comprehensible. These cover things like the names/locations of the directories where the minified files are found and the <link> and <script> code they output.

Can I point out that you can see a parameter in the setup called {cachebuster}. B4B adds a suffix to production files, just like MVC’s BundleConfig does, so that if the file changes the the new file will be used rather than the previous version in the browers local cache.

I actually use a SHA256 Hash as the cachebuster suffix rather that say the time the file was last written. This allows me to just rebuild everything and the caching suffix won’t change on files where the content is the same.

Changing the B4B options

I have used a flat object structure as that allows you to override just the item(s) you want while leaving the other properties at their default state. For instance to override just the directory where B4B looked for the JavaScript minified files then you would place the following json in a file called BundlerForBower.json in the MVC App_Data directory.

{
  "JsDirectory": "differentTopDir/bundles/"
}

See the following examples from the sample application:

  1. Override just the name of the bundle file see this file
  2. Override all the properties – see ASPNET Core 1 Config/bundlerForBower.json

Note: This last example shows how you would change the setting to match what ASP.NET Core 1 would need.

Unit Testing your bundles

Early on I was using a prototype of B4B in an e-commerce site I am working on. I deployed some code to my test site and it didn’t quite do what I had thought it should. I realised I had changed some JavaScript code and had not rebuilt the minified file.

I am pretty paranoid about problems that could hit a production site so I build a fairly comprehensive set of tests to check for any problems in the JavaScript and CSS bundles. The class is called CheckBundles.

To Unit Test your bundles then you need to create the CheckBundles in such a way that it knows where you MVC project is. If you are using the standard setup then the ctor can work this out by giving a type that is in your MVC application, e.g.

var checker = new CheckBundles(typeof(BowerBundlerHelper));

Notes:

  1. I use typeof(BowerBundlerHelper) rather than something like typeof(HomeController) as I wanted a type that did not need me to add the System.Web.Mvc assembly to my Unit Tests.
  2. There are other version of the CheckBundles ctor if you have an unusual setup. Please consult the CheckBundles code.

You most likely should run two tests:

  1. checker.CheckAllBundlesAreValid(). This checks all the bundles found in the BowerBundles.json file and returns a list of error messages. If there are no errors it returns an empty collection. (see sample project CheckBundles Unit Test example). The rules its checks against are:
    – Does the bundle contain any file references?
    – Do any of those file references use a search string that B4B does not support, e.g /**/
    – Do all of those files and their directories exist?
    – Does the concat file exist? (can be turned off via ctor param if not using concat files).
    – Was the concat file updated more recently than all the files referenced in the bundle?
    – Does the minified file exist?
    – Was the minified file updated more recently than the concat file (or all the files referenced if no concat)?
    For CDN bundles it checks:
    – Does the configuration support CDN for this file type?
    – Does your CDN bundle contain all the properties that the CDN format string needs?
    – Does any of the file definitions contain a search pattern? That is not allowed.
    – Does the ‘Development’ file and the ‘Production’ file exist?
  2. checker.CheckBundleFileIsNotNewerThanMinifiedFiles(). This does what it says. It checks that you haven’t changed the BundleFile and not run the Grunt/Gulp build process to ensure the minified files are up to date.

There is a really good example of using these methods to check your MVC bundles in the sample application. Have a look at this Unit Test class which uses CheckBundles in an NUnit based Unit Test.  I find this very helpful.

Conclusion

Hopefully this article, plus the sample application with its own ReadMe files and Unit Tests will give you good idea on whether B4B could help you. I do recommend you look at the other article called “Converting your ASP.NET MVC5 application to use Bower, Grunt and Gulp” for an overview of how to use Bower etc and how B4B fits into this.

I would appreciate your feedback on B4B. I have used it and been very happy with it, but I haven’t created a NuGet package yet. Anyone got a good link on how to produce multiple versions for each .NET version?

Happy coding!