Extending the ASP.NET Optimization Framework

We recently decided to implement .NET 4.5’s Optimization Framework, the in-built feature-set which allows you to configure bundles of JavaScript and CSS and reference these on your page in such a manner that combines-and-minifies the bundles into a single file (one for JavaScript and one for CSS).
Up until this implementation of .NET’s minification-and-bundling we had been using Cassette, and the only real cause for the changeover was the desire for integration across our .NET feature implementations.

More can be read on .NET’s minification-and-bundling here which, as far as I can tell, is the only webpage on this new feature to be published by Microsoft post-release. Like other bundling-and-minification frameworks Microsoft’s solution only combines-and-minifies when debug=”false” (which is set in the web.config), otherwise it renders references to the files in the traditional manner.

In the process of implementing this minification-and-bundling we came across a couple of shortcomings, and having spent a couple of days trying to work around these we’d like to share the solution that we came up with.

First of all, let’s cover the machinations of what goes on inside Microsoft’s bundling-and-minification Optimization Framework.
The Works

Typically to implement the Optimization Framework you will create a class, conventionally named BundleConfig. You will then be required to pass a collection of Bundles to this class. Again, conventionally you will have the following in your global.asax:

BundleConfig.RegisterBundles(BundleTable.Bundles);

BundleTable is a static class, and Bundles is a singleton instance of BundleCollection which is derived from IEnumerable.
For completeness, here are the two pertinent class declarations.

public static class BundleTable
{
    // Fields
    private static bool _enableOptimizations;
    private static bool _enableOptimizationsSet;
    private static BundleCollection _instance;
    private static Func<string, string> _mapPathMethod;
 
    // Methods
    static BundleTable();
 
    // Properties
    public static BundleCollection Bundles { get; }
    public static bool EnableOptimizations { get; set; }
    public static Func<string, string>; MapPathMethod { get; set; }
}
 
public class BundleCollection : IEnumerable, IEnumerable
{
    // Fields
    private Dictionary<string, Bundle> _bundles;
    private HttpContextBase _context;
    private Dictionary<string, DynamicFolderBundle> _dynamicBundles;
    private IgnoreList _ignoreList;
    private List _orderPriority;
    private FileExtensionReplacementList _replacementList;
    private Dictionary<string, Bundle> _staticBundles;
 
    // Methods
    public BundleCollection();
    public void Add(Bundle bundle);
    public static void AddDefaultFileExtensionReplacements(FileExtensionReplacementList list);
    public static void AddDefaultFileOrderings(IList list);
    public static void AddDefaultIgnorePatterns(IgnoreList ignoreList);
    public void Clear();
    public Bundle GetBundleFor(string bundleVirtualPath);
    protected virtual IEnumerator GetEnumerator();
    public ReadOnlyCollection GetRegisteredBundles();
    public bool Remove(Bundle bundle);
    public void ResetAll();
    public string ResolveBundleUrl(string bundleVirtualPath);
    public string ResolveBundleUrl(string bundleVirtualPath, bool includeContentHash);
    IEnumerator IEnumerable.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator();
    internal static Exception ValidateBundleVirtualPath(string bundleVirtualPath);
 
    // Properties
    internal HttpContextBase Context { get; set; }
    public int Count { get; }
    internal Dictionary<string, DynamicFolderBundle> DynamicBundles { get; }
    public FileExtensionReplacementList FileExtensionReplacementList { get; set; }
    public IList FileSetOrderList { get; }
    public IgnoreList IgnoreList { get; }
    internal Dictionary<string, Bundle> StaticBundles { get; }
    public bool UseCdn { get; set; }
}

So to summarise so far, we have a static class containing a singleton collection of Bundle objects. In other words, we have a collection of Bundle objects which is accessible across our solution.

To make use of the Optimization Framework we instantiate a Bundle object, and pass this object a list of virtual paths which each point towards a file which we wish to have it bundle.
For example, inside BundleConfig.RegisterBundles

bundles.Add(new ScriptBundle("~/scripts-1")
       .Include("~/Resources/JavaScript/jquery-1.8.3.js",
                "~/Resources/JavaScript/modernizr.form-placeholder.js"));

The above code snippet creates a Bundle and passes in a string denoting the virtual path which will be used to retrieve this Bundle, ~/scripts-1. The virtual paths for two physical script files are then passed in using the Include() method.

An important point to note here is that the specific implementation of Bundle created here is a ScriptBundle. To explain the specifics of this I’ll need to explain a little more about what’s going on inside the Optimization Framework.

We’ve already looked at the BundleTable and BundleCollection classes, and there are two other items which should also be discussed: the Bundle class and the IBundleTransform interface.
The Bundle class maintains a collection of virtual paths that should be bundled together. It also contains related properties and methods to get or set, for example, whether you have specified all files in a directory rather than specific files, returning the URL for a bundle, or for enumerating all the files in a bundle. [There are actually 15 methods and 8 properties, but there’s no need for a fuller explanation in this post.] For now, the most important property is

public IBundleTransform Transform { get; set; }

Which allows us to specify our own implementations of IBundleTransform that this Bundle will use.

Of course, at this stage you’re no doubt wondering what an IBundleTransform does. At runtime the webpage will make a call to the Optimization Framework to render the specified Bundle. The implementation of Bundle will then iterate through its Transform collection and pass the collection of physical files into that instance of IBundleTransform. For example, it is inside the built-in JsMinify implementation of IBundleTransform that minification takes place; the ScriptBundle class will automatically implement JsMinify whereas the StyleBundle class will automatically implement the built-in CssMinify implementation.

Of course, if you wished to you could exclude JsMinify or CssMinify from their respective Transforms collections if, for example, you had a class which could better perform the same task.
So to summarise once again, we have a static class (conventionally called BundleConfig) containing a singleton collection of Bundle objects. Each of these Bundle objects contains a collection of entities to be bundled together (its Items collection) and a collection of IBundleTransform implementations (its Transforms collection); the act of rendering the output of each Bundle will cause the files in that Bundle’s Items collection to be passed sequentially to each of the IBundleTransform implementations and the output from the final implementation to be rendered onto the webpage.

The final part of the jigsaw is rendering the output of a Bundle onto the webpage:

<%: Scripts.Render("~/scripts -1") %>

In the example above Scripts is a static class:

public static class Scripts
{
    // Fields
    private static HttpContextBase _context;
 
    // Methods
    public static IHtmlString Render(params string[] paths);
    public static IHtmlString Url(string virtualPath);
 
    // Properties
    internal static HttpContextBase Context { get; set; }
    private static AssetManager Manager { get; }
}

As you can see from the class declaration, if you choose to you can also have just the URLs rendered.

So the secret to customising the Optimization Framework and bending it to your will is to create your own Bundle and IBundleTransform implementations. The rest of this blog post will look into a couple of examples.

jQuery Templates

Anyone not familiar with jQuery Templates should either read this article first, or just accept this simple explanation: jQuery Templates are typically *.htm or *.html files which contain HTML as well as instructional script (which is similar to JavaScript). When these templates are passed to the jQuery plugin the plugin returns a string which will normally contain HTML, and which can then be added to the DOM. In typical use you would pass an object or an array of objects to the template and have it render HTML dependent upon the properties in the object(s).

There are a myriad of methods for maintaining these templates. In our Supertext solution we have each of them as discrete files, with the name of the template as the filename.

There are trivial methods for obtaining each template file asynchronously using jQuery (i.e. on-demand) but since we’re looking at bundling-and-minification we have good reason to investigate bundling these templates together and rendering them onto the page.

One important point here is that the recommended method of including jQuery Templates on the page is inside tags such as

<script id="your_template_name" type="text/html">// <![CDATA[

This prevents the page from both rendering the HTML contained within and from interpreting the content as JavaScript.

So with that introduction to jQuery Templates done, how can we now create a Bundle, pass it the virtual paths of jQuery Template files, and have it render automatically on to the page? There are four steps to this, the first two are as stated in the summary to the previous section: we need to create our own Bundle and IBundleTransform implementations. After this we need to build a static class which we can call from any of our pages, and the final step is to call a method on this class.

Let’s take the IBundleTransform implementation first. This class will be passed, amongst other objects, a collection of virtual paths representing each jQuery Template file. What we then want to do is read the contents of each of these, wrap this content in a tag. The implementation should really be quite trivial but the example demonstrates how we can obtain the collection of virtual paths which have been passed to this Bundle.

Notice also that the Process method has return type void. We don’t actually return the contents of the StringBuilder, we pass them to the BundleResponse object (which is derived from HttpContext) along with the relevant HTTP content type.

That’s all there is to the IBundleTransform implementation (unless you wish to add more functionality by pre-compiling your jQuery Templates). So let’s move up the order of things and look at the Bundle class that we need to build and which will use our custom Transform.

public class TemplateBundle : Bundle
{
    public TemplateBundle(string virtualPath) : base(virtualPath)
    {
        Transforms.Add(new TemplateTransform());
    }
}

As you can see, this is really quite straightforward. We’re simply deriving a class from Bundle and in the constructor we add the implementation that we’ve just created.
To demonstrate usage of our TemplateBundle class. The following code should be added to the BundleConfig class.

bundles.Add(new TemplateBundle("~/templates")
       .IncludeDirectory("~/templates/", "*.htm"));

What you can see above is that we instantiate an instance of our TemplateBundle class, passing to the constructor the virtual path which also serves as a key for each bundle, then we point it towards the templates directory and tell it to include every file with the extension .htm. Note that this IncludeDirectory() method is not specific to our Bundle implementation, the above can also be written out as, for example

bundles.Add(new TemplateBundle("~/templates")
       .Include("~/templates/template-1.htm",
                "~/templates/template-2.htm"));

Once we’ve created this bundle we add it to the bundles singleton that was discussed earlier.

So at this stage we’ve created our implementation of IBundleTransform which will be used for reading and transforming the content, we’ve derived our own Bundle class which will pass our list of content items on to our IBundleTransform implementation, and lastly for now we’ve written the code to utilise these classes and bundle the content.

The final step is now to render the content onto the webpage. We saw in an earlier example that the scripts were rendered by using the Render method on a static Scripts class. Following this logic we should derive our own version of the Scripts object, and in a flash of inspiration let’s call it Templates so we can then call a Templates.Render() method.

Unlike, say, TemplateBundle or TemplateTransform, the Templates class, like the Scripts and Styles classes, does not derive from anything. We could of course derive it from Scripts or Styles and then declare the Render and Url methods as override or new, but there seems little to be gained from this.

public static class Templates
{
    private static HttpContextBase _context;
    private static HttpContextBase Context
    {
        get { return (_context ?? new HttpContextWrapper(HttpContext.Current)); }
        set { _context = value; }
    }
 
    public static IHtmlString Render(params string[] paths)
    {
        var sb = new StringBuilder();
 
        foreach (var path in paths)
        {
            var b = BundleTable.Bundles.GetBundleFor(path);
            if (b == null) continue;
 
            var context = new BundleContext(Context, BundleTable.Bundles, path);
 
            sb.AppendLine(b.GenerateBundleResponse(context).Content);
        }
 
        return new HtmlString(sb.ToString());
    }
}

In the above example we haven’t included a Url method as this wouldn’t be relevant to jQuery Templates.

The final step is to call this static class from your webpage with

<%: Templates.Render("~/templates") %>

Remember to include a reference to the Template class’ namespace on that webpage.

So that’s the steps involved in bundling your own content: an implementation of IBundleTransform, derive your own Bundle class, configure the bundle in the BundleConfig class, create a static class that can obtain the output from the bundle implementation, then write it onto the webpage.

Bundles of Bundles

This one turned out to be a challenge that someone was looking for a solution to on StackOverflow, and also out of curiosity. It should be possible… so how do you do it?

Well first of all, what is meant by ‘Bundles of Bundles’? Let’s suppose that you’ve configured some JavaScript files to be compiled into bundle_1, and another couple of JavaScript files to be compiled into bundle_2. Is there a way in which we can create a bundle_3 which constitutes bundle_1 and bundle_2 without explicitly referencing each of the files within those respective bundles, but simply including those bundles by:

  1. declaring a new bundle, and then
  2. declaring which other bundles should be included in this bundle.

