Tuesday, August 13, 2013

Control Minification per Request with Web Optimizations

The Microsoft ASP.NET Web Optimization Framework is a great bundling and minification solution for your web applications. Simply grab the Microsoft.AspNet.Web.Optimization NuGet package, register your bundles, render them with a single line of code, and your environment will automatically resolve your dependencies based on whether or not the web server is running in debug mode.

But how can you debug minified styles and scripts in production?

Normally that is a difficult proposition, but here is a simple solution: JUST DON'T MINIFY THEM! With the little code snippets below you can add a simple query string parameter to disable minification for specific sessions or requests.

Adding this functionality to your website is extremely easy and requires no additional dependencies. Web Optimizations already has an internal AssetManager class that supports this functionality, we just need to access it via reflection.

Simply apply the following two steps and you will be ready to debug in production:

  1. Create the HtmlHelperExtensions class with the code below.
  2. Add a call to TrySetOptimizationEnabled inside of your ViewStart.

_ViewStart.cshtml

@using System.Web.Optimization
@{
    Layout = "~/Views/Shared/_Layout.cshtml";
    Html.TrySetOptimizationEnabled();
}

HtmlHelperExtensions.cs

public static class HtmlHelperExtensions
{
    public const string Key = "OptimizationEnabled";
 
    public static bool TrySetOptimizationEnabled(this HtmlHelper html)
    {
        var queryString = html.ViewContext.HttpContext.Request.QueryString;
        var session = html.ViewContext.HttpContext.Session;
 
        // Check the query string first, then the session.
        return TryQueryString(queryString, session) || TrySession(session);
    }
 
    private static bool TryQueryString(
        NameValueCollection queryString, 
        HttpSessionStateBase session)
    {
        // Does the query string contain the key?
        if (queryString.AllKeys.Contains(
            Key, 
            StringComparer.InvariantCultureIgnoreCase))
        {
            // Is the value a boolean?
            bool boolValue;
            var stringValue = queryString[Key];
            if (bool.TryParse(stringValue, out boolValue))
            {
                // Set the OptimizationEnabled flag
                // and then store that value in session.
                SetOptimizationEnabled(boolValue);
                session[Key] = boolValue;
                return true;
            }
        }
 
        return false;
    }
 
    private static bool TrySession(HttpSessionStateBase session)
    {
        if (session != null)
        {
            var value = session[Key] as bool?;
            if (value.HasValue)
            {
                // Use the session value to set the OptimizationEnabled flag.
                SetOptimizationEnabled(value.Value);
                return true;
            }
        }
 
        return false;
    }
 
    private static void SetOptimizationEnabled(bool value)
    {
        // Use reflection to set the internal AssetManager.OptimizationEnabled
        // flag for this request specific.
        var instance = ManagerProperty.GetValue(null, null);
        OptimizationEnabledProperty.SetValue(instance, value);
    }
 
    private static readonly PropertyInfo ManagerProperty = typeof(Scripts)
        .GetProperty("Manager", BindingFlags.Static | BindingFlags.NonPublic);
 
    private static readonly PropertyInfo OptimizationEnabledProperty = Assembly
        .GetAssembly(typeof(Scripts))
        .GetType("System.Web.Optimization.AssetManager")
        .GetProperty(
            "OptimizationEnabled",
            BindingFlags.Instance | BindingFlags.NonPublic);
}
Shout it

Enjoy,
Tom

2 comments:

  1. Great post!
    But i'm getting an exception when calling TrySetOptimizationEnabled,
    because my session is null.

    Any Idea what it might be?

    thnx.

    ReplyDelete
  2. The AssetManager.OptimizationEnabled property does not seem to be dependent on Session, so I am not sure why it would throw inside of there. However, if you are saying it is throwing anywhere else I would suggest just adding null checks or removing the session code all together; my use of session is just for convenience.

    FYI, here is the OptimizationEnabled code that I pull from JetBrains dotPeek:

    internal bool OptimizationEnabled
    {
    get
    {
    if (this._optimizationEnabled.HasValue)
    return this._optimizationEnabled.Value;
    else
    return BundleTable.EnableOptimizations;
    }
    set
    {
    this._optimizationEnabled = new bool?(value);
    }
    }

    ReplyDelete

Real Time Web Analytics