This project has moved. For the latest updates, please go here.

Feature Request: Option to Disable Minification per Request

Nov 5, 2013 at 4:03 AM
When using the native WebOptimizations I found a way to disable minification of resources on a per request basis via a query string parameter. I have found this feature to be very useful when trying to debug a QA or (occasionally) even a Production environment. My solution was a simple little hack to set the AssetManager.OptimizationEnabled flag per request:

http://www.tomdupont.net/2013/08/disable-minification-per-request-with-web-optimizations.html

I was hoping that this, or something similar, could be added to Bundle Transformer.

From poking around in the code, it looks the simplest way to accomplish this would be to use my same little hack, and then update the CssTransformer.Transform and JsTransformer.Transform methods to check BundleTransformerContext.Current.IsDebugMode instead of applicationInfo.IsDebugMode

However, I am assuming that there is a probably a better way to achieve this feature...thoughts?

Thanks,
Tom
Coordinator
Nov 5, 2013 at 3:42 PM
Hello, Tom!

I think that such feature should not be included in the Bundle Transformer, because it created a bit for other purposes. But, nevertheless, in the next release I'll make changes in architecture of the Bundle Transformer, so that it could switch between release and debug mode (need to completely rewrite a unit tests).

And you don't try instead of HTML helper write an HTTP module?
Coordinator
Nov 7, 2013 at 5:44 PM
Now in the Bundle Transformer 1.8.11 I am using the BundleTransformerContext.Current.IsDebugMode property.
Nov 12, 2013 at 12:35 PM
Hello Taritsyn,

I don't understand your previous comment.
With changes you have made to Bundle transformer,
is it now possible to control bundling and minification per request (without affecting other requests)?
Coordinator
Nov 12, 2013 at 12:44 PM
Hello, Vukoje!

This feature is not implemented in the Bundle Transformer. I just wrote to Tom, that fixed a bug.
Nov 12, 2013 at 1:17 PM
Hello Taritsyn,

I still don't get you.
This is a feature that you don't want to do and you say that you fixed a bug?

I would like an explanation why do you think that disabling optimization per request is not useful feature?
I see this as a crucial requirement for using optimization in production - possibility to still debug application.

Also this seams trivial to support as Tom have already implemented it with reflection.
Coordinator
Nov 12, 2013 at 1:20 PM
Discussion is closed!
Nov 12, 2013 at 1:45 PM
Hahahaha
Coordinator
Nov 12, 2013 at 1:48 PM
Edited Nov 12, 2013 at 1:50 PM
Nov 12, 2013 at 1:56 PM
Yea, I am going to need it relying on your code...
Nov 12, 2013 at 4:38 PM
All,
I will be getting latest and playing with this tonight.

Taritsyn,
Thank you so much for the fast turn around!

Vukoje,
As soon as I have the per request minification working again I will write a blog post about it.

Thanks,
Tom
Nov 13, 2013 at 6:45 AM
Taritsyn,

I see how the IsDebug flag is now passed into the TransformerBase.Process method, but because it is referencing the static context, and that is using the static BundleTable, I am not sure how I can manipulate it. Is there a way that I could control the BundleTransformerContext.Current? Perhaps by unsealing it and allowing a custom resolver?

My current trick (setting the AssetManager.OptimizationEnabled flag) is resulting in returning ALL of the imported less files...it is very odd behavior, and I am not certain what is causing it.

Any suggestions would be appreciated!

Thanks,
Tom
Coordinator
Nov 13, 2013 at 3:40 PM
Hello, Tom!

As a solution I propose to write an HTTP module:
namespace BundleTransformer.Example.Mvc.Infrastructure.HttpModules
{
    using System;
    using System.Linq;
    using System.Reflection;
    using System.Web;
    using System.Web.Optimization;

    public sealed class CustomBundleModule : BundleModule, IHttpModule
    {
        public const string OPTIMIZATION_ENABLED_KEY = "OptimizationEnabled";

        private static readonly MethodInfo _invalidateCacheEntriesMethodInfo =
            typeof(Bundle).GetMethod("InvalidateCacheEntries", BindingFlags.Instance | BindingFlags.NonPublic);


        protected override void Init(HttpApplication application)
        {
            application.BeginRequest += ProcessRequest;

            base.Init(application);
        }

