Introduction
This article provides a simple generic base class that allows fast and easy access to cached methods. The class is especially designed for high traffic web sites as it reduces to the minimum the number of concurrent queries to the data source.
Using the Code
The provided source code contains a Visual Studio 2008 web site.
This is the syntax to use in order to call a cached method:
Product product = new LoadProductMethod(
1,
1,
System.Web.Caching.CacheItemPriority.Normal,
true).GetData();
This method tries to retrieve a product (key = 1) from the database (simulated in the code), and asks to put the result in cache with a 1 minute expiration time and a normal priority.
The LoadProductMethod
class was created in order to implement a single particular query. For any new query, a new class like this has to be created. Of course, you can choose to expose a complete constructor (as in the example), or to limit the parameters that the end consumer can control.
Let's take a look at it:
public class LoadProductMethod : BaseCachedMethod<Product>
{
public LoadProductMethod(int productId,
int expiration,
System.Web.Caching.CacheItemPriority priority,
bool useCache)
{
_productId = productId;
_Expiration = expiration;
_Priority = priority;
_UseCache = useCache;
}
int _productId;
protected override string GetCacheKey()
{
return _productId.ToString();
}
protected override Product LoadData()
{
System.Threading.Thread.Sleep(2000);
Product product = new Product(_productId);
return product;
}
}
}
This cached method implements the abstract
BaseCachedMethod<T>
class. The GetCacheKey()
method returns a unique string
that identifies the query parameters. The LoadData()
method contains the actual code that retrieves data from the database (the query delay is simulated by means of a Sleep
call).
What is written so far is the only custom code that has to be written in order to implement a new cached method.
Let's now have a look at the BaseCachedMethod<T>
class.
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.Caching;
namespace CacheManager
{
public abstract class BaseCachedMethod<T>
{
private const string _CONTROL_VALUE_APPENDIX = "ControlValue";
protected int _Expiration = 1;
protected bool _UseControlValue = true;
protected int _ControlValueExpirationDifference = 1;
protected System.Web.Caching.CacheItemPriority _Priority =
System.Web.Caching.CacheItemPriority.Normal;
protected bool _DoCallBack = false;
protected bool _UseCache = true;
private string CacheKey
{
get
{
return this.GetType().ToString() + "-" + this.GetCacheKey();
}
}
private int DataExpiration
{
get {
return _Expiration + (_UseControlValue ? _ControlValueExpirationDifference : 0);
}
}
private void AddDataToCache(T localResult)
{
System.Web.HttpContext.Current.Trace.Warn("AddDataToCache", CacheKey);
CacheManager.CurrentCache.Insert(CacheKey, localResult, null,
DateTime.Now.AddMinutes(DataExpiration),
System.Web.Caching.Cache.NoSlidingExpiration, _Priority, null);
AddControlValueToCache();
}
protected abstract string GetCacheKey();
protected abstract T LoadData();
private void LoadCache(string cacheKey, object obj,
System.Web.Caching.CacheItemRemovedReason reason)
{
if (reason != System.Web.Caching.CacheItemRemovedReason.Removed &&
reason != System.Web.Caching.CacheItemRemovedReason.Underused)
{
if (obj != null)
{
CacheManager.CurrentCache.Insert(cacheKey, obj);
}
T localResult = LoadData();
AddDataToCache(localResult);
}
}
public T GetData()
{
T result = default(T);
if (_UseCache)
{
bool reloadData = false;
object objInCache = null;
if(_UseControlValue)
{
object singleReloadObj = CacheManager.CurrentCache.Get
(CacheKey + _CONTROL_VALUE_APPENDIX);
if(singleReloadObj==null)
{
System.Web.HttpContext.Current.Trace.Warn("Control value is null", CacheKey);
reloadData=true;
AddControlValueToCache();
}
}
if(!reloadData)
{
objInCache = CacheManager.CurrentCache.Get(CacheKey);
}
if (objInCache == null)
{
System.Web.HttpContext.Current.Trace.Warn("Load real data", CacheKey);
result = LoadData();
AddDataToCache(result);
}
else
{
System.Web.HttpContext.Current.Trace.Warn("Get object from cache", CacheKey);
result = (T)objInCache;
}
}
else
{
result = LoadData();
}
return result;
}
public T GetDataIfInCache()
{
return (T)CacheManager.CurrentCache.Get(CacheKey);
}
private void AddControlValueToCache()
{
if (_UseControlValue)
{
System.Web.HttpContext.Current.Trace.Warn("AddControlValueToCache", CacheKey);
CacheManager.CurrentCache.Insert(CacheKey + _CONTROL_VALUE_APPENDIX,
true, null, DateTime.Now.AddMinutes(_Expiration),
System.Web.Caching.Cache.NoSlidingExpiration, _Priority, null);
}
}
}
}
This class has one public
method (GetData
) that contains the logic needed to check whether an object is contained in the Cache or has to be retrieved from the data source.
The LoadCache
method, before calling the LoadData
method, inserts in Cache the expired object so the user will never experience a direct query on the data source as data will be always available in the Cache (of course, in order to preserve scalability, this doesn't happen when the object is explicitly removed by the ASP.NET Cache or by code).
If the variable _UseControlValue
is true
, which is the default, each time an object is added to cache, another simple boolean object, called "control value", is added as well with a lower expiration value (default difference is one minute but it can be changed). When the GetData()
method is called, the code first checks whether the so called "control value" is available. If not, the control value is immediately recreated and put in cache, and the actual query is performed and data refreshed to the updated value. In this way, other users that need the same data while the first one (very unlucky!) is loading actual data from the data source can still get it from the cache as this is not expired. This allows to have long running queries even in high traffic web sites: only one user will actually perform the query.