So, for example, if we have

bundles.Add(new ScriptBundle("~/bundle_1")
       .Include("~/JavaScript/jquery-1.8.3.js",
                "~/JavaScript/modernizr.form-placeholder.js"));
bundles.Add(new ScriptBundle("~/bundle_2")
       .Include("~/JavaScript/jquery.plugin.1.js",
                "~/JavaScript/another.jquery.plugin.js"));

Then how about declaring a new bundle with

bundles.Add(new BundleOfBundles("~/bundle_3")
       .Include("bundle_1", "bundle_2"));

I’ll point out that we’re not currently using this, and I’m not entirely sure that I see a good reason for compiling bundles together. I’ll offer an example of why I think this isn’t a great idea to use on your site. We have two aspects to the Supertext website: there’s the public-facing webpages, and there’s also an administration side which is used only by the Supertext staff. The public-facing webpages have a bundle called st-scripts-common-1, and the administration references that and a bundle called st-scripts-common-2. We didn’t want to combine these into a single file because to get to the administration pages the Supertext staff will login via the public-facing pages. At that stage their browsers already have the JavaScript contained with st-scripts-common-1 and so there’s nothing to be gained by bundling them once again into a single file; their browsers will see that it is referenced once again on the next page, alongside a new JavaScript file, st-scripts-common-2, but the browser will recognise that it already has this first file and therefore won’t attempt to re-download content that it already has. If we were to reference a third file, say st-scripts-common-3, which contained st-scripts-common-1 and st-scripts-common-2 then the browser would be re-downloading content that it already had.

So there’s something to be gained by not bundling everything together. This point only emphasises the need for proper management and maintenance of the contents in each bundle.

Anyway, if you do wish to compile bundles together, here’s how to do it.

As with the jQuery Templates example, create a class which implements IBundleTransform. As explained earlier, these Transform classes are the ones which are given a collection of virtual paths at runtime and which can be manipulated to do whatever you wish with the virtual paths or the contents within.

So in this specific class we’re going to receive a collection of virtual paths which don’t currently exist, but which we can take one by one, search for bundles with these names in the BundleCollection, and when we obtain each bundle we can then examine its collection of virtual paths. When we’ve done this for each bundle which this Transform class is passed we will then have a superset of virtual paths for this bundle of bundles.

So just to clarify, we are not actually going to add one bundle to another, instead we are going to look at the files which constitute each bundle, then add these into a new list.

Now, this is the stage where things get slightly awkward. Microsoft has implemented a lot of the classes and fields in the Optimization Framework as either private or internal. While this doesn’t make it impossible to use them, it really could be a lot easier. For now, to get around this (and to make this explanation easier to follow, and to prevent having to paste lots of code into this post…) I’m going to break away from the .Include(string virtualPath) convention. Instead I’ll create a private property on the implementation of IBundleTransform which will store the collection of bundle names rather than using the internal Items collection.

It’s also worth adding that the internal Include method also checks that the files exist in the file system, whereas in the case of bundles, the 'files' don’t exist because they are bundles. I hope this distinction between files and bundles is clear.

So let’s dive straight in. Here is the implementation of IBundleTransform.

public class BundleTransform : IBundleTransform
{
    private const string ContentTypeJavascript = "text/javascript";
    private const string ContentTypeCss = "text/css";
 
    // Stores the name of all the bundles that we want to combine
    public Collection Bundles { get; set; }
 
    // Stores the content-type for the HTTP response. Eg, "text/javascript", "text/css", etc.
    public string ContentType { get; set; }
 
    public BundleTransform(Collection bundles, string contentType)
    {
        Bundles = bundles;
        ContentType = contentType;
    }
 
