Caching of the last N objects

   —   
When developing new controls, modules or web parts, you should always think about performance and caching. But sometimes you are unable to create caching dependencies that cover your scenario, so you need to use an alternative approach to keep the most recent objects in the cache and to remove old ones before the cache expires. One of the possible approaches is discussed in this post, in which we implement a method that caches the last N requested objects. 
This is one of the possible approaches to keeping the cached data small while keeping the most recently requested data cached. We want to implement a method that will automatically cache the last N instances based on the input variables of a given object. Let’s say we have a scenario in which we want to cache a TreeNode object. Our variables are the DocumentID and the UserID, because each user has a different TreeProvider. We will use the cache key (cache item name) to identify the different objects. So the variable needs to be generated from the input parameters (UserID and DocumentID, in this example). Our cache item name can look like this:

last_n_<UserID>_<DocumentID>

This naming convention will generate a unique name for the cached object for each User-Document combination.

The method should be generic enough to be called for any object. So it could like this:

/// <summary>
    /// Caches the last N objects based on the time (last accessed time)
    /// </summary>
    /// <param name="lastN">Number of objects to cache</param>
    /// <param name="time">The time in minutes</param>
    /// <param name="method">The method getting the object</param>
    /// <param name="methodArgs">Parameters of the method (they are also used to identify the objects and generate the cache key)</param>
    /// <returns>Cached or retrieved object</returns>
    static object CacheLastN(int lastN, int time, Delegate method, params object[] methodArgs)

The method parameter is used for the method which gets the data from the database, if the object isn’t cached. The methodArgs are the arguments of the method passed via the method parameter. They are also used to create the cache item name. In our case the method would be called the following way:

TreeNode currentDocument = (TreeNode)CacheLastN(10, 60, new Func<int, int, TreeNode>(GetDocument), UserID, DocumentID);

This means that we want to cache the last 10 object variants for 60 minutes. The method GetDocument gets the data from the database and the UserID and DocumentID are passed as the GetDocument method parameters and are also used for the cache item name.

The GetDocument method could look like this:

static TreeNode GetDocument(int UserID, int DocumentID)
    {
        return CMS.DocumentEngine.DocumentHelper.GetDocument(DocumentID, new TreeProvider(UserInfoProvider.GetUserInfo(UserID)));
    }

The CacheLastN method needs to store the last N cached items without having to search the whole cache for objects named last_n_<something>. We can use a Dictionary for this. This also gives us the possibility of adding a DateTime value to each entry allowing us to get the oldest entry for deletion

static Dictionary<string, DateTime> LastCacheItemNames = new Dictionary<string, DateTime>();

The method CacheLastN will generate the Cache item name from the passed arguments and check if such an item is already in the cache (in our dictionary). If yes, the cache is returned and the timestamp is updated with the current time. If no, the oldest cached object is removed if we reach the N cached entries and the new item is cached. The method could look like this:

static object CacheLastN(int lastN, int time, Delegate method, params object[] methodArgs)
    {
        // now time
        DateTime nowTime = DateTime.Now;
 
        //Generating an unique cache item name for the cached data
        string safeCacheItemName = "last_n_";
 
        foreach (object argument in methodArgs)
        {
            // cache keys are always lower case
            safeCacheItemName += ValidationHelper.GetCodeName(argument).Replace('.', '_').ToLower() + "_";
        }
 
        safeCacheItemName = safeCacheItemName.TrimEnd('_');
 
        // if the cache item is available, update the date time stamp
        if (LastCacheItemNames.ContainsKey(safeCacheItemName))
        {
            LastCacheItemNames[safeCacheItemName] = nowTime;
        }
        else
        {
            // remove the oldest item if we have already N items
            if (LastCacheItemNames.Count >= lastN)
            {
                string removedItemKey = LastCacheItemNames.OrderBy(key => key.Value).First().Key;
                LastCacheItemNames.Remove(removedItemKey);
 
                //remove it from the cache
                CacheHelper.TouchKey(removedItemKey);
            }
 
            // add the new item
            LastCacheItemNames.Add(safeCacheItemName, nowTime);
        }
 
        // let our cache method handle the retrieval and creation of the cache
        return CacheHelper.Cache(cs => method.DynamicInvoke(methodArgs), new CacheSettings(time, safeCacheItemName));
    }

If you want to you can now check the cached objects in our Debug module under Cache items:



This is of course only an example and the code can be expanded. For example, you can take the frequency of the usage of the cached items into account and add a score value for each item. The lower the score, the less likely the item will be used and you can remove it from the cache. 

UPDATE: The proposed solution may not work in a multi threaded environment, so you should consider adding object locking, or use a threadsafe dictionary instead (also, use TryGetValue in your code for your dictionary).


Share this article on   LinkedIn

Boris Pocatko

Senior Solution Architect for Kentico

Comments