Tuesday, November 25, 2008

Caching of dynamic resources

It is a popular technique to store resources, such as CSS, XSLT, JavaScript and HTML files, in resources files embedded in an assembly. This approach makes solutions more self contained and makes localization easier using satellite assemblies. The processing of the resources is usually handled by a specific HTTP handler. Many times these resources are rather static and there is no need to reload them on each requests. For example a simple HTTP handler will look like this:

 

   1:  public class MyWebResourceHandler : System.Web.IHttpHandler
   2:  {
   3:        public bool IsReusable
   4:        {
   5:             get { return true; }
   6:        }
   7:        public void ProcessRequest(HttpContext context)
   8:        {
   9:             context.Response.ContentType = "contentType";
  10:             context.Response.Write("resourceContent"));
  11:             context.Response.End();
  12:        }
  13:  }

 

Recently I had to optimize the performance of such a HTTP Handler. It's purpose is to process a resource HTTP request, perform resource substitutions and other resource transformations and send the processed resource file back to the browser. Since this process was quite intense, it required regex and string operations, and the output rarely changed, it made sense to adjust the HTTP handler to analyze the cache information send by the browser and make the HTTP handler respond either with a complete response, sending the content of the resource, or with HTTP 304 - resource not changed.

To make this happen I added a check to determine whether the resources has been modified. This could be any condition you find appropriate. Based on this the handler either sends back the original content or responds with HTTP 304. To complete the response you have to specify the Content-Length HTTP header and set it to 0. To make sure that all caching information for this resource is correctly send to the client we have to specify the Cacheability, LastModified and Etag. All headers which control how the browser will request the cached resource at the next request.

 

   1:  public void ProcessRequest(HttpContext context)
   2:  {
   3:      if (IsFileModified(someParameters))
   4:      {
   5:          context.Response.ContentType = "ContentType";
   6:          context.Response.Write("resourceContent");
   7:      }
   8:      else
   9:      {
  10:          // File hasn't changed, so return HTTP 304 without retrieving the data 
  11:          context.Response.StatusCode = 304;
  12:          context.Response.StatusDescription = "Not Modified";
  13:   
  14:          // Explicitly set the Content-Length header so the client doesn't wait for
  15:          //  content but keeps the connection open for other requests 
  16:          context.Response.AddHeader("Content-Length", "0");                   
  17:      }
  18:   
  19:      // set cache info
  20:      context.Response.Cache.SetCacheability(HttpCacheability.Private);
  21:      context.Response.Cache.VaryByHeaders["If-Modified-Since"] = true;
  22:      context.Response.Cache.VaryByHeaders["If-None-Match"] = true;                  
  23:      context.Response.Cache.SetLastModified(creationDate);                
  24:      context.Response.Cache.SetETag(ETag);
  25:      context.Response.End();
  26:  }

 

Since resources are stored in an assembly, whenever the assembly changes we need to make sure that the resources get reloaded in the browser. Our file modification criteria will be the last modified date of the assembly, which contains the resources. To complete the handler we add two more methods IsFileModified and GetETag. IsFileModified checks if the ETag or the date modified have changed. GetETag generates a hash from the assembly's name and the last modified date.

The complete code of the handler can be found here: http://code.msdn.microsoft.com/ResourceCache

After applying these changes in our solution, we significantly reduced the traffic for downloading resources.

Now that we know how to deal with the cache I only need to find where the real cash is.

Dovizhdane!

 

PS: I would like to give credit to Dan Larson, who uses the same approach, but in a different context in his book Developing Service-Oriented AJAX Applications. Great book on AJAX in ASP.NET 3.5!

No comments: