Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

An ASP.NET abstract class Designed for Cache Management in High Traffic Web Sites

4.16/5 (13 votes)
6 Oct 2014CPOL2 min read 47.9K   316  
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.

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:

C#
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:

C#
    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;
        }

        /// <summary>
        /// This is the only parameter used by this method
        /// </summary>
        int _productId;

        /// <summary>
        /// This method builds a unique string generated by the parameters set 
        /// (in this case only one)
        /// </summary>
        /// <returns>
        protected override string GetCacheKey()
        {
            return _productId.ToString();
        }

        /// <summary>
        /// This method is a concrete implementation of an abstract method and 
        /// contains the code that retrieves the data from the data source
        /// </summary>
        /// <returns>
        protected override Product LoadData()
        {
            //This call simulate a long time running query
            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.

C#
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";
        /// <summary>
        /// cache expiration in minutes. Default is 60
        /// </summary>
        protected int _Expiration = 1;

        /// <summary>
        /// if the class uses the control value with deferred expiration mechanism. Default is true
        /// </summary>
        protected bool _UseControlValue = true;

        /// <summary>
        /// The difference in minutes between the control value expiration 
        /// and the real data expiration. Default is one
        /// </summary>
        protected int _ControlValueExpirationDifference = 1;

        /// <summary>
        /// Cache priority
        /// </summary>
        protected System.Web.Caching.CacheItemPriority _Priority = 
                                     System.Web.Caching.CacheItemPriority.Normal;

        /// <summary>
        /// Deprecated, not used anymore
        /// </summary>
        protected bool _DoCallBack = false;

        /// <summary>
        /// If true the object is saved in cache, otherwise it's always retrieved from data source
        /// </summary>
        protected bool _UseCache = true;

        /// <summary>
        /// This property builds the cache key by using the reflected name 
        /// of the class and the GetCacheKey
        /// method implemented in the concrete class
        /// </summary>
        private string CacheKey
        {
            get
            {
                return this.GetType().ToString() + "-" + this.GetCacheKey();
            }
        }

        private int DataExpiration
        {
            get {
                return _Expiration + (_UseControlValue ? _ControlValueExpirationDifference : 0);
            }
        }

        /// <summary>
        /// Adds data do cache
        /// </summary>
        /// <param name="localResult"></param>
        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();
        }

        /// <summary>
        /// This abstract method has to be redefined in the concrete class 
        /// in order to define a unique cache key
        /// </summary>
        /// <returns></returns>
        protected abstract string GetCacheKey();

        /// <summary>
        /// This abstract method has to be implemented in the concrete class
        /// and wiil contain the code that performs the query
        /// </summary>
        /// <returns></returns>
        protected abstract T LoadData();

        /// <summary>
        /// This method calls the LoadData method and is passed 
        /// to the Cache.Insert method as a callback
        /// </summary>
        /// <param name="cacheKey"></param>
        /// <param name="obj"></param>
        /// <param name="reason"></param>
        private void LoadCache(string cacheKey, object obj, 
                         System.Web.Caching.CacheItemRemovedReason reason)
        {
            //If an object has been explicitly removed or is expired due to underusage, 
            //it is not added to cache.
            if (reason != System.Web.Caching.CacheItemRemovedReason.Removed && 
                      reason != System.Web.Caching.CacheItemRemovedReason.Underused)
            {
                if (obj != null)
                {
                    //Expired object is immediately added again to cache 
                    //so the user doesn't have to wait till the end of the query
                    CacheManager.CurrentCache.Insert(cacheKey, obj);
                }
                T localResult = LoadData();
                AddDataToCache(localResult);
            }
        }

        /// <summary>
        /// Gets the method data from data source or cache
        /// </summary>
        /// <returns></returns>
        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;
                        //The control value is immediately re-inserted in the cache 
                        //so other user will not see the object as expired
                        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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)