Improving Performance with Output Caching

I’ve been spending most of my spare time over the past few weeks refactoring the code for my site, cocktailsrus.com, in the Visual Studio 11 beta–partly to play with the new stuff, but mostly just to replace my original quick-and dirty back end with something more elegant. With the process pretty much complete, the new version of the site will be much more maintainable, as well as more performant and scalable. And I don’t know about you, but whenever I see those two words together, the first thing that leaps to my mind is “caching.”

Caching is a fantastic way of reducing the amount of data access on a website. Unfortunately, it can also be a fantastic way of tying yourself in knots and serving the wrong data to the wrong user. It is just too easy to serve stale data–especially when you transition from a single server to a web farm. So which type of caching to use, and how to avoid serving stale data?

ASP.NET offers two types of caching:  data caching and output caching. Data caching is managed programmatically. Output caching is managed via attributes (MVC) or page directives (Web forms).

Historically, I’ve preferred data caching. I’ve specialized in content management systems over the years, and it’s a very rare situation where a CMS delivers the same content to everyone. Data caching allows very fine-grained control. Unfortunately, it doesn’t play very well with Web farms–at least, not since Microsoft removed some of the core SQL Server technologies that allowed it to do so.

So, given that I want to be able to scale up to a Web farm, the question I asked myself was, Could output caching help my application?

The obvious candidate for output caching is the page that displays individual cocktails. These don’t change very often, and are amongst the most heavily trafficked on the site. Also, and most significantly, retrieving a cocktail from the database involves a complex SQL query with many table joins. (A cocktail has multiple ingredients, related cocktails, an author, etc.) The fewer such calls I make, the better.

On the face of it, caching the individual cocktails should be easy. Each one has a unique ID, so you should need to do is add an OutputCache directive, like this:

output cache directive

Looks easy, doesn’t it? Each time the method is called, the individual cocktail will be cached by its ID for 60 seconds.

Unfortunately, it really isn’t as simple as that. This caching will work–but it will deliver the same version of the output to everyone: mobile device users, AJAX users, non-AJAX users…. You get the picture. A user with JavaScript enabled will suddenly find that they are getting the full page rather than an AJAX payload, leading to pages within pages, like this:

page within page screengrab

So simple caching won’t do. We’re going to need to cache four different versions for each cocktail, specifically:

  1. Mobile with no AJAX
  2. Mobile with AJAX
  3. Standard with no AJAX
  4. Standard with AJAX

Unfortunately, there isn’t a querystring parameter that’s going to allow us to make that distinction, so it’s time for a custom parameter.

The first thing we have to do is override the GetVaryByCustomString() method in the global.asax. Here, we can use the request to filter our caching into the four different groups. The site uses jQuery, and jQuery AJAX requests use the “X-Requested-With” header, so we can use that to determine if this was an AJAX request. That’s half of the problem solved. We can then use the underlying infrastructure (or, perhaps, our own version of it) to find out if this request came from a mobile device.

GetVaryByCustomString code

We then assign this to the controller method and we should have something that caches multiple versions and returns the appropriate version from the cache:

VaryByCustom argument

Which is great… except that when we look at it in Firefox, we have the same old problem:

second page within page screengrab

The issue here is where the caching is happening. Output cache is a clever beast, and it’s automatically offloading the work to the client–which is a great idea, but means that if our client switches from AJAX to non-AJAX versions, we end up delivering the wrong item from cache.

Fortunately, there is a simple solution. The OutputCache attribute accepts a Location argument, which you can use to tell it to cache on the server rather than the client:

Corrected OutputCache attribute

So now I have a caching system that will work happily on a Web farm, significantly reduce data access and provide the appropriate custom view to the client.

Kevin Rattan

For related information, check out these courses from Learning Tree:

Building Web Applications with ASP.NET MVC

Building Web Applications with ASP.NET and AJAX

jQuery: A Comprehensive Hands-On Introduction

Type to search blog.learningtree.com

Do you mean "" ?

Sorry, no results were found for your query.

Please check your spelling and try your search again.