Click here to Skip to main content
Click here to Skip to main content

An ASP.NET abstract class designed for cache management in high traffic web sites.

, 6 Oct 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
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:

Product product = new LoadProductMethod(
                      1,                                  
                      1,
                      System.Web.Caching.CacheItemPriority.Normal,  
                      true,
                      false).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, 
                                 bool doCallBack)
        {
            _productId = productId;
            _Expiration = expiration;
            _Priority = priority;
            _UseCache = useCache;
            _DoCallBack = doCallBack;
        }

        /// <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.

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>
        /// If true means that the cache wil be automatically refreshed after expiration
        /// </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);
            if (_DoCallBack)
            {
                CacheManager.CurrentCache.Insert(CacheKey, localResult, null, DateTime.Now.AddMinutes(DataExpiration), System.Web.Caching.Cache.NoSlidingExpiration, _Priority, new CacheItemRemovedCallback(LoadCache));
            }
            else
            {
                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 epired 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);
                if (_DoCallBack)
                {
                    CacheManager.CurrentCache.Insert(CacheKey + _CONTROL_VALUE_APPENDIX, true, null, DateTime.Now.AddMinutes(_Expiration), System.Web.Caching.Cache.NoSlidingExpiration, _Priority, new CacheItemRemovedCallback(LoadCache));
                }
                else
                {
                    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.

LoadCache is passed as a callback method (when DoCallBack == true) to the Cache.Insert method so the LoadData method is automatically called when the object expires.

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)

Share

About the Author

Paolo Costa
Software Developer (Senior)
Italy Italy
Paolo Costa is a software developer with long experience on any kind of .NET application. He lives in Italy and works in Switzerland for a credit card payment acquiring company.

Comments and Discussions

 
QuestionLoadCache method has a bug, that's why you wait 2 seconds (for every 5 seconds in your example). Pinmemberacincinar28-Jan-13 12:30 
GeneralWrong integration of the cache PinmemberMember 241422717-Feb-10 5:30 
GeneralRe: Wrong integration of the cache PinmemberPaolo Costa17-Feb-10 23:49 
GeneralRe: Wrong integration of the cache PinmemberPaolo Costa19-Feb-10 22:00 
GeneralEdit PinmemberJohn Simmons / outlaw programmer16-Feb-10 12:04 
GeneralRe: Edit PinmemberPaolo Costa16-Feb-10 13:49 
GeneralRe:Repost updated pattern. PinmemberJan Palmer2-Jul-10 7:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141216.1 | Last Updated 6 Oct 2014
Article Copyright 2010 by Paolo Costa
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid