Sunday, December 7, 2008

Creating Custom HTTP Handlers

Hi all, 

I am now at the stage in my project where I need to find ways of fine tune the loading times. Of course I have used tools like Firbug etc to run diagnosis on what is taking a long time to load etc. One thing I did find, was that a lot of the time I was using .ASPX files to load something that really only need a method call, and not all the overhead that comes with an ASPX page. So what else to use, but a custom HTTP Handler!

I decided that I would use a ASHX file to load my CSS into the pag
e instead of just adding a reference within the ASPX page and making .net do all the work. The payoff, was a load time of the CSS that was about half!!!!

Here is how you do it.

Create a Generic Handler file in your Website or Web Application.

Call the file "CSSHttpHandler"



You will be presented with the following code :


By default you only have to implement one method and a property when implementing the IHttpHandler interface

public void ProcessRequest (HttpContext context) {
        context.Response.ContentType = "text/plain";
        context.Response.Write("Hello World");
    }
 
    public bool IsReusable {
        get {
            return false;
        }
    }

Replace the code with this :

using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.IO.Compression;

public class CSSHttpHandler : IHttpHandler {

    private const string PrefixAllImagesWith = "~/";
    private readonly static TimeSpan KeepInCache = TimeSpan.FromDays(15);
    private readonly static Regex UrlCheck = new Regex(@"http(s)?://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)?", RegexOptions.Compiled);

    public void ProcessRequest(HttpContext context)
    {
        context.Response.ContentType = "text/css";
        var themeName = context.Request["theme"];
        var themeFileNames = context.Request["files"];
        var version = context.Request["version"];

        var isCompressed = CanGZip(context.Request);

        var encoding = new UTF8Encoding(false);

        if (WriteFromCache(context, themeName, version, isCompressed)) return;
        using (var memoryStream = new MemoryStream(5000))
        {
            using (var writer = isCompressed ? (Stream)(new GZipStream(memoryStream, CompressionMode.Compress)) : memoryStream)
            {
                if (!string.IsNullOrEmpty(themeName))
                {
                    var themeCssNames = themeFileNames.Split(',');
                    foreach (var fileName in themeCssNames)
                    {
                        var fileBytes = GetCss(context,
                                                  "~/App_Themes/" + themeName + "/" + fileName,
                                                  PrefixAllImagesWith, version, encoding);
                        writer.Write(fileBytes, 0, fileBytes.Length);
                    }
                }

                writer.Close();
            }

            var responseBytes = memoryStream.ToArray();
            context.Cache.Insert(GetCacheKey(themeName, version, isCompressed),
                                 responseBytes, null, System.Web.Caching.Cache.NoAbsoluteExpiration,
                                 KeepInCache);

            WriteBytes(responseBytes, context, isCompressed);
        }
    }

    private static bool WriteFromCache(HttpContext context, string themeName,
        string version, bool isCompressed)
    {
        var responseBytes = context.Cache[GetCacheKey(themeName,
            version, isCompressed)] as byte[];

        if (null == responseBytes) return false;

        WriteBytes(responseBytes, context, isCompressed);
        return true;
    }

    private static void WriteBytes(byte[] bytes, HttpContext context, bool isCompressed)
    {
        var response = context.Response;

        response.AppendHeader("Content-Length", bytes.Length.ToString());
        response.ContentType = "text/css";
        if (isCompressed)
            response.AppendHeader("Content-Encoding", "gzip");

        context.Response.Cache.SetCacheability(HttpCacheability.Public);
        context.Response.Cache.SetExpires(DateTime.Now.Add(KeepInCache));
        context.Response.Cache.SetMaxAge(KeepInCache);
        context.Response.Cache.AppendCacheExtension("must-revalidate, proxy-revalidate");

        response.OutputStream.Write(bytes, 0, bytes.Length);
        response.Flush();
    }

    private static bool CanGZip(HttpRequest request)
    {
        var acceptEncoding = request.Headers["Accept-Encoding"];
        return !string.IsNullOrEmpty(acceptEncoding) &&
               (acceptEncoding.Contains("gzip") || acceptEncoding.Contains("deflate"));
    }

    private static byte[] GetCss(HttpContext context, string virtualPath,
        string imagePrefix, string version, Encoding encoding)
    {
        var physicalPath = context.Server.MapPath(virtualPath);
        var fileContent = File.ReadAllText(physicalPath, encoding);

        var imagePrefixUrl = imagePrefix +
            VirtualPathUtility.GetDirectory(virtualPath).TrimStart('~').TrimStart('/');
        var cssContent = UrlCheck.Replace(fileContent,
            new MatchEvaluator(delegate(Match m)
            {
                var imgPath = m.Groups["path"].Value.TrimStart('\'').TrimEnd('\'').TrimStart('"').TrimEnd('"');

                if (!imgPath.StartsWith("http://"))
                {
                    return "url('" + imagePrefixUrl 
                        + imgPath 
                        + (imgPath.IndexOf('?') > 0 ? "&version=" + version : "?version=" + version)
                        + "')";
                }
                else
                {
                    return "url('" + imgPath + "')";
                }
            }));

        return encoding.GetBytes(cssContent);
    }

    private static string GetCacheKey(string themeName, string version, bool isCompressed)
    {
        return "CssHttpHandler." + themeName + "." + version + "." + isCompressed;
    }

    public bool IsReusable
    {
        get
        {
            return true;
        }
    }
}

Then in the Render Method of the Page you are loading e.g. Default.aspx simply use this code

 var themeName = Page.Theme;
        if (string.IsNullOrEmpty(themeName)) return;
        var linksToRemove = new List();
        foreach (Control c in Page.Header.Controls)
            if (c is HtmlLink)
                if ((c as HtmlLink).Href.Contains("App_Themes/" + themeName))
                    linksToRemove.Add(c as HtmlLink);

        string themeCssNames = "";
        linksToRemove.ForEach(delegate(HtmlLink link)
                                  {
                                      Page.Header.Controls.Remove(link);
                                      themeCssNames += VirtualPathUtility.GetFileName(link.Href) + ',';
                                  });

        var linkTag = new Literal();

        var cssPath = CSS_PREFIX + "CssHttpHandler.ashx?theme=" + themeName
                      + "&files=" + HttpUtility.UrlEncode(themeCssNames.TrimEnd(','))
                      + "&version=" + CSS_VERSION;

        linkTag.Text = string.Format(@"&ltlink href=""{0}"" type=""text/css"" rel=""stylesheet"" /%gt", cssPath);
        Page.Header.Controls.Add(linkTag);

Hope this Helps, 

 - Tim

No comments: