X

Effective bundling with ASP.NET MVC

Bundling and minification has been available in ASP.NET MVC for a long time. This blog post focuses on problems people have had with bundling and provides working solutions for those who cannot use bundling in ASP.NET MVC for different reasons. Also some ideas about more effective bundling are presented here.

Source code available! Solution with ASP.NET MVC web application that contains code given here is available in my GitHub repository gpeipman/AspNetMvcBundleMinify. All extensions shown here are available in extensions folder of AspNetMvcBundleMinify application

Default bundle config

Let’s start with default bundle config. This is what is generated when we create new ASP.NET MVC application.

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

        bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                    "~/Scripts/jquery.validate*"));

        // Use the development version of Modernizr to develop with and learn from. Then, when you're
        // ready for production, use the build tool at https://modernizr.com to pick only the tests you need.
        bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                    "~/Scripts/modernizr-*"));

        bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
                    "~/Scripts/bootstrap.js"));

        bundles.Add(new StyleBundle("~/Content/css").Include(
                    "~/Content/bootstrap.css",
                    "~/Content/site.css"));
    }
}

The idea here is to have granular bundles but this not what we need in web applications usually. In most cases we need one bundle for scripts and other for styles. This way we get the number of requests to web server down. Here is the default pahe of ASP.NET MVC application created with Visual Studio.

Optimizing bundles

With leaving modernizr as exception I modify bundling code to have two general bundles.

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

        // Use the development version of Modernizr to develop with and learn from. Then, when you're
        // ready for production, use the build tool at https://modernizr.com to pick only the tests you need.
        bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                    "~/Scripts/modernizr-*"));

        bundles.Add(new StyleBundle("~/bundles/css").Include(
                    "~/Content/bootstrap.css",
                    "~/Content/site.css"));

        BundleTable.EnableOptimizations = true;
    }
}

EnableOptimizations property tell bundling to enable optimizations no matter what configuration we are using. This way it is easy to see if bundling works when application runs with Debug configuration.

Solving file ordering issues

Even now bundling should work fine. But I have seen some cases when the order of files in bundle is incorrect. Also some of my fellow developers have told me about this weird issue. It seems to be an issue with older versions. By default, all bundles use DefaultBundleOrderer class. This class uses FileSetOrderList to read files in bundle.

Those who cannot upgrade solution to latest version and have file ordering issue can use custom orderer by Master-Inspire.

public sealed class AsIsBundleOrderer : IBundleOrderer
{
    public IEnumerable<BundleFile> OrderFiles(BundleContext context, IEnumerable<BundleFile> files)
    {
        return files;
    }
}

This class keeps the original order of files in bundle and adds no unexpected logic. To make it work we just have to assign it to bundle.

var scriptBundle = new ScriptBundle("~/bundles/scripts")
                        .Include("~/Scripts/jquery-{version}.js")
                        .Include("~/Scripts/jquery.validate*")
                        .Include("~/Scripts/bootstrap.js")
                        .Include("~/Scripts/application.js");
scriptBundle.Orderer = new AsIsBundleOrderer();
bundles.Add(scriptBundle);

In case of file ordering problems the same orderer can be assigned to styles bundle.

Fixing image paths in stylesheets

One special case that is not handled in our code are components that refer to images in stylesheets. Let’s add jQuery UI to our application through NuGet (yes, it’s old but still good example). jQUery UI script is put to Scripts folder or our application. Styles with images are added to Content/theme/base folder. There’s also folder for images.

To see if dialog works we change Index view of Home controller. I removed most of default content to keep view small.

@{
    ViewBag.Title = "Home Page";
}

<div class="jumbotron">
    <h1>ASP.NET</h1>
    <p class="lead">Let's test jQuery UI dialog</p>
    <p><a href="https://asp.net" class="btn btn-primary btn-lg orange">Open dialog &raquo;</a></p>
</div>

<div id="sampleDialog" style="display:none">
    <p>I am sample dialog</p>
</div>

We have to add jQuery UI files also to our bundles.

public static void RegisterBundles(BundleCollection bundles)
{
    var scriptBundle = new ScriptBundle("~/bundles/scripts")
                            .Include("~/Scripts/jquery-{version}.js")
                            .Include("~/Scripts/jquery.validate*")
                            .Include("~/Scripts/bootstrap.js")
                            .Include("~/Scripts/jquery-ui-{version}.js")
                            .Include("~/Scripts/application.js");
    scriptBundle.Orderer = new AsIsBundleOrderer();
    bundles.Add(scriptBundle);

    // Use the development version of Modernizr to develop with and learn from. Then, when you're
    // ready for production, use the build tool at https://modernizr.com to pick only the tests you need.
    bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                "~/Scripts/modernizr-*"));

    var stylesBundle = new StyleBundle("~/bundles/css")
                                .Include("~/Content/themes/base/jquery-ui.min.css")
                                .Include("~/Content/themes/base/all.css")                              
                                .Include("~/Content/bootstrap.css")
                                .Include("~/Content/site.css")
                                .Include("~/Content/application.css");
    stylesBundle.Orderer = new AsIsBundleOrderer();
    bundles.Add(stylesBundle);
           
    BundleTable.EnableOptimizations = true;
}

When opening the view in browser and clicking Open dialog button with developer tools open we can see that some files are missing.

As styles bundle has custom virtual path the files are not found anymore as their location is not updated in CSS.

The solution is simple. We can use transforms on files we add to bundles. For CSS there is CssRewriteUrlTransform class. Let’s add files to styles bundle using this transform.

