[NoBrainer] Strongly typed caching with System.Runtime.Caching.MemoryCache

Caching with C# has become easy since the MemoryCache component is included in the .NET framework. While this implementation provides only limited functionality it is actually quite easy to make it strongly typed and support for constructing unique cache keys per cache group. In this post I will show you how this can be done with the help of a few wrapper classes.

Requirements

Before we start let’s first define some requirements our cache manager should fulfill:

  1. Getting items from the cache and adding them if not present
  2. Setting (and potentially overwriting) items in the cache
  3. Removing items from the cache
  4. Support for fixed and sliding expiration per cache group
  5. Easy initialisation
  6. Creating unique cache keys for cache group
  7. Optional locking on GetOrAdd operations

Usage

Retrieving something from the cache via a key (42L in this case) should return it or add something to it via a value factory. The cache item type is inferred from the return type of the Func value factory.

var stringItem1 = _cache1.Get(42L, () => 42L.ToString());
// same as:
var stringItem2 = _cache1.Get<string>(42L, () => 42L.ToString());

However if we need multiple cache entries for the same id (or key) this should also be supported: In the following case the cache manager constructs a different cache key though we specified the same id the Get method. This works as we specified a different cache settings class upon initialisation of this cache instance, as we will see later.

var floatItem = _cache2.Get(42L, () => (float) 42L * 42L);

Removing should be equally easy:

_cache2.Remove(42L);

And initialisation of our cache should be as easy as that:

var cache1 = CacheManager<Cache1Settings>.GetInstance();

the cache instance will get its settings from the specified cache settings class and construct a unique prefix of cache keys per cache settings class.

Our settings class would then derive from either CacheManagerSettingsSlidingExpirationBase to support a sliding cache eviction policy or CacheManagerSettingsAbsoluteExpirationBase to support a fixed cache eviction policy. For example, the following settings class would define a sliding expiration of 60 seconds:

public class Cache1Settings 
  : CacheManagerSettingsSlidingExpirationBase
{
  public Cache1Settings(60)
    : base(60)
  {
    // N/A
  }
}

Instantiating and configuring the cache settings can further be automated and simplified with StructureMap as I will show later.

Cache Keys

As described before cache keys are partially derived from the value specified in the Get method and partially derived from the underlying cache settings class. With this it is possible to cache different items under the same id as long as different cache setting classes are used for instantiating the cache.

Internally the CacheManager will construct a cache key made up by the full name of the cache settings class and the stringified id supplied in the Get method.

Implementation

Implementing the cache manager comes in two parts:

  1. Instantiating the cache manager
  2. Implementing the accessor and setter methods of a cache instance

Cache Manager Instantiation

We define a non-generic base class that holds a map of all registered cache instances. We later on use this map in e generic class with an instance factory to retrieve an already registered instance or create a new one:

public class CacheManagerBase
{
  protected const char CACHE_KEY_DELIMITER = '.';

  protected static readonly object _SyncRoot = new object();

  protected static readonly MemoryCache _MemoryCache = MemoryCache.Default;

  protected static readonly ConcurrentDictionary<Type, ICacheManagerSettings> _Map =
    new ConcurrentDictionary<Type, ICacheManagerSettings>();
}

Then we define a generic class for serving cache items. This class is also responsible for returning cache instances (as previously described):

