I’m reposting this blog entry with code in lieu of images after Ian sent me a rant email quite rightly complaining that he couldn’t read the code in the images (apologies to Ian and anyone else who reads this blog!)
<snip/>
The ASP.NET cache is a thing of beauty. But one thing that fills me with dread whenever I see it is an application littered with ASP.NET caching access code.
One of the other issues that developers have to handle is the possibility that stuff you put in it may not be there next time you ask for it back because the cache is prone to being purged when memory is constrained. This can lead to hideous looking developer code like this:
public void Page_Load(object sender, EventArgs args)
{
Person p = (Person)Cache["human"];
if (p == null)
{
p = HumanBusinessLogic.GetPerson(Request["id"]);
Cache["human"] = p;
}
// go ahead and use p
}
Yuck!!
To clean up the mess with a little refactoring, we could at least put the cache code in the business logic:
public static Person GetPerson(string id)
{
Cache cache = HttpContext.Current.Cache;
Person p = (Person)cache["human"];
if (p == null)
{
p = HumanDataAccess.GetPerson(id);
cache["human"] = p;
}
return p;
}
But the problem now comes that for every business method that uses caching, there will be a cut and paste of the exact same code plus marginal refactor. The solution to that issue might lead us to develop a base class that offers up some kind of caching method that all business logic classes can then derive from and gain benefit.
This base class method would need to solve a couple of scenarios for it to be of any use to a derived class. First, it must look in the cache to see if the item is there and return it if it is. Second, it should have a mechanism of getting the item from the database and repopulating the cache if the item is not in the cache. The first scenario is a breeze, but the second scenario could be problematic. For example, most business logic entities come from a data access method invocation, but how many parameters will the data access method need and what types will they be?
A first crack at solving the problem might be to pretend that the data access method takes no parameters. That way, we could pass a delegate as a parameter that is invoked by the cache code when it detects a cache miss. The code would look like this (and notice the delegate takes no parameters)
public delegate DataTable RetrieveData();
public class CurriedCache
{
public static DataTable getCachedDataItem(string cacheKey,
RetrieveData retriever)
{
Cache cache = HttpRuntime.Cache;
DataTable item = (DataTable)cache[cacheKey];
if (item == null)
{
lock (typeof(CurriedCache))
{
item = (DataTable)cache[cacheKey];
if (item == null)
{
item = retriever();
cache.Add(cacheKey,
item,
null,
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
null);
}
}
}
return item;
}
}
There are some huge issues with this implementation. First of all, it is assumed that the item being stored is a DataTable. Second, there is still the assumption that the retriever function takes no parameters.
To solve the first problem, we can use generics:
public delegate TResult RetrieveData<TResult>();
public class CurriedCache
{
public static TResult getCachedDataItem<TResult>(
string cacheKey,
RetrieveData<TResult> retriever)
where TResult : class
{
Cache cache = HttpRuntime.Cache;
TResult item = (TResult)cache[cacheKey];
if (item == null)
{
lock (typeof(CurriedCache))
{
item = (TResult)cache[cacheKey];
if (item == null)
{
item = retriever();
cache.Add(cacheKey,
item,
null,
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
null);
}
}
}
return item;
}
}
A useful bit of tidy up would be to replace the RetrieveData delegate with the Func<TResult> delegate that Microsoft already define as part of a family of delegates called Func that take no arguments, one argument, two arguments, etc respectively.
namespace System
{
public delegate TResult Func<TResult>();
public delegate TResult Func<T, TResult>(T arg);
public delegate TResult Func<T, TResult>(T arg);
public delegate TResult Func<T1, T2, TResult>(T1 arg1,
T2 arg2);
public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1,
T2 arg2, T3 arg3);
public delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
}
It’s almost done, but not quite. There is still the serious limitation of the retriever function taking no parameters. The solution involves a bit of currying. We will also define overloaded versions of the getCachedDataItem to cater for retriever functions that expect more than one parameter:
public class CurriedCache
{
public static TResult getCachedDataItem<TResult>(
string cacheKey,
object monitor,
DateTime absoluteExpiration,
TimeSpan slidingExpiration,
Func<TResult> retriever)
where TResult : class
{
Cache cache = HttpRuntime.Cache;
TResult item = (TResult)cache[cacheKey];
if (item == null)
{
lock (monitor)
{
item = (TResult)cache[cacheKey];
if (item == null)
{
item = retriever();
cache.Add(cacheKey,
item,
null,
absoluteExpiration,
slidingExpiration,
CacheItemPriority.Default,
null);
}
}
}
return item;
}
public static TResult getCachedDataItem<T1, TResult>(
string cacheKey,
object monitor,
DateTime absoluteExpiration,
TimeSpan slidingExpiration,
Func<T1, TResult> retriever,
T1 a)
where TResult : class
{
return getCachedDataItem<TResult>(cacheKey,
monitor,
absoluteExpiration,
slidingExpiration,
() => { return retriever(a); });
}
public static TResult getCachedDataItem<T1, T2, TResult>(
string cacheKey,
object monitor,
DateTime absoluteExpiration,
TimeSpan slidingExpiration,
Func<T1, T2, TResult> retriever,
T1 a,
T2 b)
where TResult : class
{
return getCachedDataItem<TResult>(cacheKey,
monitor,
absoluteExpiration,
slidingExpiration,
() => { return retriever(a, b); });
}
}
Notice how the overloaded versions expect the retriever function to be a delegate that takes one or more arguments and notice how this delegate is then invoked in a wrapped delegate of the first type (Func<TResult>)
Since it’s likely we want to be able to lock cached items independently, it probably makes sense to introduce a lock parameter for finer grained locking. The addition of absolute and sliding expiration parameters also give a degree of finer control over caching behavior. The final tidy up also uses lamda expression syntax to invoke the delegates.














