How can I specify an explicit ScriptBundle include order?

asp.net Mvcasp.net Mvc-4Bundleasp.net Optimization

asp.net Mvc Problem Overview


I'm trying out the MVC4 System.Web.Optimization 1.0 ScriptBundle feature.

I have the following configuration:

public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        // shared scripts
        Bundle canvasScripts =
            new ScriptBundle(BundlePaths.CanvasScripts)
                .Include("~/Scripts/modernizr-*")
                .Include("~/Scripts/json2.js")
                .Include("~/Scripts/columnizer.js")
                .Include("~/Scripts/jquery.ui.message.min.js")
                .Include("~/Scripts/Shared/achievements.js")
                .Include("~/Scripts/Shared/canvas.js");
        bundles.Add(canvasScripts);
    }
}

and the following view:

<script type="text/javascript" src="@Scripts.Url(BundlePaths.CanvasScripts)"></script>

where BundlePaths.CanvasScripts is "~/bundles/scripts/canvas". It renders this:

<script type="text/javascript" src="/bundles/scripts/canvas?v=UTH3XqH0UXWjJzi-gtX03eU183BJNpFNg8anioG14_41"></script>

So far so good, except ~/Scripts/Shared/achievements.js is the first script in the bundled source. It depends on every script included before it in the ScriptBundle. How can I ensure that it honors the order in which I add include statements to the bundle?

Update

This was a relatively new ASP.NET MVC 4 application, but it was referencing the optimization framework pre release package. I removed it and added the RTM package from http://nuget.org/packages/Microsoft.AspNet.Web.Optimization. With the RTM version with debug=true in web.config, @Scripts.Render("~/bundles/scripts/canvas") renders the individual script tags in the correct order.

With debug=false in web.config, the combined script has the achievements.js script first, but since its a function definition (object constructor) that's called later, it runs without error. Perhaps the minifier is smart enough to figure out dependencies?

I also tried the IBundleOrderer implementation that Darin Dimitrov suggested with RTM with both debug options and it behaved the same.

So the minified version is not in the order I expect, but it works.

asp.net Mvc Solutions


Solution 1 - asp.net Mvc

You could write a custom bundle orderer (IBundleOrderer) that will ensure bundles are included in the order you register them:

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

and then:

public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        var bundle = new Bundle("~/bundles/scripts/canvas");
        bundle.Orderer = new AsIsBundleOrderer();
        bundle
            .Include("~/Scripts/modernizr-*")
            .Include("~/Scripts/json2.js")
            .Include("~/Scripts/columnizer.js")
            .Include("~/Scripts/jquery.ui.message.min.js")
            .Include("~/Scripts/Shared/achievements.js")
            .Include("~/Scripts/Shared/canvas.js");
        bundles.Add(bundle);
    }
}

and in your view:

@Scripts.Render("~/bundles/scripts/canvas")

Solution 2 - asp.net Mvc

Thank you Darin. I've added an extension method.

internal class AsIsBundleOrderer : IBundleOrderer
{
    public virtual IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files)
    {
        return files;
    }
}

internal static class BundleExtensions
{
    public static Bundle ForceOrdered(this Bundle sb)
    {
        sb.Orderer = new AsIsBundleOrderer();
        return sb;
    }
}

Usage

bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                    "~/Scripts/jquery-{version}.js",
                    "~/Scripts/jquery-migrate-{version}.js",

                    "~/Scripts/jquery.validate.js",
                    "~/Scripts/jquery.validate.messages_fr.js",
                    "~/Scripts/moon.jquery.validation-{version}.js",

                    "~/Scripts/jquery-ui-{version}.js"
                    ).ForceOrdered());

Solution 3 - asp.net Mvc

Updated the answer provided by SoftLion to handle changes in MVC 5 (BundleFile vs FileInfo).

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

internal static class BundleExtensions
{
    public static Bundle ForceOrdered(this Bundle sb)
    {
        sb.Orderer = new AsIsBundleOrderer();
        return sb;
    }
}

