Tuesday, January 25, 2011

ASP.Net MVC - How to switch javascript links between Debug and Release builds using Razor

I use JQuery and numerous associated plugins in virtually every site I develop now days. Depending on what I am doing I want to be using a specific version of a file, for instance when debugging I often want to be using jquery.js so I can debug if required, while in the production version of the code I want to use the CDN to make use of caching on the browser. Currently there is no easy way to do this, so I sat down at the keyboard to see what I could come up with.

My requirements were:
  • it had to be very easy to use in the view page, much like the current URL.Content() method
  • it had to be easy to set up the paths for the files, no fiddling around with lots of strings in  the web.config for this
  • it had to be easy to switch between debug and release versions of the strings
  • it had to perform well as it could be called a number of times
The entire code for the solution is included at the end of this post, to use it you need to do the following:

1. Set up the paths to your javascript and css files in your global.asx file as the following example shows:
protected void Application_Start()
{
   UrlHelperExt.Instance().AddUrls(
                new FileUrl("JQuery", UrlMode.Debug, "~/Scripts/jquery-1.4.4.js"),
                new FileUrl("JQuery", UrlMode.Release, "https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js")
                );
        }
The UrlHelperExt class makes use of the singleton pattern and is instantiated by the Instance() method. The AddUrls() method takes an array of FileUrl classes. The FileUrl class constructor takes the following parameters:
  • FileKey: this is the unique identifier for the script and what you will use to reference it in the view page
  • UrlMode: This is an enum which determines which Url is used depending on the UrlMode you configure in your web.config file.
  • UrlPath: This is the actual path to the script file. You can pass in a path using the ~ notation and it will be resolved to the correct path at runtime using the current HTTPRequest.
2. Use a UrlHelper extension method in your view pages as follows:
    <script src="@Url.ContentPath("JQuery")" type="text/javascript"></script>

3. Switch between the Urls that are used in your pages by adding the following to your web.config file:

<appSettings>
    <add key="URLMode" value="Release"/>
  </appSettings>
The URLMode key can be set to Release/Staging/Debug. If there is no key or it cannot be parsed correctly it will use the 'debug' mode strings.

So where are these strings cached? Well I decided that I would make use of HTTPApplication to store the UrlHelperExt class. This means that there is only one stored in memory independent of how many sessions you may have running. Any thread contention issues are taken care of by making sure that the dictionary that stores the FileUrl objects is locked each time a read takes place.

The entire code for the classes and extension methods is as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Web.Mvc;
using System.Web;

namespace DWC.MVC.Common
{
    public enum UrlMode { Debug, Staging, Release };
    
    public static class UrlHelperExtensions
    {
        public static string ContentPath(this UrlHelper helper, string urlKey)
        {
            return UrlHelperExt.Instance().GetURL(urlKey, helper);
        }
    }


    public class FileUrl
    {
        public string UrlPath { get; set; }
        public string UrlKey { get; set; }
        public UrlMode UrlMode { get; set; }


        public FileUrl(string urlKey, UrlMode mode, string urlPath)
        {
            this.UrlKey = urlKey;
            this.UrlPath = urlPath;
            this.UrlMode = mode;
        }

        public string GetKey()
        {
            return GetKey(this.UrlMode,this.UrlKey);
        }

        public static string GetKey(UrlMode mode, string urlKey)
        {
            return ((int)mode).ToString() + "_" + urlKey;
        }
    }

    public class UrlHelperExt
    {
        public UrlMode UrlMode { get; set; }
        Dictionary<string, FileUrl> Urls = new Dictionary<string, FileUrl>();

        object _syncLock = new object();

        public UrlHelperExt()
        {
            string configMode = ConfigurationManager.AppSettings["URLMode"];

            this.UrlMode = UrlMode.Debug;
        
            if (!String.IsNullOrEmpty(configMode))
            {
                UrlMode result;
                if (Enum.TryParse<UrlMode>(configMode, out result))
                    this.UrlMode = result;
            }
        }

        public static UrlHelperExt Instance()
        {
            if (HttpContext.Current.Application["URL_HELPER_EXT"] == null)
            {
                HttpContext.Current.Application["URL_HELPER_EXT"] = new UrlHelperExt();
            }
            return (UrlHelperExt)HttpContext.Current.Application["URL_HELPER_EXT"];
        }

        public void AddUrls(params FileUrl[] fileURLs)
        {
            foreach (var fileURL in fileURLs)
            {
                this.Urls.Add(fileURL.GetKey(), fileURL);
            }
        }


        public string GetURL(string urlKey, UrlHelper urlHelper)
        {
            FileUrl fileUrl = null;

            string key = FileUrl.GetKey(this.UrlMode,urlKey);

            //lock while we are reading from the dictionary as it is stored in appdata
            lock (this._syncLock)
            {
                fileUrl = this.Urls[key];
            }

            if (fileUrl.UrlPath.StartsWith("~"))
                return urlHelper.Content(fileUrl.UrlPath);
            else
                return fileUrl.UrlPath;
        }
    }
}
Of course this needn't be restricted to javascript files, you could also use it for your css files and if you were really keen you could even use it for things like displaying different messages to users, e.g. "This is the test website" or "This is the staging website".

Word of warning, this has not been tested in production yet, but it will be soon! I also blogged in an earlier post about how to easily add javascript and css links to web pages using Razor here.

Enjoy!

1 comment:

  1. I am a .Net developer and I have to admit that your post is very useful for me. I learn short methods for the given topics. The code used to describe it easy and feasible. I tried and tested the syntax on my application.

    ReplyDelete