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:
Looks easy, doesn’t it? Each time the method is called, the individual cocktail will be cached by its ID for 60 seconds.
So simple caching won’t do. We’re going to need to cache four different versions for each cocktail, specifically:
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.
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:
Which is great… except that when we look at it in Firefox, we have the same old problem:
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:
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.
For related information, check out these courses from Learning Tree: