C# Dictionary cache that has a timeout on it’s values


So I have been playing with Caching, and the need to get several objects types in an application that works in both service and form applications. This led to the dropping of the System.Web HTTP cache class as that needs all sorts of things to make the instance objects work.

I also needed it to work without having to instantiate a background thread to perform timing checks etc. but this led to stale objects being left in the cache until something requested them. That was not ideal (Think of a search application going through all your files and then never looking again !)

So I have come up with a class that allows 3 scenarios that can be driven in whatever fashion is required. If the user of the class wants a watcher thread then, they can create one and call the CheckStaleness function; If the user wants the staleness of the objects to be tested on each API call that finds a stale object, then that is possible via a constructor option; And if they want it to be just idle and keep the objects until they are “Tested” then that can be done as well.

It also has an extra features where :

  • an object can be locked into the cache, so that it is not removed until either the lock is freed,
  • Use of Touch to give an object another “Lifecycle” offset from the function caller time before it becomes stale.

Here is the class: “http://amalgam.codeplex.com/SourceControl/changeset/view/66404#1467213”

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace AmalgamClientTray.Dokan
{
   /// <summary>
   /// stolen from the discussions in http://blogs.infosupport.com/blogs/frankb/archive/2009/03/15/CacheDictionary-for-.Net-3.5_2C00_-using-ReaderWriterLockSlim-_3F00_.aspx
   /// And then made it more useable for the cache timeout implementation.
   /// I did play with the ConcurrentDictonary, but this made the simplicity of using a Mutex and a normal dictionary very difficult to read.
   /// </summary>
   /// <example>
   /// Use it like a dictionary and then add the functions required
   /// </example>
   /// <remarks>
   /// Does not implement all the interfaces of IDictionary.
   /// All Thread access locking is performed with this object, so no need for access locking by the caller.
   /// </remarks>
   public class CacheHelper<TKey, TValue>
   {
      #region private fields
      private readonly bool useAPICallToRelease;
      private readonly object cacheLock = new object();
      private class ValueObject<TValueObj>
      {
         private DateTimeOffset Created;
         public readonly TValueObj CacheValue;

         public ValueObject(uint expireSeconds, TValueObj value)
         {
            Created = new DateTimeOffset(DateTime.Now).AddSeconds(expireSeconds);
            CacheValue = value;
         }

         public bool IsValid
         {
            get
            {
               return (Lock
                  || (Created > DateTime.Now)
                  );
            }
         }

         public void Touch(uint expireSeconds)
         {
            Created = new DateTimeOffset(DateTime.Now).AddSeconds(expireSeconds);
         }

         public bool Lock { private get; set; }

      }

      private readonly uint expireSeconds;
      private readonly Dictionary<TKey, ValueObject<TValue>> Cache = new Dictionary<TKey, ValueObject<TValue>>();

      #endregion

      /// <summary>
      /// Constructor with the timout value
      /// </summary>
      /// <param name="expireSeconds">timeout cannot be -ve</param>
      /// <param name="useApiCallToRelease">When an function call is made then it will go check the staleness of the cache</param>
      /// <remarks>
      /// expiresecounds must be less than 14 hours otherwise the DateTimeOffset for each object will throw an exception
      /// </remarks>
      public CacheHelper(uint expireSeconds, bool useApiCallToRelease = true)
      {
         this.expireSeconds = expireSeconds;
         useAPICallToRelease = useApiCallToRelease;
      }

      /// <summary>
      /// Value replacement and retrieval
      /// </summary>
      /// <param name="key"></param>
      /// <returns></returns>
      public TValue this[TKey key]
      {
         get
         {
            lock (cacheLock)
            {
               ValueObject<TValue> value = Cache[key];
               if (value != null)
               {
                  if (value.IsValid)
                     return value.CacheValue;
                  // else
                  {
                     Cache.Remove(key);
                     if (useAPICallToRelease)
                        ThreadPool.QueueUserWorkItem(CheckStaleness);
                  }
               }
            }
            throw new KeyNotFoundException();

            // return default(TValue);
         }
         set
         {
            lock (cacheLock)
            {
               Cache[key] = new ValueObject<TValue>(expireSeconds, value);
            }
         }
      }

      /// <summary>
      /// Go through the cache and remove the stale items
      /// </summary>
      /// <remarks>
      /// This can be called from a thread, and is used when the useAPICallToRelease is true in the constructor
      /// </remarks>
      /// <param name="state">set to null</param>
      public void CheckStaleness(object state)
      {
         lock (cacheLock)
         {
            try
            {
               foreach (var i in Cache.Where(kvp => ((kvp.Value == null) || !kvp.Value.IsValid)).ToList())
               {
                  Cache.Remove(i.Key);
               }
            }
            catch { }
         }
      }

      /// <summary>
      /// Does the value exist at this key that has not timed out ?
      /// </summary>
      /// <param name="key"></param>
      /// <param name="value"></param>
      /// <returns></returns>
      public bool TryGetValue(TKey key, out TValue value)
      {
         lock (cacheLock)
         {
            ValueObject<TValue> valueobj;
            if (Cache.TryGetValue(key, out valueobj))
            {
               if (valueobj.IsValid)
               {
                  value = valueobj.CacheValue;
                  return true;
               }
               // else
               {
                  Cache.Remove(key);
                  if (useAPICallToRelease)
                     ThreadPool.QueueUserWorkItem(CheckStaleness);
               }
            }
         }

         value = default(TValue);
         return false;
      }

      /// <summary>
      /// Remove the value
      /// </summary>
      /// <param name="key"></param>
      public void Remove(TKey key)
      {
         lock (cacheLock)
         {
            Cache.Remove(key);
         }
      }

      /// <summary>
      /// Used to prevent an object from being removed from the cache;
      /// e.g. when a file is open
      /// </summary>
      /// <param name="key"></param>
      /// <param name="state"></param>
      /// <returns></returns>
      public void Lock(TKey key, bool state)
      {
         lock (cacheLock)
         {
            ValueObject<TValue> valueobj = Cache[key];
            valueobj.Lock = state;
            // If this is unlocking then assume that the target object will "Allowed" to be around for a while
            if (!state)
               valueobj.Touch(expireSeconds);
         }
      }
   }
}
Advertisements

One thought on “C# Dictionary cache that has a timeout on it’s values

  1. When I initially commented I clicked the “Notify me when new comments are added” checkbox and now each time a comment is added I get several e-mails with the same comment. Is there any way you can remove people from that service? Bless you!Plano Roofing Pros, 3420 14th Street, #103-C, Plano, TX 75074 – (214) 556-5050

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