Saturday, March 29, 2008

Caching Awareness Day: Part 1

There are probably zillions of posts, tutorials etc around on how to do caching in ASP.NET. However a zillion+1 will not hurt. This post is more for sel-reference more than anything. I admit that sometimes I need to go over features I haven't used in a while (or ever?) to refresh my mind, this is one of those days.

In my current job we have an application that was suffering from a performance hangover when retrieving some records from WebServices and the db. I thought for a while how caching can improve the user experience of some of guilty pages.

After a few months in the thinking room waiting for the go ahead from the bosses, we got right into it. The idea was simple: when a particular record was requested from the services it was first stored in the Cache object and any subsequent request for that data will first check in the Application Cache to see if it's there, if there retrieve it, else go to the WebServices, get it, store it in the Cache etc etc. Needless to say the results where fantastic, the users are happy and they love their 'new' app. We are happy, but I kicked myself in the guts why didn't I do it in the first place!

Anyway, enough yarns. I plan to post a series on Caching and the different techniques that can be used to put some Caching love into your apps.

What is Caching?

Caching is all about storing data, objects, pages, part of pages etc in memory immediately after is requested from the original source. These objects are usually stored for example on the Web server, proxy servers or even the client browser. Think of the benefits, much faster to retrieve data from a proxy server 'nearby'  for example, where this data has been previously requested and stored,  than retrieving the data from probably a data source thousands of miles away and likely to be some data that was computationally expensive to generate and retrieve.

ASP.NET provides two types of caching:

  1. Output Caching, where you can store page and controls responses from the original server to the requesting browser.
  2. Data Caching, where you can programmatically store objects to the server's memory so that the application can fetch them from there rather than recreating them.

Caching ASP.NET Pages: Output Caching

Page output caching allows for subsequent requests for a given page to be provided from the cache so that the code that initially created the page does not execute. This is good for pages that are static and that you are confident are very frequently accessed pages. However if you have content on your page that is generated dynamically or has some dependency on a parameter or even a random image, then the same item will be displayed for the duration of the caching! beware! :-)

To enable OutputCaching, you add a <%@ OutputCache directive to a page. For example, in the page below I have cached this page for 10 seconds.

<%@ OutputCache VaryByParam="none" Duration="10" %>

The contents of your page will not be regenerated each time a user requests the page, also the class behind the page will not be executed during that time. This is the most basic kind of OutputCaching you can set up for a page, although very limited and only useful for very static pages.

Moving on, let's see all the properties that can be set for the OutputCache directive.

ouputIntellisense

Note that there is a property called 'VaryByParam". This one is very useful. For example, typical Master/Detail scenario, say you have a page where you show all Categories of Products in the Northwind database.

categories

..and then you wanted to display the Products for each category in  a different page, passing the CategoryID in the query string.

Capture2

Capture3 

Now if you did the above setup for your details page, then all your users will see whatever was cached first.

Enter VaryByParam. This helps you by making ASP.NET cache a new instance of the page when a different value for the CategoryID query string parameter is passed to the Products page.

<%@ OutputCache Duration="100" VaryByParam="CategoryID" %>

You can also vary the output cache by multiple parameters by separating the list of params by semicolon in the VaryByParam attribute ..

<%@ OutputCache Duration="100" VaryByParam="CategoryID;ProductID" %>

Also you can generate a new version of the page when any of the params change by assigning a  * to the attribute.

<%@ OutputCache Duration="100" VaryByParam="*" %>

More information in caching by param can be found in MSDN 

OutputCaching by browser Header

It is possible to also use the VaryByHeader attribute to create different versions of a page according to the browser header. For example, my Firefox browser sends the below set of headers with information.

Connection: keep-alive
Keep-Alive: 300
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-gb,en;q=0.5
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13

My IE7 sends:

