Sunday, 27 September 2020

Generic Memory Cache for .Net Framework

The following sample code shows how to create a Generic Memory Cache for .Net Framework. This allows you to cache specific items defined by a TCacheItemData type argument, i.e. caching same type of data such as instances of a class, or arrays of instances. Inside your .csproj you should see something like:
    
Over to the implementation. Since a memory cache is shared by possibly other applications, it is important to prefix your cached contents, i.e. prefix the the keys. This makes it easier to barrier the memory cache. Note though that some barriering is done accross processes of course, this is just to make it easier within your application and running process to group the cached elements with a prefix key used for the generic memory cache operations. Now over to the implementation.

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

namespace SomeAcme.SomeUtilNamespace
{
    /// <summary>
    /// Thread safe memory cache for generic use
    /// </summary>
    /// <typeparam name="TCacheItemData">Payload to store in the memory cache</typeparam>
    /// <remarks>Uses MemoryCache.Default which defaults to an in-memory cache. All cache items are prefixed with an 'import cache session guid' to compartmentalize
    /// multiple paralell importing sessions</remarks>
    public class GenericMemoryCache<TCacheItemData> where TCacheItemData : class
    {
        private readonly string _prefixKey;
        private readonly ObjectCache _cache;
        private readonly CacheItemPolicy _cacheItemPolicy;

        public GenericMemoryCache(string prefixKey, int defaultExpirationInSeconds = 0)
        {
            defaultExpirationInSeconds = Math.Abs(defaultExpirationInSeconds); //checking if a negative value was passed into the constructor.

            _prefixKey = prefixKey;
            _cache = MemoryCache.Default;
            _cacheItemPolicy = defaultExpirationInSeconds == 0
                ? new CacheItemPolicy { Priority = CacheItemPriority.NotRemovable }
                : new CacheItemPolicy
                { AbsoluteExpiration = DateTime.Now.AddSeconds(Math.Abs(defaultExpirationInSeconds)) };
        }

        /// <summary>
        /// Cache object if direct access is desired
        /// </summary>
        public ObjectCache Cache => _cache;

        public string PrefixKey(string key) => $"{_prefixKey}_{key}";


        /// <summary>
        /// Adds an item to memory cache
        /// </summary>
        /// <param name="key"></param>
        /// <param name="itemToCache"></param>
        /// <returns></returns>
        public bool AddItem(string key, TCacheItemData itemToCache)
        {
            try
            {
                if (!key.StartsWith(_prefixKey))
                    key = PrefixKey(key);

                var cacheItem = new CacheItem(key, itemToCache);
                _cache.Add(cacheItem, _cacheItemPolicy);
                return true;
            }
            catch (Exception err)
            {
                Debug.WriteLine(err);
                return false;
            }
        }

        public virtual List<T> GetValues<T>()
        {
            List<T> list = new List<T>();
            IDictionaryEnumerator cacheEnumerator = (IDictionaryEnumerator)((IEnumerable)_cache).GetEnumerator();

            while (cacheEnumerator.MoveNext())
            {
                if (cacheEnumerator.Key == null)
                    continue;
                if (cacheEnumerator.Key.ToString().StartsWith(_prefixKey))
                    list.Add((T)cacheEnumerator.Value);
            }
            return list;
        }

        /// <summary>
        /// Retrieves a cache item. Possible to set the expiration of the cache item in seconds. 
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public TCacheItemData GetItem(string key)
        {
            try
            {
                if (!key.StartsWith(_prefixKey))
                    key = PrefixKey(key);
                if (_cache.Contains(key))
                {
                    CacheItem cacheItem = _cache.GetCacheItem(key);
                    object cacheItemValue = cacheItem?.Value;
                    UpdateItem(key, cacheItemValue as TCacheItemData);
                    TCacheItemData item = _cache.Get(key) as TCacheItemData;
                    return item;
                }
                return null;
            }
            catch (Exception err)
            {
                Debug.WriteLine(err);
                return null;
            }
        }

        public bool SetItem(string key, TCacheItemData itemToCache)
        {
            try
            {
                if (!key.StartsWith(_prefixKey))
                    key = PrefixKey(key);
                if (GetItem(key) != null)
                {
                    AddItem(key, itemToCache);
                    return true;
                }

                UpdateItem(key, itemToCache);
                return true;
            }
            catch (Exception err)
            {
                Debug.WriteLine(err);
                return false;
            }
        }


        /// <summary>
        /// Updates an item in the cache and set the expiration of the cache item 
        /// </summary>
        /// <param name="key"></param>
        /// <param name="itemToCache"></param>
        /// <returns></returns>
        public bool UpdateItem(string key, TCacheItemData itemToCache)
        {
            if (!key.StartsWith(_prefixKey))
                key = PrefixKey(key);
            CacheItem cacheItem = _cache.GetCacheItem(key);
            if (cacheItem != null)
            {
                cacheItem.Value = itemToCache;
                _cache.Set(key, itemToCache, _cacheItemPolicy);
            }
            else
            {
                //if we cant find the cache item, just set the cache directly
                _cache.Set(key, itemToCache, _cacheItemPolicy);

            }
              return true;
           
        }

        /// <summary>
        /// Removes an item from the cache 
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public bool RemoveItem(string key)
        {
            if (!key.StartsWith(_prefixKey))
                key = PrefixKey(key);

            if (_cache.Contains(key))
            {
                _cache.Remove(key);
                return true;
            }
            return false;
        }

        public void AddItems(Dictionary<string, TCacheItemData> itemsToCache)
        {
            foreach (var kvp in itemsToCache)
                AddItem(kvp.Key, kvp.Value);
        }

        /// <summary>
        /// Clear all cache keys starting with known prefix passed into the constructor.
        /// </summary>
        public void ClearAll()
        {
            var cacheKeys = _cache.Select(kvp => kvp.Key).ToList();
            foreach (string cacheKey in cacheKeys)
            {
                if (cacheKey.StartsWith(_prefixKey))
                    _cache.Remove(cacheKey);
            }
        }

    }
}