conflate.ashx

2007-10-23 @ 17:38#

i've been dealing with *lots* of external resources w/ my current round of projects. several .JS files, several .CSS files. all need to be loaded for a page and that can take some time on complex pages. i've contemplated building an offline tool that combines them for production use, but that would require some versioning and mods on our production codebase that will take too much time.

instead, i whipped up an ASP.NET handler script (conflate.ashx) that does the job for me. it can sit in the root of any of our production apps as a stand alone script and does not interfere with anything. i just dropped it on our stagin server and it works like a champ! here's the full code set:

<%@ WebHandler Language="C#" Class="Conflate" %>

/************************************************************************
 * 
 * title:   conflate.ashx
 * version: 1.0 - 2007-10-23 (mca)
 * 
 * usage:   "conflate.ashx?/folder/path/file1.js,/folder/path/file2.js,..."
 *          "conflate.ashx?/folder/path/file1.css,/folder/path/file2.css,..."
 * 
 * notes:   returns a single representation which is a combination of csv list
 *          inserts "error loading ..." msg if file was not found.
 *          ignores "empty" filenames (no load attempts, no errors)
 *          stores results in asp.net cache w/ file dependencies
 *          you modify expires var to control Cache-Control/Expires headers
 *          
 *************************************************************************/ 
using System;
using System.Web;

using System.IO;
using System.Text;
using System.Web.Caching;

public class Conflate : IHttpHandler 
{
    const double expires = 60 * 60 * 24 * 30;   // 30 days
    const string cache_control_fmt = "public,max-age:{0}";
    const string expires_fmt = "{0:ddd dd MMM yyyy HH:mm:ss} GMT";
    const string load_err_fmt = "/* error loading {0} */\n";
    
    public void ProcessRequest(HttpContext ctx)
    {
        string files = (ctx.Request.Url.Query.Length > 0 ? ctx.Request.Url.Query.Substring(1) : string.Empty);
        if (files.Length > 0)
        {
            string data = LoadFiles(ctx, files.Split(','));

            ctx.Response.Write(data);
            ctx.Response.StatusCode = 200;
            ctx.Response.ContentType = (files.IndexOf(".js") != -1 ? "text/javascript" : "text/css");

            if (expires != 0)
            {
                ctx.Response.AddHeader("Cache-Control", string.Format(cache_control_fmt, expires));
                ctx.Response.AddHeader("Expires", string.Format(expires_fmt, System.DateTime.UtcNow.AddSeconds(expires)));
            }
        }
        else
        {
            ctx.Response.ContentType = "text/plain";
            ctx.Response.StatusCode = 404;
            ctx.Response.Write("\n");
        }
        ctx.Response.End();
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }

    private string LoadFiles(HttpContext ctx, string[] files)
    {
        string data = (string)ctx.Cache.Get(ctx.Request.RawUrl);
        
        if (data == null)
        {
            string cached = string.Empty;
            string[] fnames = (string[])files.Clone();
            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < files.Length; i++)
            {
                if (files[i].Trim() != string.Empty)
                {
                    files[i] = ctx.Server.MapPath(files[i]);
                    if (File.Exists(files[i]))
                    {
                        using (TextReader tr = new StreamReader(files[i]))
                        {
                            sb.AppendLine(tr.ReadToEnd());
                        }
                        cached += files[i] + ",";
                    }
                    else
                    {
                        sb.AppendFormat(load_err_fmt, fnames[i]);
                    }
                }
            }
            
            data = sb.ToString();
            
            ctx.Cache.Add(
                ctx.Request.RawUrl,
                data,
                new CacheDependency(cached.Substring(0,cached.Length-1).Split(',')),
                Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration,
                CacheItemPriority.Normal,
                null);
        }

        return data;
    }
}

code