    public void Process(BundleContext context, BundleResponse response)
    {
        // we'll use a StringBuilder to compile all our minified files into when debug="false"
        var strBundleResponse = new StringBuilder();
 
        // the collection of files is used when debug="true"
        var files = new List();
 
        // foreach bundle that we want to compile into this super-bundle...
        foreach (var bundleName in Bundles)
        {
            // ...obtain the specified bundle from the BundleCollection
            var bundle = context.BundleCollection.GetBundleFor(bundleName);
            if (bundle == null) continue;
 
            var output = bundle.GenerateBundleResponse(context);
 
            // the Content is the actual JavaScript or CSS
            var outputContent = output.Content;
 
            // append this to our output string buffer
            strBundleResponse.Append(outputContent);
 
            // looks like when a bundle has been completed the ';' is omitted from the end
            if (output.ContentType == "text/javascript" &amp;&amp; !outputContent.EndsWith(";"))
            {
                strBundleResponse.Append(";");
            }
        }
 
        // this assigns our super-list of all bundled files to the collection which is rendered inside the HTML tags
        // - used when debug="true"
        response.Files = files;
 
        // this assigns the total combined-and-minified output from all the files
        // - used when debug="false"
        response.Content = strBundleResponse.ToString();
        // of course we should let the client know what the content-type is
        response.ContentType = ContentType;
    }
}

Ok, there’s a lot of code in there, but I hope that the comments will keep you on the straight and narrow. What can be seen is that we have an internal collection property

public Collection Bundles { get; set; }

And that it is this internal collection that we loop through to obtain each bundle rather than the response.Files collection, which in this case will be empty because we haven’t made any calls to Include().

So when the Process method is called it accesses the implementation’s own collection of bundle names, then uses the static context.BundleCollection to retrieve the requested Bundle.

As in the jQuery Template example, we then simply open the file and read its contents into a string buffer, then we move onto the next file in that bundle, and then we move onto the next bundle.
There are certainly optimisations available here: for example, each generated bundle has a ContentType property. It’s probably a good idea to check this to ensure that each bundle which is being appended has the same ContentType as its sibling bundles; it’s no good throwing a JavaScrpt bundle in with a CSS bundle. It’s also possible at this stage to read in the unminified contents and append them together, then minify the contents in a single pass.

Anyway, that’s the IBundleTransform implementation over with. Let’s now look at the Bundle class which we’ll derive which will implement our transform.

public class BundledScriptsBundle : Bundle
{
    private const string ContentTypeJavascript = "text/javascript";
 
    public BundledScriptsBundle(string virtualPath, Collection bundles) : base(virtualPath)
    {
        Transforms.Add(new BundleTransform(bundles, ContentTypeJavascript));
    }
}

The only great point to take note of here is that the constructor requires a collection of strings which is where we enumerate each of the bundle names. This gets passed straight onto the Transform which we’ve just looked at. Also, we’re hard-coding the content-type, but then we have called this class BundledScriptsBundle so there are no excuses for confusion here.

So a quick summary: we’ve created an implementation of IBundleTransform which enumerates a collection of bundle names and appends them into one string buffer. We then derived our own Bundle class which requires a collection of bundle names, then implements the aforementioned transform.

Not far to go now, all we have to do is create an instance of our derived Bundle class and pass in the names of the bundles that we wish to have compiled into a single entity.

bundles.Add(new BundledScriptsBundle("~/super-bundle",
    new Collection {
        "~/scripts-1",
        "~/scripts-2" }));

Like in earlier examples, we create a new instance of BundledScriptsBundle, declare the name that we wish to retrieve this super-bundle by, and then we declare a collection of strings containing, in this case, two pre-declared bundles, scripts-1 and scripts-2.

The last step is to render the contents of this super-bundle onto a webpage. For this we can utilise the out-of-the-box static Scripts class.

<%: Scripts.Render("~/super-bundle") %>

And that’s it. Load the page, there’s your super-bundle of bundles.

Of course, there are loads of optimisations that can be done to this but the purpose of putting together this blog post was to look at how we can customise the Optimization Framework to achieve what we want, and the exact implementation is not directly relevant.

As a last point we’d like to discuss another area which I’m sure lots of developers will be trying to implement.

LESS

We started using LESS in place of CSS a few months ago. Out of the box the Optimization Framework does not support LESS but Microsoft’s only page on the Optimization Framework contains an example of how it can be added. Unfortunately this suggested solution only works when the application is not running in debug mode.

When debug="false" the referenced files are combined-and-minified into a CSS file and a link to this is rendered onto the page.

