Bundling and Minifying an AngularJS Application with ASP.NET MVC

Bundling and minifying a website's scripts and stylesheets reduces page load time and asset size. Here's my project setup for bundling and minifying scripts of an AngularJS application hosted within an ASP.NET MVC site. If you're new to bundling and minification, make sure to read my introduction to bundling and minification first.

If you want to follow along while reading this post, I recommend you check out this little demo application I've put together. It showcases the various parts involved in creating and rendering script bundles. Oh, and I couldn't help but make it Lord of the Rings-themed. Forgive me if you're not a fan!

Project Structure

Let's take a look at the project structure I've used in the demo application:

Project Structure of an ASP.NET MVC Application Hosting an AngularJS application

As you can see, it's mostly the default structure for an ASP.NET MVC application except for a few differences. Rather than putting my JavaScript files in the Scripts folder and both images and CSS files in the Content folder, I like to nest all assets for the browser underneath Client to separate them from my server-side code.

Within the Client folder, we're entering the realms of front-end web development, so I like to adapt my naming strategy to lowercase-hyphenated-casing rather than PascalCasing. Angular services are an exception to this rule because I prefer the file names to exactly correspond to the internal names under which the services are registered.

All JavaScript libraries and frameworks we use live underneath vendor. Scripts belonging to our Angular application, on the other hand, are located within the app folder with the app.js file containing the main module definition. To reduce load times and payload size of our application, we want to go ahead and automatically minify all files within the app folder and bundle them together.

