Runtime dynamic bundling and minifying in MVC 4
Asked Answered
K

4

19

I was wondering if anybody can help me with bundling and minifying using the new optimization namespace shipped with MVC 4. I have a Multitenant-application in which I want to decide which js files should be loaded based on settings per user. One approach would be to create all bundles upfront and change the virtual path of resolvebundleurl based on the setting of the user, but that feels not really the right way. Also I have dynamic css in a cshtml view based on user-settings, which I would like to have minified in runtime.

Any suggestions? I also see a lot of reactions in other questions to check out Requestreduce, but they are all from the same user.

What would be the best approach to handle both situations?

Thanks in advance!

Karyolysis answered 16/5, 2012 at 8:11 Comment(2)
Nobody? When I change my Javascript or css during development. The minified (bundled) files get updated without a rebuild, so it must be done at runtime....Karyolysis
The question title should be changed to emphasize dynamic bundles (or per user).Chelicera
S
12

One approach you can take is building the bundle dynamically when the application starts. So if your scripts are located in ~/scripts you can do:

Bundle bundle = new Bundle("~/scripts/js", new JsMinify());

if (includeJquery == true) {     
  bundle.IncludeDirectory("~/scripts", "jquery-*");
  bundle.IncludeDirectory("~/scripts", "jquery-ui*");
} 

if (includeAwesomenes == true) {
  bundle.IncludeDirectory("~/scripts", "awesomeness.js");
}

BundleTable.Bundles.Add(bundle);

Then your markup can look like this

@Scripts.Render("~/Scripts/Libs/js")

Note: I'm using the latest nuget package for system.web.optimization (now Microsoft.AspNet.Web.Optimization) located here. Scott Hanselman has a good post about it.

Shf answered 2/6, 2012 at 4:9 Comment(1)
Your post made me gear up to Visual Studio 2012 RC and I'm converting my project as we speak. Thinks have become a lot easier now with the bundleconfig file. I'll post my final solution when it's finished.Karyolysis
V
9

