Background
In the post, Front-line of Front-end, Rational Front-end Performance Tuning in ASP.NET MVC, I, I have illustrated a way to rationally increase front-end performance. However, we still have some debts remained. Let’s quickly review what we have to settle in this post.
Basically, we made a sensational improvement on HTTP Request and File Size, which was reduced by 97% and 71% separately. The score evaluated by Page Speed was raised from 73 to 81. Nevertheless, the improvement only covered JavaScript files while images still didn’t cached and CSS files were not compressed. So, we need to take care of them just like their JS buddy.
Still remember that I said there two ways to implement caching and compression? One is Filter, another is HttpHandler. The one I would like to talk about in this post is, you guessed it, HttpHandler.
Implementation
First off, I would like to present how the configuration looks like once we finish all the work. (On the other hand, I don’t would like to explore how HttpHandler works, for more detail you can probably ask Google.)
<system.web>
<httpHandlers>
<add verb="*" path="*.gif" type="Home.HttpHandler.CachingHandler, Home"/>
<add verb="*" path="*.jpg" type="Home.HttpHandler.CachingHandler, Home"/>
<add verb="*" path="*.png" type="Home.HttpHandler.CachingHandler, Home"/>
<add verb="*" path="*.js" type="Home.HttpHandler.CachingHandler, Home"/>
<add verb="*" path="*.css" type="Home.HttpHandler.CachingHandler, Home"/>
</httpHandlers>
</system.web>
<DataDictionary> <!--Project Name-->
<Caching CachingTimeSpan="30"> <!--days-->
<FileExtensions>
<add Extension="gif" ContentType="image/gif" />
<add Extension="jpg" ContentType="image/jpeg" />
<add Extension="png" ContentType="image/png" />
<add Extension="js" ContentType="text/javascript" Compression="true" />
<add Extension="css" ContentType="text/css" Compression="true" />
</FileExtensions>
</Caching>
</DataDictionary>
In the first configuration block in the web.config, we listed all the MIME types we would ask HttpHandler to pamper. Evidently, we would like to pay attention to gif, jpg, png, js, css files in our project. Of course, you may add more types that you care about.
Then, in the following configuration block, we indicated the way of resources to be looked after (whether to be compressed, caching is default behaviour, you can read it as in the xml setting).
Well, as we have peeked the final status of the work, now, we need to drill down into the code to ask how we attain that. Basically, the implementation could be divided into two major parts.
One part is for supporting the Caching Configuration, which we can flexibly change the behaviour of our application.
Another part is to implement the HttpHandler itself to underpin our configuration.
Let’s explore them one by one.
Configuration Manager
We have three class which help us to gain the ability to configure behaviours of our application in web.config.
public class CachingSection : ConfigurationSection
{
[ConfigurationProperty("CachingTimeSpan", IsRequired = true)]
public TimeSpan CachingTimeSpan
{
get { return (TimeSpan)base["CachingTimeSpan"]; }
set { base["CachingTimeSpan"] = value; }
}
[ConfigurationProperty("FileExtensions", IsDefaultCollection = true, IsRequired = true)]
public FileExtensionCollection FileExtensions
{
get { return ((FileExtensionCollection)base["FileExtensions"]); }
}
}
CachingSection is the entry of Caching node of Caching Configuration. Which reads the caching time from configuration to CachingTimeSpan and provides a Property of FileExtensionCollection.
public class FileExtensionCollection : ConfigurationElementCollection
{
public new FileExtension this[string extension]
{
get { return (FileExtension)BaseGet(extension); }
set
{
if (BaseGet(extension) != null)
{
BaseRemove(extension);
}
BaseAdd(value);
}
}
protected override ConfigurationElement CreateNewElement()
{
return new FileExtension();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((FileExtension)element).Extension;
}
}
This piece of code is for FileExtensionCollection used in CachingSection. Nothing special, just the implementation of a collection for FileExtension.
public class FileExtension : ConfigurationElement
{
[ConfigurationProperty("Extension", IsRequired = true)]
public string Extension
{
get { return (string)base["Extension"]; }
set { base["Extension"] = value.Replace(".", ""); }
}
[ConfigurationProperty("ContentType", IsRequired = true)]
public string ContentType
{
get { return (string)base["ContentType"]; }
set { base["ContentType"] = value; }
}
[ConfigurationProperty("Compression", DefaultValue = false)]
public bool Compression
{
get { return (bool)base["Compression"]; }
set { base["Compression"] = value; }
}
}
FileExtension if the leaf node of Caching Configuration. There three properties including Extension for extension of file, ContentType for MIME type, and Compression to enable or disable compressing.
That’s all the classes for Caching Configuration. You can use it simply by following sample code,
var config = (CachingSection)context.GetSection("DataDictionary/Caching");
N.B. DataDictionary/Caching is the path of your configuration section in web.config.
Caching Handler
Now, you may say "Too much for the configuration stuff, but that’s not what I would like to see at all. Where the heck is the Caching and Compression Handler?"
Well, I admit the things described above are somewhat nice to have. But calm down, now I am ready to dissect the code of handler. First, let’s have a bird view of code.
public class CachingHandler : IHttpHandler
{
private CachingSection m_Config;
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
// ...
}
public static void SetCache(HttpContext context, TimeSpan cacheDuration)
{
// ...
}
private static void SetCompression(HttpContext context, FileExtension extension)
{
// ...
}
}
From the code, we spot there are one field and three methods. The m_Config is used to load configuration from web.config. ProcessRequest is the implementation of IHttpHandler, which handles the requests from client (configured in HttpHandler Configuration). And the rest two, SetCache and SetCompression, apparently, are the core of this post. Here is the details of code.
public void ProcessRequest(HttpContext context)
{
string file = context.Request.FilePath;
string extension = file.Substring(file.LastIndexOf('.') + 1);
if (m_Config == null)
{
m_Config = (CachingSection)context.GetSection("DataDictionary/Caching");
}
SetCache(context, m_Config.CachingTimeSpan);
var fileExtension = m_Config.FileExtensions[extension];
if (fileExtension != null)
{
context.Response.ContentType = fileExtension.ContentType;
SetCompression(context, fileExtension);
}
context.Response.WriteFile(file);
}
The code block above controls the flow of caching and compression. Firstly, when the request comes in, it reads the configuration thru the CachingSection, then calls SetCache method to apply cache, next, calls SetCompression to enable compressing, finally, writes the file requested to response stream. Cool! Let's have a look at SetCache and SetCompression.
public static void SetCache(HttpContext context, TimeSpan cacheDuration)
{
var cache = context.Response.Cache;
cache.SetCacheability(HttpCacheability.Public);
cache.SetExpires(DateTime.Now.Add(cacheDuration));
cache.SetMaxAge(cacheDuration);
cache.SetLastModified(DateTime.MinValue);
cache.SetETag("v1.0");
cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
}
Pretty much like the implementation of this post?
private static void SetCompression(HttpContext context, FileExtension extension)
{
if (!extension.Compression)
{
return;
}
var request = context.Request;
string acceptEncoding = request.Headers["Accept-Encoding"];
if (string.IsNullOrEmpty(acceptEncoding))
{
return;
}
acceptEncoding = acceptEncoding.ToUpperInvariant();
var response = context.Response;
if (acceptEncoding.Contains("DEFLATE"))
{
response.AppendHeader("Content-encoding", "deflate");
response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
}
else if (acceptEncoding.Contains("GZIP"))
{
response.AppendHeader("Content-encoding", "gzip");
response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
}
}
Still similar to this? Of course, because the idea is same. But are we all set? NO, NOT AT ALL. If you are using WebForm, it’s okay for you to run it now. Since we are using ASP.NET MVC, if we don’t take the next action, we can never use our fancy handler, nor can we run the web site correctly. Why? Because the router is too smart to spoil our handler. Remember, this is rather important. Well, the point is, we need to one sentence in Global.asax.cs to tell route system to ignore our handler request.
public class MvcApplication : HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
// ...
routes.IgnoreRoute("{*allashx}", new { allashx = @".*\.ashx(/.*)?" });
// ...
}
}
Now, let’s have look at our new score (still remember that we got 81 in previous post?).
Amazing, score 91, 10 point’s upgrade. It’s much more better than I thought. Hey, let’s grab a beer to celebrate. I will be back soon to iron out the remaining issues. Keep tuned. ;)
References