        private void ProcessRequest(object sender, EventArgs e)
        {
            HttpContext context = ((HttpApplication)sender).Context;
            var queryString = context.Request.QueryString;

            // Does the query string contain the key?
            if (queryString.AllKeys.Contains(OPTIMIZATION_ENABLED_KEY, StringComparer.InvariantCultureIgnoreCase))
            {
                // Is the value a boolean?
                bool boolValue;
                var stringValue = queryString[OPTIMIZATION_ENABLED_KEY];

                if (bool.TryParse(stringValue, out boolValue))
                {
                    // Set the OptimizationEnabled flag
                    SetOptimizationEnabled(boolValue);
                }
            }
        }

        private static void SetOptimizationEnabled(bool value)
        {
            if (value != BundleTable.EnableOptimizations)
            {
                BundleTable.EnableOptimizations = value;

                foreach (Bundle bundle in BundleTable.Bundles)
                {
                    _invalidateCacheEntriesMethodInfo.Invoke(bundle, new object[0]);
                }
            }
            
        }

        void IHttpModule.Dispose()
        {
            Dispose();
        }

        void IHttpModule.Init(HttpApplication application)
        {
            Init(application);
        }
    }
}
And then register it in the Web.config file:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  …
  <system.webServer>
    …
    <modules runAllManagedModulesForAllRequests="true">
      <remove name="BundleModule" />
      <add name="BundleModule"
        type="BundleTransformer.Example.Mvc.Infrastructure.HttpModules.CustomBundleModule, BundleTransformer.Example.Mvc"
        preCondition="" />
        …
    </modules>
    …
  </system.webServer>
  …
</configuration>
Nov 13, 2013 at 5:51 PM
I understand and agree with the want to put this functionality into an HTTP module, it is definitely a more robust solution than the HtmlHelper extension. However I believe that this solution would disable minification for the whole site, where as I was wanting to disable minification per request/user.

I was hoping that there would be a way to update the BundleTransformerContext.Current.IsDebugEnable, and make that property context sensitive so that it could return different values for different users. Unfortunately right now I can not identify a way to extend or customize that value, as those classes are sealed and they are relying on the global BundleTable property.

Here is a super simple example of what I had in mind:
/// <summary>
/// Bundle transformer context
/// </summary>
public sealed class BundleTransformerContext
{
    /// <summary>
    /// Instance of bundle transformer context
    /// </summary>
    private static readonly Lazy<BundleTransformerContext> _instance =
        new Lazy<BundleTransformerContext>(() => new BundleTransformerContext());
    
    /// <summary>
    /// Gets a instance of bundle transformer context
    /// </summary>
    public static BundleTransformerContext Current
    {
        get { return _instance.Value; }
    }

    /// <summary>
    /// Gets a flag that web application is in debug mode
    /// </summary>
    public bool IsDebugMode
    {
        get
        {
            return _isDebugModeHandler == null
                ? !BundleTable.EnableOptimizations
                : handler();
        }
    }

    /// <summary>
    /// Private constructor for implementation Singleton pattern
    /// </summary>
    private BundleTransformerContext()
    { }

    /// <summary>
    /// The override to resolve IsDebugMode.
    /// </summary>
    private Func<bool> _isDebugModeHandler;

    /// <summary>
    /// Set an override to resolve IsDebugMode.
    /// </summary>
    public void SetIsDebugModeHandler(Func<bool> handler)
    {
        _isDebugModeHandler = handler;
    }
Thoughts?

Thanks again,
Tom
Coordinator
Nov 14, 2013 at 6:11 AM
Edited Nov 14, 2013 at 6:11 AM
Hello, Tom!

Feature that you propose it is impossible to implement. Microsoft ASP.NET Web Optimization Framework a apply transformations to bundles only in 3 cases:
  1. On first request to bundle
  2. After expiration of the bundle cache entry
  3. After restarting of web application
Therefore it is useless to change any properties of the Bundle Transformer, if contents of bundle is located in server-side cache.

Regarding @imports (as well as other types of file dependencies): currently the Microsoft ASP.NET Web Optimization Framework do not gives us opportunities to directly manage the cache dependencies of bundle, and therefore we can only add @imports to the BundleResponse.Files property.
Nov 16, 2013 at 3:45 PM
Thanks again Taritsyn.
I will have to experiment with some other ideas and get back to you. :)

Tom
Nov 16, 2013 at 9:20 PM
New Idea:
I can create TWO bundles, one for debugging and one for release; then when I go to render the style/scripts, I can modify their paths based on whether or not minification is desired.

The Problem:
Current the BundleTransformerContext.IsDebugMode is returning the BundleTable.OptimizationEnabled value, where as what I really need to modify is the HttpContext.IsDebuggingEnabled.

Could the BundleTransformerContext.IsDebugMode be changed to reflect the value of HttpContext.IsDebuggingEnabled, OR could a second boolean parameter be added to TransformerBase.Process?

Below is the code I have come up with for now.
Please let me know if you have any questions or suggestions!

Thanks again,
Tom

CSHTML
@*@Styles.Render(BundleConfig.BootstrapPath)
@Scripts.Render(BundleConfig.ScriptsPath)*@

@Html.RenderDebuggableStyles(BundleConfig.BootstrapPath)
@Html.RenderDebuggableScripts(BundleConfig.ScriptsPath)
C#
public static class BundleConfig
{
    public const string BootstrapPath = "~/Bundles/Bootstrap";
    public const string ScriptsPath = "~/Bundles/Scripts";

    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.AddDebuggable(
            BootstrapPath,
            n => new CustomStyleBundle(n) {Orderer = new NullOrderer()},
            b =>
            {
                b.Include("~/Content/bootstrap/bootstrap.less");
                b.Include("~/Content/calendar.less");
            });

        bundles.AddDebuggable(
            ScriptsPath,
            n => new CustomScriptBundle(n) { Orderer = new NullOrderer() },
            b =>
            {
                b.Include("~/scripts/jquery-2.0.3.js");
                b.Include("~/scripts/mobile/jquery.mobile.ns.js");
                b.Include("~/scripts/mobile/jquery.mobile.vmouse.js");
                b.Include("~/scripts/mobile/jquery.mobile.support.touch.js");
                b.Include("~/scripts/mobile/touch.js");
                b.Include("~/scripts/calendar.js");
            });
    }
}

public static class DebuggableBundleExtensions
{
    public const string Key = "OptimizationEnabled";
    public const string DebugSuffix = "_debug";

    public static void AddDebuggable<T>(this BundleCollection bundles, string path, Func<string, T> create, Action<T> update)
        where T : Bundle
    {
        var release = create(path);
        update(release);
        bundles.AddWrapper(release, false);

        var debug = create(path + DebugSuffix);
        update(debug);
        bundles.AddWrapper(debug, true);
    }

    private static void AddWrapper(this BundleCollection bundles, Bundle bundle, bool isDebug)
    {
        var bases = bundle.Transforms
            .OfType<TransformerBase>()
            .ToArray();
            
        foreach (var b in bases)
        {
            var i = bundle.Transforms.IndexOf(b);
            bundle.Transforms[i] = new TransformerWrapper(b, isDebug);
        }

        bundles.Add(bundle);
    }

    private class TransformerWrapper : IBundleTransform
    {
        private readonly TransformerBase _transformerBase;
        private readonly bool _isDebug;

        public TransformerWrapper(TransformerBase transformerBase, bool isDebug)
        {
            _transformerBase = transformerBase;
            _isDebug = isDebug;
        }

        public void Process(BundleContext context, BundleResponse response)
        {
            _transformerBase.Process(context, response, _isDebug);
        }
    }
    
    public static IHtmlString RenderDebuggableStyles(this HtmlHelper html, params string[] paths)
    {
        var currentPaths = html.GetCurrentPaths(paths);
        return Styles.Render(currentPaths);
    }
        
    public static IHtmlString RenderDebuggableScripts(this HtmlHelper html, params string[] paths)
    {
        var currentPaths = html.GetCurrentPaths(paths);
        return Scripts.Render(currentPaths);
    }
        
    private static string[] GetCurrentPaths(this HtmlHelper html, string[] paths)
    {
        var queryStringValue = html.ViewContext.HttpContext.Request.QueryString[Key];
        var isDebug = html.ViewContext.HttpContext.IsDebuggingEnabled;

        if (!String.IsNullOrWhiteSpace(queryStringValue))
        {
            bool b;
            if (Boolean.TryParse(queryStringValue, out b))
                isDebug = b;
        }

        if (isDebug)
            paths = paths
                .Select(p => p + DebugSuffix)
                .ToArray();

        return paths;
    }
}
Coordinator
Nov 17, 2013 at 6:47 AM
Hello, Tom!

This is a bad idea, because your solution is based on hacks. In addition, the debug bundle should not contain any transformations. Debug bundle must be created on basis of the Bundle class or completely clear the Transforms property.

I think, that the Microsoft ASP.NET Web Optimization Framework and Bundle Transformer should be used for its intended purpose. I can't afford to change the architecture of Bundle Transformer for each such hack.

It's time to close this discussion.
Nov 17, 2013 at 7:02 AM
Fair enough, thanks again for responses and updates!

Tom