ASP.NET: Application level data caching with callbacks
In my current application, the vast majority of the web site is broken into content ‘parts’ that can all be edited through a built in content manager. Pages consist of one or multiple parts which are elements of HTML persisted to the SQL database. In order to improve performance I wanted to look at some techniques for caching these content elements and coupled with my desire to learn new things I decided to use the Cache class directly instead of the more common methods of Output Caching.
My plan was to load all of the content elements into cache during the application startup and then wire up a dependency to a file that I would ‘touch’ any time some content was updated. This was largely experimental at this stage so I chose to load all of the content into a string dictionary, cache the collection and wire the dependency to one file. (In the near term I plan to test this against the perf of caching each content element separately and wiring dependencies to an individual file for each element; but that is later)
Simple enough so far…I want to call my method that will fill the cache during Application_Startup event in the Global ASA
protected void Application_Start(Object sender, EventArgs e)
{
FillContentCache();
}
private void FillContentCache()
{
HttpContext.Current.Cache.Insert(
"CONTENT",
new Content(true).GetActiveElements(),
new CacheDependency(cfg.AppSettings["ContentDependencyFile"]),
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
new CacheItemRemovedCallback (this.CacheItemRemoved)
);
}
In the above code you will notice that I am wiring up the callback to CacheItemRemoved which is the name of my method that I want called when the dependency file is touched or the item is removed from cache for some other reason. In my case I am making this really simple, if it gets removed then call put it back in the cache…
{
FillContentCache();
}
Pretty simple, only problem is that this does not work. The dependency wires up fine and the callback works when you change the file but the code in FillContentCache does not work when it is invoked from CacheItemRemoved.
OK, so I do a little searching on google and fear washes over me. There are a lot of people who have had this same problem, and no one was offering any answers. I could go into all the things that were suggested and things I tried but I will skip all that and tell you why this does not work (it made me feel stupid) and how to make it work.
This code will not work because when CacheItemRemoved is called, it does not necessarily happen inside the context of a request. This fact means that HttpContext.Current.Cache will cause a NullReferenceException to be thrown; but I failed (and I assume most others who took this road) because I did not detect this Exception right away. I was testing with a page that enumerated the cache for me and a button that would invalidate it; the cache enumerates itself before the click event of the button would fire thus visually tricking me into believing the problem was somewhere other than right in front of me…DOH!
Well, beyond my brain fart there is still the problem of the fact that when the callback occurs, we lack the Context necessary to tap the Cache object; luckily though it is a problem easily fixed.
In the Global ASA we create a static instance of the Cache class and then set it to the Cache object during Application_Startup.
private static Cache _cache = null;
protected void Application_Start(Object sender, EventArgs e)
{
_cache = System.Web.HttpContext.Current.Cache;
FillContentCache();
}
Since Cache is a reference type, when we assign our _cache instance to System.Web.HttpContext.Current.Cache we are effectively creating a pointer to that spot in memory. We then change our code in FillContentCache to insert into _cache which refers to the Cache while we own some context to it…
private void FillContentCache()
{
_cache.Insert(
"CONTENT",
new Content(true).GetActiveElements(),
new CacheDependency(cfg.AppSettings["ContentDependencyFile"]),
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.Default,
new CacheItemRemovedCallback (this.CacheItemRemoved)
);
}
This will then properly update the application cache while being fired outside the scope of a request which holds application context.
Keep in mind this was largely experimental tinkering and I have not done any serious perf testing on this to determine if it is road worthy.