0 Comments

I needed a simple caching component that could be passed around in my code and should be thread safe.

This is what I came up with (ps the code can be found on mine PTB project GitHub page https://github.com/roelvanlisdonk/PTB):

I you really want to do some fancy caching check out the Azure caching service:

http://weblogs.asp.net/scottgu/archive/2013/09/03/windows-azure-new-distributed-dedicated-high-performance-cache-service-more-cool-improvements.aspx

 

namespace PTB.Cs.Components
{
    using System;
    using System.Collections.Concurrent;

    /// <summary>
    /// Holds cached data.
    /// </summary>
    public interface ICacheComponent
    {
        /// <summary>
        /// Adds the given data to the cache (with a key based on the given type), if it does not exist.
        /// Updates the data in the cache (with a key based on the given type), if it does exists.
        /// </summary>
        void AddOrUpdateCache<T>(T value);
        /// <summary>
        /// Gets cached data base on the given type.
        /// </summary>
        T GetCachedData<T>();
        /// <summary>
        /// Gets cached data base on the given type, if the key is not found, the given func is executed to get the data.
        /// </summary>
        T GetCachedData<T>(Func<T> getData);
        /// <summary>
        /// Gets cached data base on the given type, if the key is not found, the given func is executed to get the data.
        /// </summary>
        T GetCachedData<K,T>(Func<K,T> getData, K parameter);
        /// <summary>
        /// Resets all cached data, by setting it to null.
        /// </summary>
        void ResetCache(ConcurrentDictionary<Type, object> cache = null);
        /// <summary>
        /// Resets cache for a specific item.
        /// By setting it's value to the default of the given type.
        /// </summary>
        void ResetCache<T>();
    }

    /// <summary>
    /// Holds cached data.
    /// </summary>
    public class CacheComponent : ICacheComponent
    {
        /// <summary>
        /// Holds all cached data (thread safe).
        /// It is declared as static readonly, so it will be initialised only once per appdomain and will never be null.
        /// In this case it is not a problem that is always created (lazy loading is not needed here).
        /// Using a static readonly variable ensure thread safety.
        /// </summary>
        private static readonly ConcurrentDictionary<Type, object> _cache = new ConcurrentDictionary<Type, object>();

        /// <summary>
        /// Constructor
        /// </summary>
        public CacheComponent()
            : this(null)
        {
        }

        /// <summary>
        /// Added for testability.
        /// Uses the ResetCache function to reset the cache, when needed.
        /// </summary>
        public CacheComponent(ConcurrentDictionary<Type, object> cache)
        {
            // Cache alleen resestten, indien de meegegeven cache niet leeg is.
            if (cache != null)
            {
                ResetCache(cache);
            }
        }

        /// <summary>
        /// Adds the given data to the cache (with a key based on the given type), if it does not exist.
        /// Updates the data in the cache (with a key based on the given type), if it does exists.
        /// </summary>
        public void AddOrUpdateCache<T>(T value)
        {
            object updatedValue = _cache.AddOrUpdate(typeof(T), value, (x, existingVal) => { existingVal = value; return existingVal; });
        }

        /// <summary>
        /// Gets cached data base on the given type.
        /// </summary>
        public T GetCachedData<T>()
        {
            object result;
            bool succeeded = _cache.TryGetValue(typeof(T), out result);
            return (T)result;
        }

        /// <summary>
        /// Gets cached data base on the given type, if the key is not found, the given func is executed to get the data and this data will be stored in the cache.
        /// </summary>
        public T GetCachedData<T>(Func<T> getData)
        {
            T cachedItem = GetCachedData<T>();
            if(cachedItem == null)
            {
                cachedItem = getData();
                AddOrUpdateCache(cachedItem);
            }
            return cachedItem;
        }

        /// <summary>
        /// Gets cached data base on the given type, if the key is not found, the given func is executed with the given parameter to get the data and this data will be stored in the cache.
        /// </summary>
        public T GetCachedData<K, T>(Func<K, T> getData, K parameter)
        {
            T cachedItem = GetCachedData<T>();
            if (cachedItem == null)
            {
                cachedItem = getData(parameter);
                AddOrUpdateCache(cachedItem);
            }
            return cachedItem;
        }

        /// <summary>
        /// This function is added for testability and return the total count of cached items.
        /// </summary>
        /// <returns></returns>
        public int Count()
        {
            return _cache.Count;
        }

        /// <summary>
        /// Resets all cached data, by clearing the current cache or using the given cache, when this given cache is not null.
        /// </summary>
        public void ResetCache(ConcurrentDictionary<Type, object> cache = null)
        {
            // Clear current cache.
            _cache.Clear();

            // Gebruik alleen de meegegeven cache, indien die niet leeg is.
            if (cache != null)
            {
                foreach (var item in cache)
                {
                    bool succeeded = _cache.TryAdd(item.Key, item.Value);
                    if (!succeeded)
                    {
                        throw new ApplicationException(string.Format("Could not add item with key [{0}] and value [{1}]", item.Key, item.Value));
                    }
                }
            }
        }

        /// <summary>
        /// Resets cache for a specific item.
        /// By setting it's value to the default of the given type.
        /// </summary>
        public void ResetCache<T>()
        {
            AddOrUpdateCache<T>(default(T));
        }
    }
}

 

 

namespace PTB.Cs.Test.Components
{
    using PTB.Cs.Components;
    using Xunit;

    public class CacheComponentTester
    {
        [Fact]
        public void Getting_non_existing_key_should_return_null()
        {
            var cache = new CacheComponent();
            cache.ResetCache();

            Assert.Equal(null, cache.GetCachedData<TypeForTesting>());
        }

        [Fact]
        public void Getting_non_existing_key_with_func_should_return_correct_value_and_data_must_be_stored_in_cache()
        {
            var cache = new CacheComponent();
            cache.ResetCache();

            var expectedTypeForTesting = GetDataWithoutParameter();
            TypeForTesting result = cache.GetCachedData(GetDataWithoutParameter);
            Assert.Equal(expectedTypeForTesting.Id, result.Id);
            result = cache.GetCachedData<TypeForTesting>();
            Assert.Equal(expectedTypeForTesting.Id, result.Id);

            cache.ResetCache();
            result = cache.GetCachedData(GetDataWithParameter, 100);
            Assert.Equal(expectedTypeForTesting.Id, result.Id);
            result = cache.GetCachedData<TypeForTesting>();
            Assert.Equal(expectedTypeForTesting.Id, result.Id);
        }

        [Fact]
        public void Getting_existing_key_should_return_correct_value()
        {
            var cache = new CacheComponent();
            cache.ResetCache();

            int expectedInt = 100;
            cache.AddOrUpdateCache<int>(expectedInt);
            Assert.Equal(expectedInt, cache.GetCachedData<int>());

            var expectedTypeForTesting = new TypeForTesting { Id = expectedInt };
            cache.AddOrUpdateCache<TypeForTesting>(expectedTypeForTesting);
            Assert.Equal(expectedTypeForTesting.Id, cache.GetCachedData<TypeForTesting>().Id);
        }

        [Fact]
        public void Adding_multiple_items_with_same_key_should_result_in_one_cached_item()
        {
            var cache = new CacheComponent();
            cache.ResetCache();

            int expectedInt = 100;
            cache.AddOrUpdateCache<int>(expectedInt);
            cache.AddOrUpdateCache<int>(expectedInt);
            cache.AddOrUpdateCache<int>(expectedInt);
            cache.AddOrUpdateCache<int>(expectedInt);
            cache.AddOrUpdateCache<int>(expectedInt);
            cache.AddOrUpdateCache<int>(expectedInt);
            
            Assert.Equal(1, cache.Count());
        }

        public TypeForTesting GetDataWithoutParameter()
        {
            int expectedInt = 100;
            return new TypeForTesting { Id = expectedInt };
        }

        public TypeForTesting GetDataWithParameter(int arg1)
        {
            int expectedInt = arg1;
            return new TypeForTesting { Id = expectedInt };
        }
    }

    public class TypeForTesting
    {
        public int Id { get; set; }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related Posts