However, when debug="true" this combining-and-minification doesn’t take place, so what you’re left with is on-page references to your LESS files. Not only will a client browser be unable to comprehend these (wouldn’t it be great if browsers could natively read LESS?), IIS doesn’t have a handler for that file extension.

The solution for this was quite straightforward: all the developers here at Supertext have the Web Essentials plugin for Visual Studio installed. If configured correctly Web Essentials will create a 'buddy file' for each *.less file at the point of file creation. [A 'buddy file' is one of those dependant files which Visual Studio shows underneath another file when you click the adjacent arrow. For example, an ASPX file has *.aspx.cs and *.aspx.designer.cs buddy files.] These buddy files are the *.css version and the *.min.css version. Therefore, all you have to do to implement bundling-and-minification of LESS files regardless of debug mode is to ignore the advice on Microsoft’s page for achieving this and instead use the Web Essentials plugin. It’s not as neat and tidy as it should be because every developer working on the solution must have this plugin installed, but once it is installed it requires no further configuration to achieve the bundling-and-minification of CSS.

As an addendum to this point about using LESS, something else to keep in mind is that when debug=”true” the Optimization Framework will not bundle files which contain ".min" in the filename. Again, Web Essentials has us covered because it creates both un-minified and minified versions of the files, so no further configuration is necessary.

Summary

I hope this blog post has helped you to solve a problem, or has helped create an idea you’re now going to follow-up. Like we said earlier, please don’t pay too much attention to the exact specifics of the code we’ve used to, for example, read the contents of a file, or how we’ve appended the files together; the purpose of this post was to disassemble Microsoft’s bundling-and-minification solution, the Optimization Framework, and discuss how we can derive and implement our own classes to achieve what we want.




Ähnliche Beiträge


7 Kommentare zu “Extending the ASP.NET Optimization Framework”



  • Dew Drop – February 6, 2013 (#1,494) | Alvin Ashcraft's Morning Dew am 6. February 2013 8:44 am Uhr

    [...] Extending the ASP.NET Optimization Framework (Andrew Jameson) [...]


  • Martin am 6. February 2013 9:45 am Uhr

    The only problem I have with using the Web Essentials tool for LESS support is that you have to save your .less file in order to compile it into the .css files.

    I like to create a LESS file for each module on my site, and import them all into my site.less file. When I make a change on say my nav.less, and go to the browser, nothing changes, because I need to manually go back and save the site.less each time. Kind of a pain, but it works.


  • Rémy Blättler am 6. February 2013 9:56 am Uhr

    @Martin: Bundle Transformer: LESS could be an alternative.
    http://bundletransformer.codeplex.com/
    We have not tried it out, but sounds like it would solve your issue.


  • jameson am 8. February 2013 6:55 am Uhr

    @Martin – take a look in the BUILD menu, there’s a Web Essentials sub-menu which has options for re-compiling or re-building all your bundles and LESS files.


  • How to Extend the ASP.NET Optimization Framework | Best Windows Web Hosting am 27. February 2013 4:56 am Uhr

    [...] are other steps involved in extending ASP.NET Optimization Framework, so I suggest to read this ASP.NET blog for a detailed [...]


  • Eric Sassaman am 3. March 2013 7:35 am Uhr

    I highly recommend the bundle transformers Remy noted above. The best thing is that if optimizations are off, his transformer still processes the .less files so they are perfectly useable. Before I switched to Andrey’s LESS transformer, you could use the standard less scripts whken optimizations are off by doing something like this, which worked great for me:

    <% if (!BundleTable.EnableOptimizations)
    {
    Response.Write("”);
    Response.Write(“”);
    }
    else
    {
    Response.Write(Styles.Render(“~/styles/less”));
    } %>

    Where ~/styles/less is my bundled version rendered when optimizations are on.


  • Eric Sassaman am 3. March 2013 7:38 am Uhr

    Well looks like my HTML tags were stripped out. Sorry I can’t post that here, but basically just put your link tag to your .less file and script tag to your less.js in those two empty response.writes and you’ll be good to go.


Leave a Reply

Your email address will not be published. Required fields are marked *



*