public class CacheManager<TSettings> : CacheManagerBase
  where TSettings : ICacheManagerSettings
{
  // we want this lock only for the current generic class<T>
  // ReSharper disable once StaticMemberInGenericType
  private static readonly object _syncRoot = new object();

  protected CacheManager()
  {
    // N/A
  }

  public static CacheManager<TSettings> GetInstance()
  {
    if (_Map.ContainsKey(typeof(TSettings)))
    {
      return new CacheManager<TSettings>();
    }

    lock (_SyncRoot)
    {
      if (_Map.ContainsKey(typeof(TSettings)))
      {
        return new CacheManager<TSettings>();
      }

      Contract.Assert(!typeof(TSettings).IsAbstract);
      Contract.Assert(!typeof(TSettings).IsInterface);

      var settings = IoC.IoC.DefaultContainer.GetInstance<TSettings>();
      Contract.Assert
      (
        settings is ICacheManagerSettingsAbsoluteExpiration
        ||
        settings is ICacheManagerSettingsSlidingExpiration
      );

      var isCacheSettingsTypeAddedToCache = _Map.TryAdd(typeof(TSettings), settings);
      Contract.Assert(isCacheSettingsTypeAddedToCache, typeof(TSettings).FullName);

      return new CacheManager<TSettings>();
    }
  }

The interface for TSettings is needed to differentiate between sliding and absolute expiration and is a simple as that:

public interface ICacheManagerSettings
{
  bool IsSlidingExpiration { get; set; }
}

Our settings classes then determine the actual cache eviction timeout and add some helper code to facilitate the instantiation:

Absolute Expiration

public abstract class CacheManagerSettingsAbsoluteExpirationBase : ICacheManagerSettingsAbsoluteExpiration
{
  private const string VIRTUAL_MEMBER_CALL_IN_CONSTRUCTOR = "You may only either override property ExpirationInSeconds or use protected .ctor(long expirationInSeconds).";
  private const BindingFlags BINDING_FLAGS = BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy;

  public bool IsSlidingExpiration { get; set; } = false;

  public virtual long ExpirationInSeconds { get; set; } = long.MaxValue;

  protected CacheManagerSettingsAbsoluteExpirationBase(long expirationInSeconds)
  {
    var propertyInfo = this.GetType().GetProperty(nameof(ExpirationInSeconds), BINDING_FLAGS);
    Contract.Assert(null != propertyInfo);

    Contract.Assert(propertyInfo.DeclaringType == typeof(CacheManagerSettingsAbsoluteExpirationBase), VIRTUAL_MEMBER_CALL_IN_CONSTRUCTOR);

    // ReSharper disable once VirtualMemberCallInConstructor
    ExpirationInSeconds = expirationInSeconds;
  }

  protected CacheManagerSettingsAbsoluteExpirationBase()
  {
    // N/A
  }
}

Sliding Window

public abstract class CacheManagerSettingsSlidingExpirationBase : ICacheManagerSettingsSlidingExpiration
{
  private const string VIRTUAL_MEMBER_CALL_IN_CONSTRUCTOR = "You may only either override property CacheItemPolicy or use protected .ctor(long slidingExpirationInSeconds).";
  private const BindingFlags BINDING_FLAGS = BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy;

  protected CacheManagerSettingsSlidingExpirationBase(long slidingExpirationInSeconds)
  {
    Contract.Requires(0 < slidingExpirationInSeconds);

    var propertyInfo = this.GetType().GetProperty(nameof(CacheItemPolicy), BINDING_FLAGS);
    Contract.Assert(null != propertyInfo);

    Contract.Assert(propertyInfo.DeclaringType == typeof(CacheManagerSettingsSlidingExpirationBase), VIRTUAL_MEMBER_CALL_IN_CONSTRUCTOR);

    // ReSharper disable once VirtualMemberCallInConstructor
    CacheItemPolicy = new CacheItemPolicy
    {
      SlidingExpiration = TimeSpan.FromSeconds(slidingExpirationInSeconds),
    };
  }

  protected CacheManagerSettingsSlidingExpirationBase()
  {
    // N/A
  }

  public bool IsSlidingExpiration { get; set; } = true;

  public virtual CacheItemPolicy CacheItemPolicy { get; set; } = new CacheItemPolicy
  {
    SlidingExpiration = ObjectCache.NoSlidingExpiration,
  };
}

Cache Manager Operations

Now that we have everything in place we need to define our cache operations. We define instrinsic methods for cache keys of the following types (but can add any further types as we require):

T Get<T>(long id, bool useLock = false);
T Get<T>(long id, Func<T> valueFactory, bool useLock = false);
T Get<T>(string key, bool useLock = false);
T Get<T>(string key, Func<T> valueFactory, bool useLock = false);
T Get<T>(Guid key, bool useLock = false);
T Get<T>(Guid key, Func<T> valueFactory, bool useLock = false);
T Get<T>(Type key, bool useLock = false);
T Get<T>(Type key, Func<T> valueFactory, bool useLock = false);
void Set<T>(long key, T value);
void Set<T>(string key, T value);
void Set<T>(Guid key, T value);
void Set<T>(Type key, T value);
T Remove<T>(long key);
T Remove<T>(string key);
T Remove<T>(Guid key);
T Remove<T>(Type key);

We will show tha actual implementation for the string type:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected virtual string GetCacheKey<T>(string key)
{
  return string.Concat(typeof(TSettings).FullName, CACHE_KEY_DELIMITER, typeof(T).FullName, CACHE_KEY_DELIMITER, key);
}

public virtual T Get<T>(string key, bool useLock = false)
{
  Contract.Requires(!string.IsNullOrWhiteSpace(key));

  return GetInternal<T>(GetCacheKey<T>(key), default(Func<T>), typeof(TSettings), useLock);
}

private static T GetInternal<T>(string cacheKey, Func<T> valueFactory, Type settingsType, bool useLock = false)
{
  if (null == valueFactory)
  {
    return (T) _MemoryCache[cacheKey];
  }

  T result;
  Func<T> internaValueFactory = () =>
  {
    if (_MemoryCache.Contains(cacheKey))
    {
      return (T) _MemoryCache[cacheKey];
    }

    var value = valueFactory();
    if (null == value)
    {
      return default(T);
    }

    var settings = _Map[settingsType];
    if (settings.IsSlidingExpiration)
    {
      var policy = ((ICacheManagerSettingsSlidingExpiration) settings).CacheItemPolicy;
      _MemoryCache.Set(cacheKey, value, policy);
    }
    else
    {
      var expirationInSeconds = ((ICacheManagerSettingsAbsoluteExpiration) settings).ExpirationInSeconds;
      var expiration = long.MaxValue != expirationInSeconds
        ? DateTimeOffset.Now.AddSeconds(expirationInSeconds)
        : ObjectCache.InfiniteAbsoluteExpiration;
      _MemoryCache.Set(cacheKey, value, expiration);
    }

    return value;
  };

  if (useLock)
  {
    lock (_syncRoot)
    {
      result = internaValueFactory();
    }
  }
  else
  {
    result = internaValueFactory();
  }

  return result;
}

We actually do all the work in the GetInternal method and only use the public Get method to construct the cache key. Optional locking is possible but seldomly required.

Setting and removing is even easier:

Setting items

public virtual void Set<T>(string key, T value)
{
  Contract.Requires(!string.IsNullOrWhiteSpace(key));
  Contract.Requires(null != value);

  SetInternal(GetCacheKey<T>(key), value, typeof(TSettings));
}

private static void SetInternal<T>(string cacheKey, T value, Type settingsType)
{
  var settings = _Map[settingsType];
  if (settings.IsSlidingExpiration)
  {
    var policy = ((ICacheManagerSettingsSlidingExpiration)settings).CacheItemPolicy;
    _MemoryCache.Set(cacheKey, value, policy);

    return;
  }

  var expirationInSeconds = ((ICacheManagerSettingsAbsoluteExpiration) settings).ExpirationInSeconds;
  var expiration = long.MaxValue != expirationInSeconds
    ? DateTimeOffset.Now.AddSeconds(expirationInSeconds)
    : ObjectCache.InfiniteAbsoluteExpiration;
  _MemoryCache.Set(cacheKey, value, expiration);
}

Removing items

public virtual T Remove<T>(string key)
{
  Contract.Requires(!string.IsNullOrWhiteSpace(key));

  return RemoveInternal<T>(GetCacheKey<T>(key));
}

private static T RemoveInternal<T>(string cacheKey)
{
  return (T) _MemoryCache.Remove(cacheKey);
}

Configuring CacheManager Settings

To further facilitate the configuration of cache settings we can use a mixture of ConfigurationSections and StructureMap. The approach could be the following:

  1. Use a configuration section and settings from app.config to load settings
  2. Use defaults defined in application constants if no settings are defined
  3. Use these settings as constructor arguments for the respective cache settings class

Keeping all of our default cache timeout settings could be achieved by having a common constants class:

public static class CacheSettings
{
  public const string SECTION_NAME = "cacheManagerSettingsConfiguration";

  public const long DEFAULT_TIMEOUT = 60;

  // we use lower camel case for these constants as they are used as XML attributes 
  // inside app.configconfiguration section

  // ReSharper disable InconsistentNaming
  public const long defaultCacheManagerSettingsTimeout = DEFAULT_TIMEOUT;
  public const long modelCacheManagerSettingsTimeout = DEFAULT_TIMEOUT;
  // ReSharper restore InconsistentNaming
}

Mapping these values to a configuration section is a breeze since C# 6.0:

public class CacheSettingsConfigurationSection
  : ConfigurationSection
{
  //<configuration>

  //<!-- Configuration section-handler declaration area. -->
  //  <configSections>
  //    <section 
  //      name="cacheManagerSettingsConfiguration" 
  //      type="Net.Appclusive.Core.Cache.CacheSettingsConfigurationSection, Net.Appclusive.Core" 
  //      allowLocation="true" 
  //      allowDefinition="Everywhere" 
  //    />
  //      <!-- Other <section> and <sectionGroup> elements. -->
  //  </configSections>

  //  <!-- Configuration section settings area. -->
  //  <cacheManagerSettingsConfiguration modelCacheManagerSettingsTimeout="60" />

  //</configuration>

  [ConfigurationProperty(
    nameof(CacheSettings.defaultCacheManagerSettingsTimeout), 
    DefaultValue = CacheSettings.defaultCacheManagerSettingsTimeout, 
    IsRequired = false
  )]
  public long DefaultCacheManagerSettingsTimeout
  {
    get { return (long)this[nameof(CacheSettings.defaultCacheManagerSettingsTimeout)]; }
    set { this[nameof(CacheSettings.defaultCacheManagerSettingsTimeout)] = value; }
  }
}

Without any need for duplication we reference the constant settings by using nameof. Loading the configuration section is as easy as defining a singleton mapping as this:

For<CacheSettingsConfigurationSection>()
  .Use(() => (CacheSettingsConfigurationSection) ConfigurationManager.GetSection(CacheSettings.SECTION_NAME))
  .Singleton();

Summary

With some overloads and a generic base class we can enhance the builtin .NET MemoryCache with added functionality such as strong typing and much simpler interfaces for accessing and setting cache items.

The Code

The complete CacheManager can be found here: https://gist.github.com/dfch/5623b1f9dd36a13b40696f6928f42c72

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

%d bloggers like this: