Elegant ASP.NET Caching
Thursday, August 16 2007 6 Comments
So you have a central method that returns some common data, say for example US States.
public IList<State> GetUsStates()
{
List<State> states = new List<State>();
using(IDataReader dr = DataAccess.ExecuteReader("SELECT * FROM US_STATES"))
{
State theState = State.Fetch(dr);
states.Add(theState);
}
return states;
}
Pretty simple stuff, loop over a datareader, building “State” objects based on a row in the database.
Well since this data hardly ever changes and is the same for every user, it is a prime target for caching. (Psst, if you didn’t know, caching can be one of the most effective performance tuning techniques you can perform — if done correctly).
Now that we want to enable caching, we can create a little helper class that makes it a bit easier to work with…
This is a watered down version of the cache API, but it solves 90% of the cases.
public class CacheHelper
{
public static bool ItemExists(string key)
{
return HttpContext.Current.Cache[key] != null;
}
public static void Insert(string key, object obj)
{
TimeSpan oneHour = new TimeSpan(1,0,0);
Insert(key, obj, oneHour);
}
public static void Insert(string key, object obj, TimeSpan span)
{
HttpContext.Current.Cache.Add(
key,
obj,
null, //no dependencies
DateTime.Now.Add(span),
Cache.NoSlidingExpiration,
CacheItemPriority.Normal,
null //no remove callback
);
}
public static T Retrieve<T>(string key)
{
return (T)HttpContext.Current.Cache[key];
}
}
Now that we have the utility methods in place, we can alter our US States function to enable caching:
public IList<State> GetUsStates()
{
string statesKey = "US_STATES";
if (!CacheHelper.ItemExists(statesKey))
{
//add it
List<State> states = new List<State>();
using (IDataReader dr = DataAccess.ExecuteReader("SELECT * FROM US_STATES"))
{
State theState = State.Fetch(dr);
states.Add(theState);
}
CacheHelper.Insert(statesKey, states);
}
//now it definitely exists in cache
return CacheHelper.Retrieve<IList<State>>(statesKey);
}
This code quickly gets repetetive and ugly, but it gets the job done. How can we do better?
We need to be able to provide the means of getting the data, but without actually executing it every time. This sounds like a good use for a delegate!
In our CacheHelper class we define the delegate:
public delegate object RetrieveDelegate();
It’s just a method signature that returns an object.
Then we define another method that will first check the cache, and execute the delegate if the data is not there:
public static T GetAndCache(string key, RetrieveDelegate retrieveObject, TimeSpan span)
{
if (!ItemExists(key))
{
object value = retrieveObject(); //this makes the call to the database
Insert(key, value, span);
}
return Retrieve<T>(key);
}
Now we have a method that can be smart about caching, but it is completely agnostic of what data to cache. Now we can refactor our GetStates() method to this:
public IList<State> GetUsStates()
{
return CacheHelper.GetAndCache<IList<State>>(
"US_STATES",
delegate {
List<State> states = new List<State>();
using (IDataReader dr = DataAccess.ExecuteReader("SELECT * FROM US_STATES"))
{
State theState = State.Fetch(dr);
states.Add(theState);
}
return states;
},
TimeSpan.FromDays(1));
}
We only have to interact with the CacheHelper once, thus we only use the cache key once as well. There are no if statements here either. Just a single function call that excepts an anonymous method.
This is pretty clean and it makes caching in your application easier. What do you think? Do you have any ideas for improvement? Let’s hear in the comments!


mnichols AT cei-az.com (Mike Nichols)
8.17.2007
3:19 AM
It seems like you are violating Separation of Concerns here. First, you are having your method get the cache mechanism. I'd rather inject an ICache that the method consumes (perhaps at class level, not method parameter). I guess you could do the injection into the CacheHelper, though so that slips by that barely. Also, the actual access code should be in its own object as well rather than here I think since that is another mix of concerns here. If you encapsulated it in an object you could mark the object as ICacheable or something to be passed to a ChainOfResponsibility handler that caches the results once it has accessed the cache.Finally, in my opinion you are blending a dataaccess concern into your cache even though it is wrapped up in a delegate. Abstracting the conditional in a delegate seems to obscure the code to me. Chain Of Responsibility or perhaps decorating the class that has this method would be my first go at it.Just my 2,004 pesos. :)