2
\$\begingroup\$

I have previously attempted to write a object-cache that I use in co-junction with Redis keeping a persistent data-model. Old post can be read here Cached-object Store c# with Redis client for persistent storage

Not focusing on Redis it's the Object-cache that is the important portion, one of the questions i previously got was what if the data-model changes in Redis how will the application know about this? It wont, and at current stage this is not something that is a concern, for now I use Redis just as a dump, read / writes happen from only "this" application.

Where is this used? In the environment where this is used, application itself is a REST / RPC (json) service where multiple clients connect and 99% of the time they read and most of the time its a matter of searching through very long lists of entities, I store entities in HashSet's where i override both Equals and GetHashCode code so that the "key" of each entity is it's unique Id of each entity (in some cases a composite key). With tests of millions of entities in the HashSet's finding single/multiple entities take a few milliseconds compared to several seconds or minutes at times when model/predicate is getting complex when I use ORM's such as EF / Castle ActiveRecord / native NHibernate. I am not trying to write my own ORM, however I duplicate some of the functionality that are typically found in ORM's.

Is this looking right? Previously my code contained a huge amount of mistakes and outright wrong code.

using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Runtime.Caching;
using System.Linq;
using ServiceStack.Redis;

namespace Datamodel
{
    public class Repository
    {
        private static readonly PooledRedisClientManager m = new PooledRedisClientManager();

        readonly static ConcurrentDictionary<Type, HashSet<object>> _cache = new ConcurrentDictionary<Type, HashSet<object>>();

        /// <summary>
        /// Load {T} into Object-cache from Data Store.
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        public static void LoadIntoCache<T>() where T : class
        {
            // Lets make sure we never replace _cache[T] if key is already present. 
            if (!_cache.ContainsKey(typeof(T)))
            {
                _cache[typeof(T)] = new HashSet<object>(RedisGetAll<T>().Cast<object>().ToList());
            }
        }

        /// <summary>
        /// Ensures that given type exist in Object-cache.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static Type EnsureTypeInCache(Type type)
        {
            // Check if type exist in _cache 
            if (!_cache.ContainsKey(type))
            {
                // I am aware that this portion should contain exception handling. 
                // Key not present adding key and empty cache.
                _cache.TryAdd(type, new HashSet<object>());
            }

            return type;
        }

        /// <summary>
        /// Find First {T} in Object-cache.
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        /// <param name="predicate">linq statement</param>
        /// <returns></returns>
        public static T FindFirstBy<T>(Func<T, bool> predicate) where T : class
        {
            // Lets prevent race conditions, locking down cache.
            lock (EnsureTypeInCache(typeof(T)))
            {
                return _cache[typeof(T)].Cast<T>().Where(predicate).FirstOrDefault();
            }
        }

        /// <summary>
        /// Find Single {T} in Object-cache.
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        /// <param name="predicate">linq statement</param>
        /// <returns></returns>
        public static T FindSingleBy<T>(Func<T, bool> predicate) where T : class
        {
            // Lets prevent race conditions, locking down cache.
            lock (EnsureTypeInCache(typeof(T)))
            {
                return _cache[typeof(T)].Cast<T>().Where(predicate).SingleOrDefault();
            }
        }

        /// <summary>
        /// Add or Update entity in Object-cache and Data Store.
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        /// <param name="predicate">linq expression</param>
        /// <param name="entity">entity</param>
        public static bool AddOrUpdate<T>(T entity) where T : class
        {
            // Lets prevent race conditions, locking down cache.
            lock (EnsureTypeInCache(typeof(T)))
            {
                if (_cache[typeof(T)].Contains(entity))
                {
                    _cache[typeof(T)].Remove(entity);
                }
                _cache[typeof(T)].Add(entity);
            }

            // Redis does not care if record is new or old as it will Add or Update regardless.
            RedisStore<T>(entity);

            return true;
        }

        /// <summary>
        /// Delete single {T} from Object-cache and Data Store.
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        /// <param name="entity">class object</param>
        public static void Remove<T>(T entity) where T : class
        {
            // Lets prevent race conditions, locking down cache.
            lock (EnsureTypeInCache(typeof(T)))
            {
                if (_cache[typeof(T)].Contains(entity))
                {
                    _cache[typeof(T)].Remove(entity);
                }

                RedisDelete<T>(entity);
            }
        }

