Try our conversational search powered by Generative AI!

Dynamic bundling in EpiServer

Vote:
 

Hello, 

I have tried to implement something like dynamic bundling. The idea is that on one page I might have different blocks and each of them to require different script files. I want to combine them in one bundle, which is page specific. What I have implemented: 

public static void AddScriptFileToPageBundle(this HtmlHelper htmlHelper, string scriptUrl)
{
      AddFileToPageBundle(BundleType.Script, scriptUrl);
}
private static void AddFileToPageBundle(BundleType bundleType, string fileUrl)
        {
            string templateName = bundleType == BundleType.Script ? ScriptsPageBundleTemplate : StylesCssPageBundleTemplate;
            string bundleVirtualPath = String.Format(templateName, HttpContext.Current.Request.Path);

            // Remove trailing slash if exists
            if (bundleVirtualPath.EndsWith("/"))
                bundleVirtualPath = bundleVirtualPath.Substring(0, bundleVirtualPath.Length - 1);

            var bundle = BundleTable.Bundles.GetBundleFor(bundleVirtualPath);
            if (bundle != null)
            {
                bundle.Include(fileUrl);
            }
            else
            {
                // File URL must start with ~, otherwise is not found
                if (!fileUrl.StartsWith("~"))
                    fileUrl = "~" + fileUrl;

                if (bundleType == BundleType.Script)
                {
                    var pageBundle = new CustomScriptBundle(bundleVirtualPath);
                    pageBundle.Include(fileUrl);
                    pageBundle.Transforms.Add(new ScriptTransformer(new MicrosoftAjaxJsMinifier()));
                    pageBundle.Orderer = new NullOrderer();
                    BundleTable.Bundles.Add(pageBundle);
                }
                else 
                {
                    var pageBundle = new CustomStyleBundle(bundleVirtualPath);
                    pageBundle.Include(fileUrl);                    
                    pageBundle.Transforms.Add(new StyleTransformer(new MicrosoftAjaxCssMinifier()));
                    pageBundle.Orderer = new NullOrderer();
                    BundleTable.Bundles.Add(pageBundle);
                }
            }
        }

Then in everyblock, which requires custom scripts, I have the following

@{
    Html.AddScriptFileToPageBundle("~/Static/Scripts/Blocks/CookieWarningBlock/CookieWarningBlock.js");
}

I combine all page specific scripts. Then, I have another method: 

public static MvcHtmlString RenderScriptsPageBundle(this HtmlHelper htmlHelper)
        {
            return RenderPageBundle(BundleType.Script);
        }
private static MvcHtmlString RenderPageBundle(BundleType bundleType)
        {
            string templateName = bundleType == BundleType.Script ? ScriptsPageBundleTemplate : StylesCssPageBundleTemplate;
            string bundleVirtualPath = String.Format(templateName, HttpContext.Current.Request.Path);

            // Remove trailing slash if exists
            if (bundleVirtualPath.EndsWith("/"))
                bundleVirtualPath = bundleVirtualPath.Substring(0, bundleVirtualPath.Length - 1);

            var bundle = BundleTable.Bundles.GetBundleFor(bundleVirtualPath);
            if (bundle != null)
            {
                string bundleUrl = BundleTable.Bundles.ResolveBundleUrl(bundleVirtualPath, true);
                string result = bundleType == BundleType.Script ?
                    String.Format("", bundleUrl) :
                    String.Format("", bundleUrl);

                return MvcHtmlString.Create(result);
            }

            return MvcHtmlString.Empty;
        } 

and in the _Root.cshtml I invoke it

@Html.RenderScriptsPageBundle()

At the end on every page I have the following result 

Homepage: 

Our-Products:

... and so on for the rest of the pages. As you can see, I am using the page request path as a unique bundle identifier. It all worked well in our dev & test environments. However, when we deployed to production I started getting the following error:

System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
   at System.Collections.Generic.List`1.Enumerator.MoveNextRare()
   at System.Web.Optimization.Bundle.EnumerateFiles(BundleContext context)
   at System.Web.Optimization.Bundle.GenerateBundleResponse(BundleContext context)
   at System.Web.Optimization.Bundle.GetBundleUrl(BundleContext context, Boolean includeContentHash)
   at MyCompany.Web.Core.Business.Extensions.HtmlHelperExtensions.RenderPageBundle(BundleType bundleType) in c:\1250\489\Sources\WEB\Main\Web_dev\MyCompany.Web.Main.Site\MyCompany.Web.Core\Business\Extensions\HtmlHelperExtensions.cs:line 189
   at ASP._Page_Views_Shared_Layouts__Root_cshtml.Execute() in e:\Sites\CompanyWebsite\Views\Shared\Layouts\_Root.cshtml:line 35

Another weird thing: this is reproducible only if I copy and paste the link (for example the homepage) in a new browser tab and it happens randomly. If have the site already opened, and I click on the logo or other link pointing to the homepage, it is not reproducible.  

I would appreaciate any help on this. Thank you in advance.

#169211
Edited, Oct 25, 2016 17:37
Vote:
 

Sounds like a threading issue where one thread is trying to loop through the bundle with a for each and the other one is trying to add items to it. Solution is to add locks both on writing and reading that collection in that case. Or set it up once and for all in application start where there is only one thread and cache the result...

Or use variables that are unique per request. Haven't checked but I guess that Bundles collection above is a static/cached collection that is shared between all incoming requests. Modifying it in one will affect all others resulting in random errors depending where in execution they are.

#171874
Edited, Nov 20, 2016 11:09
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.