i wrote a helper function to dynamic minify my css & js

    public static IHtmlString RenderStyles(this HtmlHelper helper, params string[] additionalPaths)
    {
        var page = helper.ViewDataContainer as WebPageExecutingBase;
        if (page != null && page.VirtualPath.StartsWith("~/"))
        {
            var virtualPath = "~/bundles" + page.VirtualPath.Substring(1);
            if (BundleTable.Bundles.GetBundleFor(virtualPath) == null)
            {
                var defaultPath = page.VirtualPath + ".css";
                BundleTable.Bundles.Add(new StyleBundle(virtualPath).Include(defaultPath).Include(additionalPaths));
            }
            return MvcHtmlString.Create(@"<link href=""" + HttpUtility.HtmlAttributeEncode(BundleTable.Bundles.ResolveBundleUrl(virtualPath)) + @""" rel=""stylesheet""/>");
        }
        return MvcHtmlString.Empty;
    }

    public static IHtmlString RenderScripts(this HtmlHelper helper, params string[] additionalPaths)
    {
        var page = helper.ViewDataContainer as WebPageExecutingBase;
        if (page != null && page.VirtualPath.StartsWith("~/"))
        {
            var virtualPath = "~/bundles" + page.VirtualPath.Substring(1);
            if (BundleTable.Bundles.GetBundleFor(virtualPath) == null)
            {
                var defaultPath = page.VirtualPath + ".js";
                BundleTable.Bundles.Add(new ScriptBundle(virtualPath).Include(defaultPath).Include(additionalPaths));
            }
            return MvcHtmlString.Create(@"<script src=""" + HttpUtility.HtmlAttributeEncode(BundleTable.Bundles.ResolveBundleUrl(virtualPath)) + @"""></script>");
        }
        return MvcHtmlString.Empty;
    }

usage

~/views/Home/Test1.cshtml

~/Views/Home/Test1.cshtml.css

~/Views/Home/Test1.cshtml.js

in Test1.cshtml

@model object
@{
   // init
}@{

}@section MainContent {
  {<div>@{
     if ("work" != "fun")
     {
        {<hr/>}
     }
  }</div>}
}@{

}@section Scripts {@{
  {@Html.RenderScripts()}
}@{

}@section Styles {@{
  {@Html.RenderStyles()}
}}

but ofcoz, i put most of my sripts,styles in ~/Scripts/.js, ~/Content/.css

and register them in Appp_Start

Vermination answered 16/5, 2012 at 8:11 Comment(1)
I like this solution because it allows you to dynamically create the bundles from the cshtml file. It doesnt assume your cshtml files are static and that you know the bundles you want ahead of time.Foulup
A
5

We considered supporting dynamic bundles early on, but the fundamental issue with that approach is multi server scenarios (i.e. cloud) won't work. If all bundles are not defined in advance, any bundle requests that get sent to a different server than the one that served the page request will get 404 response(as the bundle definition would only exist on server that handled the page request). As a result, I would suggest creating all bundles up front, that's the mainline scenario. Dynamic configuration of bundles might work as well, but that is not a fully supported scenario.

Aborning answered 15/6, 2012 at 23:22 Comment(4)
I'm not sure I totally understand why it's a problem, because when I change a js-file on the server and reload the page the change is in the new minified/bundled file. I now have as pointed out created all bundles upfront...Karyolysis
Even if you create all the bundles on the server up front, they don't really exist on the server until you register them in the BundleTable.Bundles collection. As a result, in a multi server scenario, if the request for the bundle goes to a different server which has not registered the bundle yet, you will get a 404.Aborning
@HaoKung: that's no different from any Http handler such as asp.net mvc's controllers: of course you need to be able to identify the correct bundle based on the http request. But just like other requests, there's no reason the handler couldn't in some cases be parameterized, or even dependent on session state if necessary.Heehaw
How would someone include a bundle from another server (consider the bundle is registered on the other server) something like <script src="@Url.Content("http://localhost/CardGame/bundles/jquery")" type="text/javascript"></script> didnt work for me. Response 500.Sochor
C
0

Update: Not sure if it matters but I am using MVC 5.2.3 and Visual Studio 2015, question is a little old.

However I made dynamic bundling that works in _viewStart.cshtml. What I did was I made a helper class that stores bundles in a dictionary of bundles. Then at app start I pull them from the dictionary and register them. And I made a static boolen "bundlesInitialzed" so that the bundles only add to the dictionary once.

Example Helper:

public static class KBApplicationCore: .....
{
    private static Dictionary<string, Bundle> _bundleDictionary = new Dictionary<string, Bundle>();
    public static bool BundlesFinalized { get { return _BundlesFinalized; } }
    /// <summary>
    /// Add a bundle to the bundle dictionary
    /// </summary>
    /// <param name="bundle"></param>
    /// <returns></returns>
    public static bool RegisterBundle(Bundle bundle)
    {
        if (bundle == null)
            throw new ArgumentNullException("bundle");
        if (_BundlesFinalized)
            throw new InvalidOperationException("The bundles have been finalized and frozen, you can only finalize the bundles once as an app pool recycle is needed to change the bundles afterwards!");
        if (_bundleDictionary.ContainsKey(bundle.Path))
            return false;
        _bundleDictionary.Add(bundle.Path, bundle);
        return true;
    }
    /// <summary>
    /// Finalize the bundles, which commits them to the BundleTable.Bundles collection, respects the web.config's debug setting for optimizations
    /// </summary>
    public static void FinalizeBundles()
    {
        FinalizeBundles(null);
    }
    /// <summary>
    /// Finalize the bundles, which commits them to the BundleTable.Bundles collection
    /// </summary>
    /// <param name="forceMinimize">Null = Respect web.config debug setting, True force minification regardless of web.config, False force no minification regardless of web.config</param>
    public static void FinalizeBundles(bool? forceMinimize)
    {
        var bundles = BundleTable.Bundles;
        foreach (var bundle in _bundleDictionary.Values)
        {
            bundles.Add(bundle);
        }
        if (forceMinimize != null)
            BundleTable.EnableOptimizations = forceMinimize.Value;
        _BundlesFinalized = true;
    }        
}

Example _ViewStart.cshtml

@{

    var bundles = BundleTable.Bundles;
    var baseUrl = string.Concat("~/App_Plugins/", KBApplicationCore.PackageManifest.FolderName, "/");
    //Maybe there is a better way to do this, the goal is to make the bundle configurable without having to recompile the code
    if (!KBApplicationCore.BundlesFinalized)
    {
        //Note, you need to reset the application pool in order for any changes here to be reloaded as the BundlesFinalized property is a static field that will only reset to false when the app restarts.
        Bundle mainScripts = new ScriptBundle("~/bundles/scripts/main.js");
        mainScripts.Include(new string[] {
            baseUrl + "Assets/lib/jquery/jquery.js",
            baseUrl + "Assets/lib/jquery/plugins/jqcloud/jqcloud.js",
            baseUrl + "Assets/lib/bootstrap/js/bootstrap.js",            
            baseUrl + "Assets/lib/bootstrap/plugins/treeview/bootstrap-treeview.js",   
            baseUrl + "Assets/lib/angular/angular.js",
            baseUrl + "Assets/lib/ckEditor/ckEditor.js"      
        });
        KBApplicationCore.RegisterBundle(mainScripts);

        Bundle appScripts = new ScriptBundle("~/bundles/scripts/app.js");
        appScripts.Include(new string[] {
            baseUrl + "Assets/app/app.js",
            baseUrl + "Assets/app/services/*.js",
            baseUrl + "Assets/app/directives/*.js",
            baseUrl + "Assets/app/controllers/*.js"
        });
        KBApplicationCore.RegisterBundle(appScripts);

        Bundle mainStyles = new StyleBundle("~/bundles/styles/main.css");
        mainStyles.Include(new string[] {
           baseUrl + "Assets/lib/bootstrap/build/less/bootstrap.less",
           baseUrl + "Assets/lib/bootstrap/plugins/treeview/bootstrap-treeview.css",   
           baseUrl + "Assets/lib/ckeditor/contents.css",
           baseUrl + "Assets/lib/font-awesome/less/font-awesome.less",
           baseUrl + "Assets/styles/tlckb.less"
        });
        mainStyles.Transforms.Add(new BundleTransformer.Core.Transformers.CssTransformer());
        mainStyles.Transforms.Add(new CssMinify());
        mainStyles.Orderer = new BundleTransformer.Core.Orderers.NullOrderer();
        KBApplicationCore.RegisterBundle(mainStyles);


        KBApplicationCore.FinalizeBundles(true); //true = Force Optimizations, false = Force non Optmizations, null = respect web.config which is the same as calling the parameterless constructor.
    }
}

Note: This should be updated to use thread locking to prevent 2 requests entering the bundle code before the first one exits.

The way this works is the view start runs on the first request to the site after an app pool reset. It calls the RegisterBundle on the helper and passes the ScriptBundle or StyleBundle to the dictionary in the order RegisterBundles is called.

When FinalizeBundles is called you can specify True which will force optimizations regardless of web.config debug setting, or leave it null or use the constructor without that parameter to have it respect the web.config setting. Passing false will force it to use no optimization even if debug is true. FinalizeBundles Registers the bundles in the bundles table and set's _BundlesFinalized to true.

Once finalized, an attempt to call RegisterBundle again will throw an exception, it's frozen at that point.

This setup allows you to add new bundles to view start and reset the app pool to get them to take effect. The original goal I had writing this was because I am making something others will use so I wanted them to be able to completely change the front end UI without having to rebuild the source to change the bundles.

Crews answered 27/8, 2015 at 0:43 Comment(2)
Hi Ryios, What is the purpose of below line KBApplicationCore.PackageManifest.FolderName. Is this line of code will return the base folder path?Cissie
Ah, misread your comment. That was code I forgot to edit out from my project when I posted here. KBApplicationCore.PackageManifest.FolderName is the name of the folder the PackageManifest is in. So yes it's used as a basepath for the assets. The app I designed has a theme system where there is a Package.Manifest file in every theme folder. The Theme folder is used as a base path for all assets. If you change the theme, it will be different, and have all different assets.Crews

© 2022 - 2024 — McMap. All rights reserved.