Connection: Keep-Alive
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-ms-application, application/vnd.ms-xpsdocument, application/xaml+xml, application/x-ms-xbap, application/x-silverlight, application/x-shockwave-flash, application/x-silverlight-2-b1, */*
Accept-Encoding: gzip, deflate
Accept-Language: en-AU,es-UY;q=0.7,zh-CN;q=0.3
Host: localhost
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; SV1; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506; .NET CLR 3.5.21022)
UA-CPU: x86

You can check what your browser is sneaking out here or here ( as an aside, I heard that in the in 2001 a browser had some customer headers that were used to sneak information about that particular user...read it and see). In .Net I did the above by calling and then iterating the collection.

NameValueCollection headerCollection = Request.Headers;


So......
<%@ OutputCache Duration="1000" VaryByParam="none" VaryByHeader="Accept-Language" %>

That will cache a different page version according to the language used by the user.Right?

...Cache by Browser, getting introduced to the very handy VaryByCustom :-)

You can also cache by the user's browser by using VaryByCustom attribute and using the 'special' browser attribute. By using this attribute, the page will be cached for each mayor browser, its   name and version, eg IE7 and IE8 will result in two different cached versions.

<%@ OutputCache Duration="1000" VaryByParam="none" VaryByCustom="browser" %>

VaryByCustom is very handy. Basically what you can do is write a function in Global.asax or write an HttpModule to handle caching for pages in any way you want.

In Global.asax you do it by overriding the GetVaryCustomString() function.

public override string GetVaryByCustomString(HttpContext context, string custom)
{
     return base.GetVaryByCustomString(context, custom);
}
The function takes two parameters, the HttpContext for the request and a string which is the value you can set in VaryByCustom="[custom_string]", this string can be also be a nunber of string passed along separated by semicolon and you can then split it in GetVaryByCustomString() The context gives you access to everything you expect from HttpContext, session, Request etc etc, 
Example: Say you want to cache pages according to the bosses's mood. 
<%@ OutputCache Duration="1000" VaryByParam="none" VaryByCustom="BossHappinessLevel" %>
Then your custom function could look like..
  
public override string GetVaryByCustomString(HttpContext context, string custom)
{
    if (custom == "BossHappinessLevel")
    {
        MyAjaxEnabledService a = new MyAjaxEnabledService();
        return a.BossHappinessLevel().ToString();       
    }
    return "default";        
}

Based on the string passed, the logic will just return a string that can uniquely identify a request and thus cache a different version of the page according to the current mood! ( The above example doesn't make any sense if your boss is agro all the time, meaning that the "angry" page will be cached most of the time. Of course for this to make more sense, your page will return different content according to the mood too..if he/she is angry or happy or just indifferent. Please drop me an email if you don't get it, I need a drink now! LOL

Caching profiles in Web.Config

Instead of setting the cache policy for each page individually, you can configure page caching in the web.config and then apply your killer settings to lots of pages...

<system.web>
   <caching>
       <outputCacheSettings>
         <outputCacheProfiles>
               <add duration="1000" name="ByBossHappiness" varyByParam="none" varyByCustom="BossHappinessLevel" />
               <add duration="1000" name="ByBrowser" varyByCustom="browser" varyByParam="none" />
         </outputCacheProfiles>
       </outputCacheSettings>
   </caching>
</system.web>

Then your pages can recall any of the cacheprofiles by using the CacheProfile attribute of OutputCache

<%@ OutputCache CacheProfile="ByBossHappiness" %>

Next post:

In my next post I'd like to investigate how to do the above programmatically and also expire pages in code due to some event for example, if someone in the Northwind database adds a new Product by using the Reponse.RemoveOutputCacheItem() function.

Other things left to do include:
  • Partial Page caching.
  • UserControl caching.
  • Data caching
  • Caching by dependencies
  • SQL caching policies by using dependencies!

2 comments:

Dani said...

A good and extensive intro to caching in ASP.NET. Looking forward to read more...

elsharpo said...

Thanks Dani,

Appreciate your comments and I'm glad that you find it useful.

Cheers,

Juan