        /// <summary>
        /// Find List<T>(predicate) in Object-cache.
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        /// <param name="predicate">linq statement</param>
        /// <returns></returns>
        public static List<T> FindBy<T>(Func<T, bool> predicate) where T : class
        {
            // Lets prevent race conditions, locking down cache.
            lock (EnsureTypeInCache(typeof(T)))
            {
                return _cache[typeof(T)].Cast<T>().Where(predicate).ToList();
            }
        }

        /// <summary>
        /// Get all {T} from Object-cache.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static List<T> All<T>() where T : class
        {
            // Lets prevent race conditions, locking down cache.
            lock (EnsureTypeInCache(typeof(T)))
            {
                return _cache[typeof(T)].Cast<T>().ToList();
            }
        }

        /// <summary>
        /// Get Next Sequence for the given {T} Entity from Data Store. 
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        /// <returns>long</returns>
        public static long Next<T>() where T : class
        {
            return RedisNext<T>();
        }

        #region Redis Commands
        //
        // Following methods are ment as static private methods.
        //

        private static long RedisNext<T>() where T : class
        {
            using (var ctx = m.GetClient())
                return ctx.As<T>().GetNextSequence();
        }

        private static void RedisDelete<T>(T entity) where T : class
        {
            using (var ctx = m.GetClient())
                ctx.As<T>().Delete(entity);
        }

        private static T RedisFind<T>(long id) where T : class
        {
            using (var ctx = m.GetClient())
                return ctx.As<T>().GetById(id);
        }

        private static HashSet<T> RedisGetAll<T>() where T : class
        {
            using (var ctx = m.GetClient())
                return new HashSet<T>(ctx.As<T>().GetAll());
        }

        private static void RedisStore<T>(T entity) where T : class
        {
            using (var ctx = m.GetClient())
                ctx.Store<T>(entity);
        }

        #endregion
    }
}
\$\endgroup\$
  • \$\begingroup\$ Can multiple threads be calling into this class concurrently? \$\endgroup\$ – RobH Jun 16 '15 at 11:02
  • \$\begingroup\$ Yes multiple threads could be calling the class concurrently. (LoadIntoCache is only used when application loads and never called by new threads) \$\endgroup\$ – Joachim Jun 16 '15 at 12:00
3
\$\begingroup\$

I'm not seeing any show stopping problems. Just two minor suggestions.

Making this Testable with Unit Tests

You can still use static fields for the Redis client manager and cache, but allow this class to be instantiated and passed in. This, in conjunction with implementing an interface and allowing constructor injection can make your application as a whole testable with unit tests.

Allow constructor injection of Redis Client Manager and cache

You could make this class testable in unit tests, if you desire to. First, assuming you are using this NuGet package, you can use the IRedisClientsManager interface as the type and IDictionary for the cache then create private instance properties to access both:

public class Repository
{
    private static readonly PooledRedisClientManager _redisManager = new PooledRedisClientManager();

    readonly static ConcurrentDictionary<Type, HashSet<object>> _cache = new ConcurrentDictionary<Type, HashSet<object>>();

    private IRedisClientManager RedisManager { get; set; }
    private IDictionary<Type, HashSet<object>> Cache { get; set; }

    public Repository()
    {
        RedisManager = _redisManager;
        Cache = _cache;
    }

    public Repository(IRedisClientsManager redisManager, IDictionary<Type, HashSet<object>> cache)
    {
        RedisManager = redisManager;
        Cache = cache;
    }

That way you could use a library like Moq to throw some unit tests at this class. To further aid in the testability of the larger application, you could have your Repository class implement an interface:

public interface IRepository
{
    void LoadIntoCache<T>() where T : class;
    Type EnsureTypeInCache(Type type);
    T FindFirstBy<T>(Func<T, bool> predicate) where T : class;
    T FindSingleBy<T>(Func<T, bool> predicate) where T : class;
    bool AddOrUpdate<T>(T entity) where T : class;
    void Remove<T>(T entity) where T : class;
    List<T> FindBy<T>(Func<T, bool> predicate) where T : class;
    List<T> All<T>() where T : class;
    long Next<T>() where T : class;
}

public class Repository : IRepository
{
    ...
}

Now the code that uses this repository can be made testable, for instance, your API controller class:

public class MyApiController : ApiController
{
    private IRepository repository;

    public MyApiController()
    {
        repository = new Repository();
    }

    public MyApiController(IRepository repository)
    {
        this.repository = repository;
    }

    public IEnumerable<Foo> GetAll()
    {
        return repository.All<Foo>();
    }
}

Unnecessary check in AddOrUpdate

This snippet of code:

public static bool AddOrUpdate<T>(T entity) where T : class
{
    // Lets prevent race conditions, locking down cache.
    lock (EnsureTypeInCache(typeof(T)))
    {
        if (_cache[typeof(T)].Contains(entity))
        {
            _cache[typeof(T)].Remove(entity);
        }
        _cache[typeof(T)].Add(entity);
    }

Looks like it contains an unnecessary check to see if that key exists.

public static bool AddOrUpdate<T>(T entity) where T : class
{
    // Lets prevent race conditions, locking down cache.
    lock (EnsureTypeInCache(typeof(T)))
    {
        _cache[typeof(T)] = entity;
    }

A ConcurrentDictionary implements the IDictionary interface, so square bracket notation should work just fine.

Aside from these suggestions, this class is very clean looking and the code is easy to follow.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your reply, I'll definitely add the things you pointed out, and I should start add unit testing which I have been doing far too little in the past :) \$\endgroup\$ – Joachim Jun 16 '15 at 14:27
1
\$\begingroup\$

Here is my new version, thanks to Greg Burghardt measures was taken to make the code testable, I also changed some return values from list to IEnumerable to gain more power with defered LINQ query whenever I want to apply more logic on-top of the already ran predicate.

I also did some research finding that HashSet has a "free" Contains(T) which is why I have also added a method for this, improvement searching for a object in a huge set, lets say 400k objects by making a temp item takes a few ns compared to several ms (searching for a keyed entity) which in the end is thousands percent performance increase. However it's then important that Equals and GetHashCode is overridden with proper key logic in entities themselves.

Adding a million objects now take seconds compared to minutes when cross referencing object relations.

For now this is my final version as it currently contains every need I have at this time.

using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using ServiceStack.Redis;

namespace Datamodel
{
    public interface IRepository
    {
        void LoadIntoCache<T>() where T : class;
        Type EnsureTypeInCache(Type type);
        T FindFirstBy<T>(Func<T, bool> predicate) where T : class;
        T FindSingleBy<T>(Func<T, bool> predicate) where T : class;
        bool AddOrUpdate<T>(T entity) where T : class;
        void Remove<T>(T entity) where T : class;
        IEnumerable<T> FindBy<T>(Func<T, bool> predicate) where T : class;
        IEnumerable<T> All<T>() where T : class;
        long Next<T>() where T : class;
        bool Contains<T>(T Entity) where T : class;
    }

    public class Repository : IRepository
    {
        private static readonly PooledRedisClientManager m = new PooledRedisClientManager();
        readonly static ConcurrentDictionary<Type, HashSet<object>> _cache = new ConcurrentDictionary<Type, HashSet<object>>();

        private IRedisClientsManager RedisManager { get; set; }
        private IDictionary<Type, HashSet<object>> Cache { get; set; }

        public ObjectRepository()
        {
            RedisManager = m;
            Cache = _cache;
        }

        public ObjectRepository(IRedisClientsManager redisManager, IDictionary<Type, HashSet<object>> cache)
        {
            RedisManager = redisManager;
            Cache = cache;
        }

        /// <summary>
        /// Load {T} into Object-cache from Data Store.
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        public void LoadIntoCache<T>() where T : class
        {
            // Lets make sure we never replace _cache[T] if key is already present. 
            if (!_cache.ContainsKey(typeof(T)))
            {
                _cache[typeof(T)] = new HashSet<object>(RedisGetAll<T>().Cast<object>().ToList());
            }
        }

        /// <summary>
        /// Ensures that given type exist in Object-cache.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public Type EnsureTypeInCache(Type type)
        {
            // Check if type exist in _cache 
            if (!_cache.ContainsKey(type))
            {
                // I am aware that this portion should contain exception handling. 
                // Key not present adding key and empty cache.
                _cache.TryAdd(type, new HashSet<object>());
                //_cache[type] = new HashSet<object>();
            }

            return type;
        }

        /// <summary>
        /// Find First {T} in Object-cache.
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        /// <param name="predicate">linq statement</param>
        /// <returns></returns>
        public T FindFirstBy<T>(Func<T, bool> predicate) where T : class
        {
            // Lets prevent race conditions, locking down cache.
            lock (EnsureTypeInCache(typeof(T)))
            {
                return _cache[typeof(T)].Cast<T>().Where(predicate).FirstOrDefault();
            }
        }

        /// <summary>
        /// Find Single {T} in Object-cache.
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        /// <param name="predicate">linq statement</param>
        /// <returns></returns>
        public T FindSingleBy<T>(Func<T, bool> predicate) where T : class
        {
            // Lets prevent race conditions, locking down cache.
            lock (EnsureTypeInCache(typeof(T)))
            {
                return _cache[typeof(T)].Cast<T>().Where(predicate).SingleOrDefault();
            }
        }

        /// <summary>
        /// Tries to update or Add entity to Object-cache and Data Store.
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        /// <param name="predicate">linq expression</param>
        /// <param name="entity">entity</param>
        public bool AddOrUpdate<T>(T entity) where T : class
        {
            // Lets prevent race conditions, locking down cache.
            lock (EnsureTypeInCache(typeof(T)))
            {
                if (_cache[typeof(T)].Contains(entity))
                {
                    _cache[typeof(T)].Remove(entity);
                }
                _cache[typeof(T)].Add(entity);
            }

            // Redis does not care if record is new or old as it will Add or Update regardless.
            RedisStore<T>(entity);

            return true;
        }

        /// <summary>
        /// Delete single {T} from Object-cache and Data Store.
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        /// <param name="entity">class object</param>
        public void Remove<T>(T entity) where T : class
        {
            // Lets prevent race conditions, locking down cache.
            lock (EnsureTypeInCache(typeof(T)))
            {
                if (_cache[typeof(T)].Contains(entity))
                {
                    _cache[typeof(T)].Remove(entity);
                }

                RedisDelete<T>(entity);
            }
        }

        /// <summary>
        /// Check if {T} exists in Object-cache.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="entity"></param>
        /// <returns></returns>
        public bool Contains<T>(T entity) where T : class
        {
            // Lets prevent race conditions, locking down cache.
            lock (EnsureTypeInCache(typeof(T)))
            {
                return _cache[typeof(T)].Contains(entity);
            }
        }

        /// <summary>
        /// Find List<T>(predicate) in Object-cache.
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        /// <param name="predicate">linq statement</param>
        /// <returns></returns>
        public IEnumerable<T> FindBy<T>(Func<T, bool> predicate) where T : class
        {
            // Lets prevent race conditions, locking down cache.
            lock (EnsureTypeInCache(typeof(T)))
            {
                return _cache[typeof(T)].Cast<T>().Where(predicate);
            }
        }

        /// <summary>
        /// Get all {T} from Object-cache.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public IEnumerable<T> All<T>() where T : class
        {
            // Lets prevent race conditions, locking down cache.
            lock (EnsureTypeInCache(typeof(T)))
            {
                return _cache[typeof(T)].Cast<T>();
            }
        }

        /// <summary>
        /// Get Next Sequence for the given {T} Entity from Data Store. 
        /// </summary>
        /// <typeparam name="T">class</typeparam>
        /// <returns>long</returns>
        public long Next<T>() where T : class
        {
            return RedisNext<T>();
        }

        #region Redis Commands
        //
        // Following methods are ment as static private methods.
        //

        private long RedisNext<T>() where T : class
        {
            using (var ctx = m.GetClient())
                return ctx.As<T>().GetNextSequence();
        }

        private void RedisDelete<T>(T entity) where T : class
        {
            using (var ctx = m.GetClient())
                ctx.As<T>().Delete(entity);
        }

        private T RedisFind<T>(long id) where T : class
        {
            using (var ctx = m.GetClient())
                return ctx.As<T>().GetById(id);
        }

        private HashSet<T> RedisGetAll<T>() where T : class
        {
            using (var ctx = m.GetClient())
                return new HashSet<T>(ctx.As<T>().GetAll());
        }

        private void RedisStore<T>(T entity) where T : class
        {
            using (var ctx = m.GetClient())
                ctx.Store<T>(entity);
        }

        #endregion
    }
}

Fourth iteration with generic class constructor, less clutter and no longer need to have multiple types in same repository.

using System;
using System.Collections.Generic;
using System.Linq;
using ServiceStack.Redis;

namespace Datamodel
{
    public interface IObjectRepository<T> where T : class
    {
        void LoadIntoCache();
        T FindFirstBy(Func<T, bool> predicate);
        T FindSingleBy(Func<T, bool> predicate);
        bool AddOrUpdate(T entity);
        void Remove(T entity);
        IEnumerable<T> FindBy(Func<T, bool> predicate);
        IEnumerable<T> All();
        long Next();
        bool Contains(T Entity);
    }

    public class ObjectRepository<T> : IObjectRepository<T> where T : class
    {
        private static readonly PooledRedisClientManager m = new PooledRedisClientManager();
        private static HashSet<T> _cache = new HashSet<T>();

        private IRedisClientsManager RedisManager { get; set; }
        private HashSet<T> Cache { get; set; }

        public ObjectRepository()
        {
            RedisManager = m;
            Cache = _cache;
        }

        public ObjectRepository(IRedisClientsManager redisManager, HashSet<T> cache)
        {
            RedisManager = redisManager;
            Cache = cache;
        }

        /// <summary>
        /// Load {T} into Object-cache from Data Store.
        /// </summary>
        public void LoadIntoCache()
        {
            // Lets make sure we never replace _cache[T] if key is already present. 
            _cache = new HashSet<T>(RedisGetAll().ToList());
        }

        /// <summary>
        /// Find First {T} in Object-cache.
        /// </summary>
        /// <param name="predicate">linq statement</param>
        /// <returns></returns>
        public T FindFirstBy(Func<T, bool> predicate)
        {
            // Lets prevent race conditions, locking down cache.
            lock (_cache)
            {
                return _cache.Where(predicate).FirstOrDefault();
            }
        }

        /// <summary>
        /// Find Single {T} in Object-cache.
        /// </summary>
        /// <param name="predicate">linq statement</param>
        /// <returns></returns>
        public T FindSingleBy(Func<T, bool> predicate)
        {
            // Lets prevent race conditions, locking down cache.
            lock (_cache)
            {
                return _cache.Where(predicate).SingleOrDefault();
            }
        }

        /// <summary>
        /// Tries to update or Add entity to Object-cache and Data Store.
        /// </summary>
        /// <param name="predicate">linq expression</param>
        /// <param name="entity">entity</param>
        public bool AddOrUpdate(T entity)
        {
            // Lets prevent race conditions, locking down cache.
            lock (_cache)
            {
                if (_cache.Contains(entity))
                {
                    _cache.Remove(entity);
                }
                _cache.Add(entity);
            }

            // Redis does not care if record is new or old as it will Add or Update regardless.
            RedisStore(entity);

            return true;
        }

        /// <summary>
        /// Delete single {T} from Object-cache and Data Store.
        /// </summary>
        /// <param name="entity">class object</param>
        public void Remove(T entity)
        {
            // Lets prevent race conditions, locking down cache.
            lock (_cache)
            {
                if (_cache.Contains(entity))
                {
                    _cache.Remove(entity);
                }

                RedisDelete(entity);
            }
        }

        /// <summary>
        /// Check if {T} exists in Object-cache.
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public bool Contains(T entity)
        {
            // Lets prevent race conditions, locking down cache.
            lock (_cache)
            {
                return _cache.Contains(entity);
            }
        }

        /// <summary>
        /// Find List<T>(predicate) in Object-cache.
        /// </summary>
        /// <param name="predicate">linq statement</param>
        /// <returns></returns>
        public IEnumerable<T> FindBy(Func<T, bool> predicate)
        {
            // Lets prevent race conditions, locking down cache.
            lock (_cache)
            {
                return _cache.Where(predicate);
            }
        }

        /// <summary>
        /// Get all {T} from Object-cache.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<T> All()
        {
            // Lets prevent race conditions, locking down cache.
            lock (_cache)
            {
                return _cache;
            }
        }

        /// <summary>
        /// Get Next Sequence for the given {T} Entity from Data Store. 
        /// </summary>
        /// <returns>long</returns>
        public long Next()
        {
            return RedisNext();
        }

        #region Redis Commands
        //
        // Following methods are ment as static private methods.
        //

        private long RedisNext()
        {
            using (var ctx = m.GetClient())
                return ctx.As<T>().GetNextSequence();
        }

        private void RedisDelete(T entity)
        {
            using (var ctx = m.GetClient())
                ctx.As<T>().Delete(entity);
        }

        private T RedisFind(long id)
        {
            using (var ctx = m.GetClient())
                return ctx.As<T>().GetById(id);
        }

        private HashSet<T> RedisGetAll() 
        {
            using (var ctx = m.GetClient())
                return new HashSet<T>(ctx.As<T>().GetAll());
        }

        private void RedisStore(T entity)
        {
            using (var ctx = m.GetClient())
                ctx.Store<T>(entity);
        }

        #endregion
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.