In-memory Data Caching – few insights.

The content below assumes single Web server hosting the web application. I’m using Framework 4.0, WebForms and System.Runtime.Caching namespace’s MemoryCache class (the in-memory cache provider) meaning cache is stored in web-server’s memory. Any data or complex business object (created with data from multiple sources or after complex calculations) and very often used, can be cached. Cache by default is application scoped although based on the key can be scoped to a more granular level.Cache takes up server memory, but as against session, ASP.Net can invalidate cache and free that memory anytime. Hence it’s better to save in Cache for a small amount of time, serve that data to as many requests as possible and then release it, thereby increasing the scalability when load increases. Items in the cache can expire (will be removed from cache by ASP.Net for various reasons) and not guaranteed to be found all the time. It’s advisable to consider cache as an option, only when the performance is really affected, the page appears noticeably slow and the bottleneck has been identified.

Few examples now:
1:  Let’s say I have a page, which is loaded very often and the page does some expensive calculations on the data and then saves it to DB. Trying to use cache in this dummy example. Now when the page is
requested, if the cache is empty, I retrieve data from DB, run the calculations and save it in cache say for 10 secs. Now when multiple users update/insert data, the cache gets updated. When the cache is about to expire, it raises a callback and posts the complete set of changes to database. So for high multiple requests on this page, lot of database SQLs are saved.Here’s a custom generic cache class and a business-layer using that class.

Generic Cache Class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Caching;

public class GenericCacheClass
{
    public static readonly ObjectCache cache = MemoryCache.Default;
    private static CacheEntryUpdateCallback callbackU = null;
    public static void AddToMyCache(string CacheKeyName,
                            Object CacheItem,
                            CacheItemPolicy policy, TimeSpan UpTimeInSecs)
    {
        callbackU = new CacheEntryUpdateCallback(MyCachedItemUpdatedCallback);
        policy = new CacheItemPolicy();
        policy.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(UpTimeInSecs.Seconds);
        policy.UpdateCallback = callbackU;
        //Set will overwrite if the key already present, 
        //else will insert it.
        cache.Set(CacheKeyName, CacheItem, policy);
    }
    
    private static void MyCachedItemUpdatedCallback(CacheEntryUpdateArguments arguments)
    {
        if (arguments.RemovedReason == CacheEntryRemovedReason.Expired)
        {
            //Post the changes to DB and 
            //then remove the item from cache.
            BusinessLayer.PostToDB(cache[arguments.Key]);
        }
    }

    public void RemoveFromCache(string key)
    {
        cache.Remove(key);
    }
    public static bool IsKeyPresent(string key)
    {
        return cache.Get(key) != null;
    }
    public static CacheItem GetCacheItem(string key)
    {
        return cache.GetCacheItem(key);
    }
    public static List<string> GetAllItems()
    {
        return cache.Select(kv => kv.Key).ToList();
    }
    public static List<string> GetAllKeys()
    {
        return cache.Select(kv => kv.Key).ToList();
    }
}
public class CustomerAddress
{
    public int AddressID { get; set; }
    public string AddressLine1 { get; set; }
    public string City { get; set; }
}

Business Class using the Cache class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data;
using System.Runtime.Caching;

public class BusinessLayer
{
public static IEnumerable<CustomerAddress> GetCustomerAddress()
{
    DataTable dt = new DataTable();
    //Check if present in cache
    var _CustomerAddress = GenericCacheClass.cache.GetCacheItem("AdminRole") as CacheItem;

    // If not in cache, fetch from DB
    if (_CustomerAddress == null)
    {
        dt = DataLayer.getAllAddress();
        if (_CustomerAddress is object)
        {
            //Add it in cache for 10 secs.
            GenericCacheClass.AddToMyCache("AdminRole", 
                        dt, 
                        new CacheItemPolicy(), 
                        TimeSpan.FromSeconds(10));
        }
    }
    IEnumerable<DataRow> var1 = (IEnumerable<DataRow>)dt.AsEnumerable();
    return ConvertToCustomerAddress(dt);
}

private static IEnumerable<CustomerAddress> ConvertToCustomerAddress(DataTable dataTable)
{
    foreach (DataRow row in dataTable.Rows)
    {
        yield return new CustomerAddress
        {
            AddressID = Convert.ToInt32(row["AddressID"]),
            AddressLine1 = row["AddressLine1"].ToString(),
            City = row["City"].ToString(),
        };
    }
}

private DataTable GetCustomerAddressDt()
{
    DataTable dt = new DataTable();
    //Check cache
    var _CustomerAddress = GenericCacheClass.cache.GetCacheItem("AdminRole") as CacheItem;

    // If not in cache, fetch from DB
    if (_CustomerAddress == null)
    {
        dt = DataLayer.getAllAddress();
        if (_CustomerAddress is object)
        {
            GenericCacheClass.AddToMyCache("AdminRole", 
                                    dt, 
                                    new CacheItemPolicy(), 
                                    TimeSpan.FromSeconds(10));
        }
    }
    return dt;
}

public void UpdateAddressPresentInCache(DataRow dr)
{
    DataTable dt = GetCustomerAddressDt();
    DataRow[] RowToUpdate;
    RowToUpdate = dt.Select("AddressID = "+dr["AddressID"].ToString());
    if (RowToUpdate.Count() > 0)
    {
        RowToUpdate[0]["AddressLine1"] = dr["AddressLine1"].ToString();
        RowToUpdate[0]["City"] = dr["City"].ToString();
    }
    GenericCacheClass.AddToMyCache("AdminRole", dt, new CacheItemPolicy(), TimeSpan.FromSeconds(10));
}

public static void PostToDB(object dt)
{
    //The cache is about to expire.
    //Post all the changes done in Address DataTable as present in Cache to DB
    //Your code to UpdateDB
}

}

Codebehind:

private void BindRepeater()
    {
        Repeater1.DataSource = BusinessLayer.GetCustomerAddress();
        Repeater1.DataBind();
    }

2: Cache can be used as an alternative to ViewState.
Say as an example, I have a heavy complex page which uses 4 or 5 UserControls. On the Page Load I fetch a big DataSet containing multiple resultsets from the DB. Now using that Dataset, I can create some
custom objects holding different set of processed data or just plain individual DataTables. When the page postbacks to itself, if required, all those 4/5 user controls can grab data from the cache instead of ViewState. If the data is user specific, then a user-specific key can be generated and cache-duration can be kept to just few seconds depending on that particular page’s functionality. Cache entry can be evicted based on other key-dependency too and not just absolute seconds.

3: SqlDataSource and ObjectDataSource supports built-in caching. As an example, SqlDataSource if used with DataSet can take advantage of Caching. This can be immensely helpful if the parent Databound control like ListView, GridView has pager associated with it. Then during paging, the data can be retrieved from cache. More here  http://msdn.microsoft.com/en-us/library/z56y8ksb(v=vs.100).aspx

4: PageMethods can use caching. For example:
[WebMethod(CacheDuration=90)]
public static void abc()
{
//function implementation
}

Apart from these, any kind of masterdata like country names,products, which changes in-frequently, but used often, are good candidates of caching.The cache configuration can be stored in web.config file, so the eviction times or any other property can be dynamically changed without any code-change. Summarily, caching isn’t free because it saves items in server memory. It’s a delicate balance between memory,usage and cache-eviction. If by keeping certain chunk of data in memory, brings on a big performance-boost then fine, else database is a better place to save. Also no point in reserving server memory (by using cache) for something, which will be used very rarely.

Thanks for reading.

Advertisements
This entry was posted in General ASP.Net C# and tagged . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s