I started extracting some caching code into a more reusable class this week. It comes from one of my favorite applications, which has ~10,000 objects cached at any given time. I figured I'd start this blog off the right way and show off one of my more boneheaded code decisions.
Data in the applications I deal with can usually be identified in a number of ways. The standard "some variant of the type" + "an arbitrary separator" + "a primary key value" cache key definition is a pretty good start, but I'll often need to retrieve data using something other than the primary key value. So here's what I did:
class CacheItemAlias
{
private readonly string key;
public string Key
{
get { return key; }
}
public CacheItemAlias(string key)
{
this.key = key;
}
}
That's simple, right? I can cache my alias object and then use it to "fall through" to another key when it's retrieved:
object something = "This is my cached object!";
Cache.Insert("something", something);
Cache.Insert("alias-to-something", new CacheItemAlias("something"),
new CacheDependency(null, new string[]{"something"})
);
object value = Cache.Get("alias-to-something");
if(value is CacheItemAlias)
{
CacheItemAlias alias = (CacheItemAlias)value;
return Cache.Get(alias.Key);
}
But wait, no, that's really not a good way of doing it. If you translate that code into english, it's "storing a reference to another cache location". Guess what the cache does when I cram an object in it? Basically the same thing.
The cache is really just a great big hashtable, with some extra goodness. Value types (primitives, structs, etc) are stored as copies, other types (almost everything I'm interested in caching) are stored as references. It's obvious, now that I think about it, that my first solution was a terrible idea. This is much better:
object value = new Something();
Cache.Insert("something", value);
Cache.Insert("alias-to-something", value,
new CacheDependency(null, new string[]{"something"})
);
Console.WriteLine(
"Objects are the same? {0}",
object.ReferenceEquals(
Cache["something"],
Cache["alias-to-something"]
)
);
There's no extra work required when retrieving objects, performance will be marginally better, I'm not creating extra objects that I don't need, and so on. On the one hand, I'm depressed that I defaulted to a worse solution. On the other hand, deleting code makes me one happy nerd. My helper function for caching things now looks something like this:
void Insert(string key, object value, params string[] aliases){
Cache.Insert(key, value);
foreach(string alias in aliases)
{
Cache.Insert(alias, value,
new CacheDependency(null, new string[]{key})
);
}
}
One more thing!
When I started playing with "the right" solution, I was concerned about holding onto references that were no longer needed. For some reason, I thought that time based expirations were deferred until the next Cache.Get("key") request, thus keeping the "alias" entries around indefinitely. Fortunately, a quick little console program (and, in hindsight, common sense) proved me wrong. Further investigation with Reflector confirmed the results of that test program. There's a "time based expiration" timer that fires every 20 seconds or so, cleaning out expired items and anything that depends on them.
As an aside, the disassembled code from System.Web.Caching gave me an epic headache.