(We're not going to bundle the libraries and frameworks in this post. Those should be fetched from a CDN to take advantage of the possibility that they're already in the user's browser cache. Of course, you should still define a fallback script just in case the CDN is unreachable for whatever reason.)

Installing the Optimization Framework

We're going to use the ASP.NET Web Optimization Framework to bundle together and minify all scripts of an AngularJS application hosted within an ASP.NET MVC site. It's available on NuGet and can be installed quickly via the following command:

Install-Package Microsoft.AspNet.Web.Optimization

Besides dealing with JavaScript files, the Web Optimization Framework can also bundle and minify CSS files (and even other types of files, given you provide a custom bundle transformation). For the purpose of bundling and minifying our Angular demo application, we're sticking to only JavaScript bundles in this post, though.

Bundling All Angular Application Scripts

Let's create a BundleConfig.cs file underneath the App_Start folder which will define our script bundle within a RegisterScriptBundles method. Here's how we call it from within Global.asax.cs, passing it the global variable holding all bundles in a collection:

BundleConfig.RegisterScriptBundles(BundleTable.Bundles);

Here's a first stab at the implementation:

using System.Web.Optimization;

namespace AngularMvcBundlingMinification
{
    public static class BundleConfig
    {
        public static void RegisterScriptBundles(BundleCollection bundles)
        {
            const string ANGULAR_APP_ROOT = "~/Client/scripts/app/";
            const string VIRTUAL_BUNDLE_PATH = ANGULAR_APP_ROOT + "main.js";

            var scriptBundle = new ScriptBundle(VIRTUAL_BUNDLE_PATH)
                .IncludeDirectory(ANGULAR_APP_ROOT, "*.js", searchSubdirectories: true);

            bundles.Add(scriptBundle);
        }
    }
}

The ANGULAR_APP_ROOT points to our app folder, and the VIRTUAL_BUNDLE_PATH holds the name of the bundled script file we'll emit later. We're then creating an instance of the ScriptBundle class and adding to it all JavaScript files underneath app using the IncludeDirectory method. To do that, we specify the pattern *.js and a recursive directory traversal.

A nice side effect of this wildcard syntax is that you don't need to explicitly add new Angular scripts to the script bundle. If you define new services within the app folder, the Web Optimization Framework will pick up on those new files automatically. It's a joy to work with!

Our bundle now contains all the files we need, but what about their order? We can't register Angular services on a module that doesn't exist yet. Therefore, we have to somehow make sure the module definition comes first.

Ensuring Correct File Order in the Bundle

If an Angular service attempts to register itself with a module that doesn't exist, the framework will complain and we'll see the following error message in the browser console:

Angular Error: Module Unavailable

The solution to this problem is quite simple, actually. We have to change the file inclusion order to ensure the app.js file is included first:

var scriptBundle = new ScriptBundle(VIRTUAL_BUNDLE_PATH)
    .Include(ANGULAR_APP_ROOT + "app.js")
    .IncludeDirectory(ANGULAR_APP_ROOT, "*.js", searchSubdirectories: true);

Fortunately, the Web Optimization Framework won't include the app.js script twice, even though the *.js pattern passed to the IncludeDirectory method matches the file name, too. It will instead recognize it has already seen the file and simply disregard any additional includes of app.js.

Rendering the Script Bundle

Now that we've defined a bundle for our Angular application, we need to render the appropriate <script> tags in our Razor layout view. We do that by calling the Scripts.Render() static method (found in the System.Web.Optimization namespace) with the virtual path identifying the script bundle.

To avoid having to manually reference this namespace at the top of the containing Razor file, we're going to include it in the Web.config file within the Views folder:

<system.web.webPages.razor>
    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <pages pageBaseType="System.Web.Mvc.WebViewPage">
        <namespaces>
            <!-- ... -->
            <add namespace="System.Web.Optimization" />
        </namespaces>
    </pages>
</system.web.webPages.razor>

Inside your Razor views, IntelliSense should now suggest the Scripts.Render method:

IntelliSense Suggesting the Scripts.Render() Method

Now that that's taken care of, let's take a look at the <script> tags we want to render. Depending on the value of the debug attribute on the <compilation> tag in your ASP.NET application's main Web.config file, the Web Optimization Framework will emit different forms of <script> tags.

If debug="true" is set, every script file in the bundle will be requested through a separate <script> tag. Also, the scripts won't be minified. This helps during development time because you're working with the original source file which you can easily debug into:

<!-- Angular application scripts -->
<script src="/Client/scripts/app/app.js"></script>
<script src="/Client/scripts/app/controllers/ElvenRingsController.js"></script>
<script src="/Client/scripts/app/controllers/FellowshipController.js"></script>
<script src="/Client/scripts/app/directives/wikipediaLink.js"></script>

In case debug="false" is set or the attribute is removed through a Web.config transformation (such as Web.Release.config), each bundle will be emitted as only one <script> tag referencing the minified and concatenated bundle file. Also, the Web Optimization Framework will include a cachebreaker in the URL:

<!-- Angular application scripts -->
<script src="/Client/scripts/app/main.js?v=82p3oFlAKRu4Bx3_mEBzPrRCr1IEEJY_AfBpok4CIx01"></script>

Browsers are thus forced to request and use the latest bundle version rather than a possibly outdated one from their cache.

Dependency Resolution with Minified Angular Code

Before we finish up, there's one more thing we need to take care of, and it's about not breaking Angular's dependency resolver when minifying the service script files.

You shall not break when minified!

Angular infers a controller's dependencies from the names of the arguments passed to its constructor function. This is why you can simply list all dependencies in the constructor and have those parameters "magically" filled with appropriate values:

(function() {
    angular
        .module("lordOfTheRings")
        .controller("FellowshipController", FellowshipController);

    function FellowshipController($scope) {
        $scope.fellowship = {
            companions: [
                "Frodo",
                "Sam",
                "Merry",
                "Pippin",
                "Gandalf",
                "Aragorn",
                "Legolas",
                "Gimli",
                "Boromir"
            ]
        };
    }
}());

As long as you use this controller in its unminified version, the $scope parameter will be injected correctly. If we minify the above code, however, the output looks something along the lines of this (with line breaks added for legibility):

!function(){function o(o){o.fellowship={companions:["Frodo","Sam","Merry",
"Pippin","Gandalf","Aragorn","Legolas","Gimli","Boromir"]}}
angular.module("lordOfTheRings").controller("FellowshipController",o)}();

Note that the $scope argument name has been shortened to o. Now the dependency resolution, being purely based on argument names, won't work correctly anymore. Of course, the Angular team is aware of this issue and offers a minification-safe solution.

Minification-Safe Angular Services

While JavaScript minifiers will shorten identifiers where possible, they won't modify any string literals in your code. The idea is to provide the dependency names as a separate array of strings which will survive the minification process. My favorite approach to pass this array to the constructor function is through the $inject property:

angular
    .module("lordOfTheRings")
    .controller("FellowshipController", FellowshipController);

    FellowshipController.$inject = ["$scope", "$http", "$q"];

    function FellowshipController($scope, $http, $q) {
        // ...
    }

Another approach is outlined in the Angular documentation. It uses a more compact syntax that I've found to be slightly harder to read with several dependencies. Besides that, both approaches work exactly the same: They ensure your dependencies are resolved correctly despite minification.

Related Links:

Use the coupon code LAUNCHDAY for $10 off!

Learn ES6

20 Comments

jim

Thanks. Just started a MVC project after years of web forms. Will the same general code work on web forms? Also lets say I have angular framework, jQuery stuff and a small amount of my JavaScript - will the user have a better experience with CDN vs me bundling?

Marius Schulz

Jim: Yes, the bundle definition should probably be the same, just the way the bundle is referenced will differ. Check out Adding Bundling and Minification to Web Forms.

As for CDNs: You should attempt to load Angular and jQuery from a CDN, but with a local fallback. I usually only bundle my own scripts while fetching the frameworks and libraries from a CDN.

Carlos Bolanos

Marius thx i was missing the $inject part the time of declaring the Controller and dependencies on the App.js file.

i would definetly give it a try thx!

Sebastain Beyer

If debug is set to false in web.config. The order of the scripts is not respected. At leas in MVC 4. So app is not the first script in the minified version and it does not work

Tahir Alvi

Hi Marius,

The article is really help specially for those who want to start working with ASP.NET & AngularJS.

Thanks a lot for sharing & Keep that spirit Up.

Tahir Alvi

Armando

This is awesome, just what I was looking for. Thanks!

Steven

This is a well thought article and is helpful for new developers. I enjoyed it well enough, great job.

But if you are like me, and AngularJS SPA's are amazing not only for the obvious reasons, but also because you get to move away from that heavy B.S. called Microsoft MVC, then this is not the right answer. When you do this, your html apps (now CSHTML) need to go through the View Engine to be Razor Compiled. So what have you bought? You now have bundles and save time, but lose time because you no longer have an AngularJS SPA, you have an MVC View Engine slowing you down. And they at LEAST Cancel each other out, if not slow you down more.

The best way to benefit is to package your own bundles explicitly, add them to a CDN you own (or should create), and have your SPA Apps reference the CDN/Version. That has much more benefit than you can imagine and is nothing short of the fastest speeds/caching strategy you can get. Maybe I'll write an article in my own blog about how to do that, but there are enough if you Google how to CDN.

Renso

Great article. @Steven, I don't think Marius' example was promoting using MVC bundling over CDN. I think everyone knows CDN's are faster. Marius, thank you for pointing out how to tie it in with MVC. One point I would like to make is SPA are a real issue if you have a very large web app. I prefer an MVC/Angularjs hybrid so that I can employ mini SPAs in my MVC app and use MVC routing to route between my SPAs. This keeps my JS file clusters isolated by mini SPA so that I don't have to load all my scripts in the index page.

optional

Thanks. Just started a MVC project after years of web forms. Will the same general code work on web forms? Also lets say I have angular framework, jQuery stuff and a small amount of my JavaScript - will the user have a better experience with CDN vs me bundling?

Anil

I have an Issues: How to minify angularjs scripts? Go in detail http://goo.gl/d2t8Ox

Thanks, Anil

casey

@steven The time in the view engine is going to be minimal and it only needs to happen once because your SPA is never going to refresh the page

eshan.sarpotdar

That's an amazing post on Angular Js and ASP.NET. You have meticulously highlighted the crux of using this framework. Following up on this, I came across and registered for a webinar on 'Building Amazing Web Applications Rapidly with Emerging Tech' and gain insights on how to leverage the emerging trends and technologies in web development to build a quality app rapidly, here is the registration link, it does look promising- http://j.mp/1QCH5nf

Akash Kumar

Good contain

Dinesh

Nice article, Thanks for sharing. Here also I found video tutorial for implementing B&M with need and concepts

http://modernpathshala.com/Forum/Thread/Interview/7158/bundling-and-minification-in-mvc

matt

Thanks for the write-up. Exactly what I was looking for!

shaimaa

Hello , i have a project i am using angular js but not in mvc framework , can i install this package for optimization to bundle my files or there is a better way ?

Manoj

Hi Marius,

This is a great article and very well written. Demo code is also useful.

Wanted to share something - in this bundling + minification method there is a drawback I see. If the web application restarts then a new version number would be generated. So even if no files have changed the users cache would be invalidated and files would load again. Not an ideal situation.

Is there some way we can generate our own version number? So if I am making a new release only then will I change the version number.

harry

my html has line(with angularjs ):<report print="true" ></report>

when i minify it ,the line changed to:<report print></report>

it lost the:="true"

my app run error

Vivek Ealumalai

Great Article and neat walk through. helped a lot !!