var stylesBundle = new StyleBundle("~/bundles/css")
                            .Include("~/Content/themes/base/jquery-ui.min.css", new CssRewriteUrlTransform())
                            .Include("~/Content/themes/base/all.css", new CssRewriteUrlTransform())                              
                            .Include("~/Content/bootstrap.css", new CssRewriteUrlTransform())
                            .Include("~/Content/site.css", new CssRewriteUrlTransform())
                            .Include("~/Content/application.css", new CssRewriteUrlTransform());

And here is the result.

Images used in stylesheets have now correct paths.

I have issues with CSS path transform

Some older MVC applications may use buggy CSS transform. For those I have replacement class available. It’s taken from Stackoverflow thread MVC4 StyleBundle not resolving images. Just use it in place of CssUrlRewriteTransform class.

public void Process(BundleContext context, BundleResponse response)
{
    Regex pattern = new Regex(@"url\s*\(\s*([""']?)([^:)]+)\1\s*\)", RegexOptions.IgnoreCase);

    response.Content = string.Empty;

    // open each of the files
    foreach (BundleFile bfile in response.Files)
    {
        var file = bfile.VirtualFile;
        using (var reader = new StreamReader(file.Open()))
        {
                   
            var contents = reader.ReadToEnd();

            // apply the RegEx to the file (to change relative paths)
            var matches = pattern.Matches(contents);

            if (matches.Count > 0)
            {
                var directoryPath = VirtualPathUtility.GetDirectory(file.VirtualPath);

                foreach (Match match in matches)
                {
                    // this is a path that is relative to the CSS file
                    var imageRelativePath = match.Groups[2].Value;

                    // get the image virtual path
                    var imageVirtualPath = VirtualPathUtility.Combine(directoryPath, imageRelativePath);

                    // convert the image virtual path to absolute
                    var quote = match.Groups[1].Value;
                    var replace = String.Format("url({0}{1}{0})", quote, VirtualPathUtility.ToAbsolute(imageVirtualPath));
                    contents = contents.Replace(match.Groups[0].Value, replace);
                }

            }
            // copy the result into the response.
            response.Content = String.Format("{0}\r\n{1}", response.Content, contents);
        }
    }
}

Improving bundling code

Our bundles work now and it’s time to make code look better. We can get rid of assigning the orderer to bundles by defining ordered bundle classes.

public class OrderedScriptBundle : ScriptBundle
{
    public OrderedScriptBundle(string virtualPath) : this(virtualPath, null)
    {
    }

    public OrderedScriptBundle(string virtualPath, string cdnPath) : base(virtualPath, cdnPath)
    {
        Orderer = new AsIsBundleOrderer();
    }
}

public class OrderedStyleBundle : StyleBundle
{
    public OrderedStyleBundle(string virtualPath) : this(virtualPath, null)
    {
    }

    public OrderedStyleBundle(string virtualPath, string cdnPath) : base(virtualPath, cdnPath)
    {
        Orderer = new AsIsBundleOrderer();
    }
}

There’s also one repeated this – including CSS path transform. For this we can write extension method to keep our bundling code shorter.

public static class BundleExtensions
{
    public static Bundle IncludeWithRewrite(this Bundle bundle, string virtualPath)
    {
        bundle.Include(virtualPath, new CssRewriteUrlTransform());

        return bundle;
    }
}

Using orderred bundle classes and path transform extension method we can write our bundle config class like shown here.

public static void RegisterBundles(BundleCollection bundles)
{
    var scriptBundle = new OrderedScriptBundle("~/bundles/scripts")
                            .Include("~/Scripts/jquery-{version}.js")
                            .Include("~/Scripts/jquery.validate*")
                            .Include("~/Scripts/bootstrap.js")
                            .Include("~/Scripts/jquery-ui-{version}.js")
                            .Include("~/Scripts/application.js");
    bundles.Add(scriptBundle);

    // Use the development version of Modernizr to develop with and learn from. Then, when you're
    // ready for production, use the build tool at https://modernizr.com to pick only the tests you need.
    bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                "~/Scripts/modernizr-*"));

    var stylesBundle = new OrderedStyleBundle("~/bundles/css")
                                .IncludeWithRewrite("~/Content/themes/base/jquery-ui.min.css")
                                .IncludeWithRewrite("~/Content/themes/base/all.css")                              
                                .IncludeWithRewrite("~/Content/bootstrap.css")
                                .IncludeWithRewrite("~/Content/site.css")
                                .IncludeWithRewrite("~/Content/application.css");
    bundles.Add(stylesBundle);
           
    BundleTable.EnableOptimizations = true;
}

Using this new code we still have our bundle config almost like it was before but it works smarter.

Wrapping up

Although there have been good and bad times in ASP.NET MVC bundling there are still solutions available for all common problems people have faced. We were able to solve file ordering and CSS path transform problems. Also we have replacement class for path transforms to use with some problematic releases. To close the topic we created our own ordered bundle classes and version of Include() method that applies path transform to CSS files automatically. Tricks given here should solve most of problems we have faced with bunding in classic ASP.NET MVC applications.

Liked this post? Empower your friends by sharing it!
Categories: ASP.NET

View Comments (2)

  • Hi, Nice Article,
    Bundling and minification are two techniques you can use in ASP.NET 4.5 to improve request load time. Bundling and minification improves load time by reducing the number of requests to the server and reducing the size of requested assets (such as CSS and JavaScript.)

    Most of the current major browsers limit the number of simultaneous connections per each hostname to six. That means that while six requests are being processed, additional requests for assets on a host will be queued by the browser. In the image below, the IE F12 developer tools network tabs shows the timing for assets required by the About view of a sample application.

  • How do i apply compression for bundled JS files. I'm able to compress inline JS/CSS files but not the bundles.

Related Post