Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Effective Bundling With ASP.NET MVC

DZone's Guide to

Effective Bundling With ASP.NET MVC

In this post, we take a look at some effective bundling techniques to help streamline your ASP.NET MVC application. Ready? Let's go!

· Web Dev Zone ·
Free Resource

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Bundling and minification have 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! The 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 the extensions folder of the AspNetMvcBundleMinify application

Default Bundle Config

Let's start with the 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 another for styles. This way we get the number of requests to the web server down. Here is the default page of an ASP.NET MVC application created with Visual Studio.

Optimizing Bundles

Leaving modernizr as an exception, I modify the 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;    
  }
}

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

Solving File Ordering Issues

Even now bundling should work fine. But I have seen some cases when the order of the files in the 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 the DefaultBundleOrderer class. This class uses FileSetOrderList to read the files in the bundle.

Those who cannot upgrade their solution to the latest version and have file ordering issues can use the 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 the bundle and adds no unexpected logic. To make it work we just have to assign it to a 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 a good example). jQUery UI script is put into the Scripts folder or our application. Styles with images are added to the Content/theme/base folder. There's also a folder for images.

To see if the dialog works, we change the Index view of the Home controller. I removed most of the default content to keep the 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 the 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 the browser and clicking the Open dialog button with developer tools open, we can see that some files are missing.

As the styles bundle has a 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 the files we add to bundles. For CSS, there is the CssRewriteUrlTransform class. Let's add files to the 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 now have the correct paths.

I Have Issues With CSS Path Transform

Some older MVC applications may use a buggy CSS transform class. For those, I have a replacement class available. It's taken from a StackOverflow thread, MVC4 StyleBundle not resolving images. Just use it in place of the 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 the 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 the CSS path transform. For this, we can write an 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 ordered bundle classes and path transform extension methods, we can write our bundle config class as 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 the common problems people have faced. We were able to solve file ordering and CSS path transform problems. Also, we have a 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 the Include() method that applies path transform to CSS files automatically. The tricks given here should solve most of the problems we have faced with bunding in classic ASP.NET MVC applications.

Deploying code to production can be filled with uncertainty. Reduce the risks, and deploy earlier and more often. Download this free guide to learn more. Brought to you in partnership with Rollbar.

Topics:
c# ,asp.net mvc ,bundling ,modernizr ,web dev

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}