Ruminations of J.net idle rants and ramblings of a code monkey

Cool way to do ASP.NET Caching with Linq

.NET Stuff | Linq | Web (and ASP.NET) Stuff

OK, well, I think it's cool (and since the mind is its own place ...). I've been a big fan of ASP.net's cache API since I found out it way back in the 1.0 beta. It certainly solves something that was problematic in ASP "Classic" in a clean, elegant and darn easy to use way. Unfortunately, not a lot of folks seem to know about it. So I'll start with a little overview of ASP.net caching.

As the name implies, it's a cache that sits server side. All of the relevant, .Net-supplied classes are in the System.Web.Caching namespace and the class representing the cache itself is System.Web.Caching.Cache. You can access it from the current HttpContext (which you'll see). The management of the cache is handled completely by ASP.net ... you just have to add objects to it and then read from it. When you add to the cache, you can set options like dependencies, expiration, priority and a delegate to call when the item is removed from the cache. Dependencies are interesting ... they will automatically invalidate (and remove) the cache item based on notification from the dependency. ASP.net 1.x had only 1 cache dependency class (System.Web.Caching.CacheDependency) that allowed you to have a dependency on a file, another cache item, and array of them or another CacheDependency. Framework 2.0 introduced System.Web.Caching.SqlCacheDependency for database dependencies and System.Web.Caching.AggregateCacheDependency for multiple, related dependencies. With the AggregateCacheDependency, if one of the dependencies changes, it item is invalidated and tossed from the cache. Framework 2.0 also (finally) "unsealed" the CacheDependency class, so you could create your own cache dependencies. With expiration, you can have an absolute expiration (specific time) or a sliding expiration (TimeSpan after last access). Priority plays into the clean-up algorithm; the Cache will remove items that haven't expired if the cache taking up too much memory/resources. Items with a lower priority are evicted first. Do yourself a favor and make sure that you keep your cache items reasonable. Your AppDomain will thank you for it.

ASP.net also provides page and partial-page caching mechanisms. That, however, is out of our scope here. For the adventurous among that don't know what that is ...

So ... the cache ... mmmmm ... yummy ... gooooood. It's golly-gee-gosh-darn useful for items that you need on the site, but don't change often. Those pesky drop-down lookup lists that come from the database are begging to be cached. It takes a load off the database and is a good way to help scalability - at the cost of server memory, of course. (There ain't no free lunch.) Still, I'm a big fan of appropriate caching.

So ... what's the technique I mentioned that this post is title after? Well, it's actually quite simple. It allows you to have 1 single common method to add and retrieve items from the cache ... any Linq item, in fact. You don't need to know anything about the cache ... just the type that you want and the DataContext that it comes from. And yes, it's one method to rule them all, suing generics (generics are kewl!) and the Black Voodoo Majick goo. From there, you can either call it directly from a page or (my preferred method) write a one-line method that acts as a wrapper. The returned objects are detached from the DataContext before they are handed back (so the DataContext doesn't need to be kept open all) and returned as a generic list object. The cache items are keyed by the type name of the DataContext and the object/table so that it's actually possible to have the same LinqToSql object come from two different DataContexts and cache both of them. While you can load up the cache on application start up, I don't like doing that ... it really is a killer for the app start time. I like to lazy load on demand. (And I don't wanna hear any comments about the lazy.)

Here's the C# code:

/// <summary>
/// Handles retrieving and populating Linq objects in the ASP.NET cache/// </summary>
/// <typeparam name="LinqContext">The DataContext that the object will be retrieved from.</typeparam>
/// <typeparam name="LinqObject">The object that will be returned to be cached as a collection.</typeparam>
/// <returns>Generic list with the objects</returns>public static List<LinqObject> GetCacheItem<LinqContext, LinqObject>()
where LinqObject : class
where LinqContext : System.Data.Linq.DataContext, new()
{
//Build the cache item name. Tied to context and the object.string cacheItemName = typeof(LinqObject).ToString() + "_" + typeof(LinqContext).ToString();//Check to see if they are in the cache.List<LinqObject> cacheItems = HttpContext.Current.Cache[cacheItemName] as List<LinqObject>;if (cacheItems == null)
{
//It's not in the cache -or- is the wrong type. 
//Create a new list.cacheItems = new List<LinqObject>();//Create the contect in a using{} block to ensure cleanup.using (LinqContext dc = new LinqContext())
{
try{
//Get the table with the object from the data context.System.Data.Linq.Table<LinqObject> table = dc.GetTable<LinqObject>();//Add to the generic list. Detaches from the data context.cacheItems.AddRange(table);//Add to the cache. No absolute expirate and a 60 minute sliding expirationHttpContext.Current.Cache.Add(cacheItemName, cacheItems, null,
System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(60),
System.Web.Caching.CacheItemPriority.Normal, null);
}
catch (Exception ex)
{
//Something bad happened.throw new ApplicationException("Could not retrieve the request cache object", ex);
}
}
}
//return ...return cacheItems;
}

And in VB (see, I am multi-lingual!) ...

''' <summary>''' Handles retrieving and populating Linq objects in the ASP.NET cache
''' </summary>''' <typeparam name="LinqContext">The DataContext that the object will be retrieved from.</typeparam>''' <typeparam name="LinqObject">The object that will be returned to be cached as a collection.</typeparam>''' <returns>Generic list with the objects</returns>Public Shared Function GetCacheItem(Of LinqContext As {DataContext, New}, LinqObject As Class)() As List(Of LinqObject)
Dim cacheItems As List(Of LinqObject)
'Build the cache item name. Tied to context and the object.Dim cacheItemName As String = GetType(LinqObject).ToString() + "_" + GetType(LinqContext).ToString()
'Check to see if they are in the cache.Dim cacheObject As Object = HttpContext.Current.Cache(cacheItemName)
'Check to make sure it's the correct type.If cacheObject.GetType() Is GetType(List(Of LinqObject)) ThencacheItems = CType(HttpContext.Current.Cache(cacheItemName), List(Of LinqObject))
End If
If cacheItems Is Nothing Then'It's not in the cache -or- is the wrong type. 
'Create a new list.cacheItems = New List(Of LinqObject)()
'Create the contect in a using   block to ensure cleanup.Using dc As LinqContext = New LinqContext()
Try'Get the table with the object from the data context.Dim table As Linq.Table(Of LinqObject) = dc.GetTable(Of LinqObject)()
'Add to the generic list. Detaches from the data context.cacheItems.AddRange(table)
'Add to the cache. No absolute expirate and a 60 minute sliding expirationHttpContext.Current.Cache.Add(cacheItemName, cacheItems, Nothing, _
Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(60), _
CacheItemPriority.Normal, Nothing)
Catch ex As Exception'Something bad happened.Throw New ApplicationException("Could not retrieve the request cache object", ex)
End Try
End Using
End If'return ...Return cacheItemsEnd Function

 

The comments, I think, pretty much say it all. It is a static method (and the class is a static class) because it's not using any private fields (variables). This does help performance a little bit and, really, there is no reason to instantiate a class if it's not using any state. Also, note the generic constraints - these are actually necessary and make sure that we aren't handed something funky that won't work. These constraints are checked and enforced by the compiler.

Using this to retrieve cache items is now quite trivial. The next example shows a wrapper function for an item from the AdventureWorks database. I made it a property but it could just as easily be a method. We won't get into choosing one over the other; that gets religious.

public static List<StateProvince> StateProvinceList
{
get{
return GetCacheItem<AdvWorksDataContext, StateProvince>(); 
}
}

And VB ...

Public ReadOnly Property StateProvinceList() As List(Of StateProvince)
Get
Return GetCacheItem(Of AdvWorksDataContext, StateProvince)()
End Get
End Property

Isn't that simple? Now, if you only have one DataContext type, you can safely code that type into the code instead of taking it as a generic. However, looking at this, you have to admit ... you can use this in any ASP.net project where you are using Linq to handle the cache. I think it's gonna go into my personal shared library of tricks.

As I think you can tell, I'm feeling a little snarky. It's Friday afternoon so I have an excuse. BTW ... bonus points to whoever can send me an email naming the lit reference (and finish it!) in this entry. Umm, no it isn't Lord of the Rings.

Comments are closed