Usage:

    bundles.Add(new ScriptBundle("~/content/js/site")
        .Include("~/content/scripts/jquery-{version}.js")
        .Include("~/content/scripts/bootstrap-{version}.js")
        .Include("~/content/scripts/jquery.validate-{version}")
        .ForceOrdered());

I like using fluent syntax but it also works with a single method call and all the scripts passed as parameters.

Solution 4 - asp.net Mvc

I'm not seeing this behavior on the RTM bits, are you using the Microsoft ASP.NET Web Optimization Framework 1.0.0 bits: http://nuget.org/packages/Microsoft.AspNet.Web.Optimization ?

I used a similar repro to your sample, based off of a new MVC4 Internet application website.

I added to BundleConfig.RegisterBundles:

        Bundle canvasScripts =
            new ScriptBundle("~/bundles/scripts/canvas")
                .Include("~/Scripts/modernizr-*")
                .Include("~/Scripts/Shared/achievements.js")
                .Include("~/Scripts/Shared/canvas.js");
        bundles.Add(canvasScripts); 

And then in the default index page, I added:

<script src="@Scripts.Url("~/bundles/scripts/canvas")"></script>

And I verified that in the minified javascript for the bundle, the contents of achievements.js was after modernizr...

Solution 5 - asp.net Mvc

You should be able to set the order with help of the BundleCollection.FileSetOrderList. Have a look at this blog post: http://weblogs.asp.net/imranbaloch/archive/2012/09/30/hidden-options-of-asp-net-bundling-and-minification.aspx . The code in your instance would be something like:

BundleFileSetOrdering bundleFileSetOrdering = new BundleFileSetOrdering("js");
bundleFileSetOrdering.Files.Add("~/Scripts/modernizr-*");
bundleFileSetOrdering.Files.Add("~/Scripts/json2.js");
bundleFileSetOrdering.Files.Add("~/Scripts/columnizer.js");
bundleFileSetOrdering.Files.Add("~/Scripts/jquery.ui.message.min.js");
bundleFileSetOrdering.Files.Add("~/Scripts/Shared/achievements.js");
bundleFileSetOrdering.Files.Add("~/Scripts/Shared/canvas.js");
bundles.FileSetOrderList.Add(bundleFileSetOrdering);

Solution 6 - asp.net Mvc

@Darin Dimitrov's answer works perfectly for me, but my project is written in VB, so here's his answer converted to VB

Public Class AsIsBundleOrderer
	Implements IBundleOrderer

	Public Function IBundleOrderer_OrderFiles(ByVal context As BundleContext, ByVal files As IEnumerable(Of FileInfo)) As IEnumerable(Of FileInfo) Implements IBundleOrderer.OrderFiles
		Return files
	End Function
End Class

To Use it:

Dim scriptBundleMain = New ScriptBundle("~/Scripts/myBundle")
scriptBundleMain.Orderer = New AsIsBundleOrderer()
scriptBundleMain.Include(
	"~/Scripts/file1.js",
	"~/Scripts/file2.js"
)
bundles.Add(scriptBundleMain)

Solution 7 - asp.net Mvc

You should consider using Cassette http://getcassette.net/ It supports auto detection of script dependencies based on script references found inside each file.

If you prefer sticking to MS Web Optimization solution but like the idea of arranging scripts based on script references you should read my blog post at http://blogs.microsoft.co.il/oric/2013/12/27/building-single-page-application-bundle-orderer/

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionjrummellView Question on Stackoverflow
Solution 1 - asp.net MvcDarin DimitrovView Answer on Stackoverflow
Solution 2 - asp.net MvcSoftlionView Answer on Stackoverflow
Solution 3 - asp.net MvcGerald DavisView Answer on Stackoverflow
Solution 4 - asp.net MvcHao KungView Answer on Stackoverflow
Solution 5 - asp.net MvcArne H. BitubekkView Answer on Stackoverflow
Solution 6 - asp.net MvcFiniteLooperView Answer on Stackoverflow
Solution 7 - asp.net MvcOri CalvoView Answer on Stackoverflow