3
\$\begingroup\$

I have been using Repositories in my ASP.NET MVC projects and I felt the need to fully cache small tables data (dictionaries, cities, countries etc.). This kind of information is changed (very) infrequently, so caching brings a great benefit. Also, I thought it would be nice to have cached repositories setup as simple as possible (even invisible to the services using them).

My inspiration was this nice article, but I needed to cache entire table content, rather than query results.

Here it is what I have managed to do so far:

1) Repository interface

public interface IRepository<T> : IRepository
    where T : class
{
    IQueryable<T> AllNoTracking { get; }

    IQueryable<T> All { get; }
    DbSet<T> GetSet { get; }

    T Get(int id);

    void Insert(T entity);
    void BulkInsert(IEnumerable<T> entities);
    void Delete(T entity);
    void RemoveRange(IEnumerable<T> range);
    void ClearAll();
    void Update(T entity);

    IList<T> ExecProcedure(string procedureName, IList<Tuple<string, object>> parameters);
    void Truncate();

    IList<T> Select(string queryText);
}


/// <summary>
/// provides methods specific to a cached repository (as opposed to those from normal repositories)
/// </summary>
public interface ICachedRepository<T> where T : class, new()
{
    string CacheKey { get; }

    void InvalidateCache();
    void InsertIntoCache(T item);
}

2) Cached repository should work above a normal repository, that's why a cached repository requires a reference to a normal one. It is not fully implemented, but the core functionality is there:

public class CachedRepository<T> : ICachedRepository<T>, IRepository<T> where T : class, new()
{
    #region Properties
    private int AbsoluteExpiration { get; }
    private int SlidingExpiration { get; }
    #endregion

    #region Variables
    private readonly IRepository<T> _modelRepository;

    private static readonly object CacheLockObject = new object();
    #endregion

    #region Properties
    public string CacheKey => $"CachedRepository-{typeof(T).Name}";

    #endregion

    #region Constructor
    public CachedRepository(IRepository<T> modelRepository, int absoluteExpiration, int slidingExpiration)
    {
        _modelRepository = modelRepository;
        AbsoluteExpiration = absoluteExpiration;
        SlidingExpiration = slidingExpiration;
    }

    #endregion

    #region Private methods

    private IList<T> ThreadSafeCacheAccessAction(Action<IList<T>> action = null)
    {
        // refresh cache if necessary
        var list = HttpRuntime.Cache[CacheKey] as IList<T>;
        if (list == null)
        {
            lock (CacheLockObject)
            {
                list = HttpRuntime.Cache[CacheKey] as IList<T>;
                if (list == null)
                {
                    list = _modelRepository.All.ToList();
                    HttpRuntime.Cache.Insert(CacheKey, list, dependencies: null, 
                        absoluteExpiration: DateTime.UtcNow.AddMinutes(AbsoluteExpiration), 
                        slidingExpiration: SlidingExpiration <= 0 ? Cache.NoSlidingExpiration : TimeSpan.FromMinutes(SlidingExpiration));
                }
            }
        }

        // execute custom action, if one is required
        if (action != null)
        {
            lock (CacheLockObject)
            {
                action(list);
            }
        }

        return list;
    }
    #endregion

    public IList<T> GetCachedItems()
    {
        IList<T> ret = ThreadSafeCacheAccessAction();
        return ret;
    }

    /// <summary>
    /// returns value without using cache, to allow Queryable usage
    /// </summary>
    public IQueryable<T> All => _modelRepository.All;

    public IQueryable<T> AllNoTracking
    {
        get
        {
            var cachedItems = GetCachedItems();
            return cachedItems.AsQueryable();
        }
    }

    public DbSet<T> GetSet => _modelRepository.GetSet;

    public IQueryable AllNonGeneric(Type t)
    {
        throw new NotImplementedException();
    }

    public IQueryable AllNoTrackingGeneric(Type t)
    {
        throw new NotImplementedException();
    }

    public void BulkInsert(IEnumerable<T> entities)
    {
        var enumerable = entities as IList<T> ?? entities.ToList();
        _modelRepository.BulkInsert(enumerable);

        // also inserting items within the cache
        ThreadSafeCacheAccessAction((list) =>
        {
            foreach (var item in enumerable)
                list.Add(item);
        });
    }

    public void Delete(T entity)
    {
        _modelRepository.Delete(entity);

        ThreadSafeCacheAccessAction((list) =>
        {
            list.Remove(entity);
        });
    }

    public IList<T> ExecProcedure(string procedureName, IList<Tuple<string, object>> parameters)
    {
        throw new NotImplementedException();
    }

    public void Truncate()
    {
        _modelRepository.Truncate();

        ThreadSafeCacheAccessAction(list =>
        {
            list.Clear();
        });
    }

    public T Get(int id)
    {
        //TODO: use cache
        return _modelRepository.Get(id);
    }

    public DbSet GetSetNonGeneric(Type t)
    {
        return _modelRepository.GetSetNonGeneric(t);
    }

    public void Insert(T entity)
    {
        _modelRepository.Insert(entity);

        ThreadSafeCacheAccessAction((list) =>
        {
            list.Add(entity);
        });
    }

    public void RemoveRange(IEnumerable<T> range)
    {
        var enumerable = range as IList<T> ?? range.ToList();
        _modelRepository.RemoveRange(enumerable);

        ThreadSafeCacheAccessAction(list =>
        {
            foreach (var item in enumerable)
                list.Remove(item);
        });
    }

    public void ClearAll()
    {
        //TODO:
        throw new NotImplementedException();
    }

    public IList<T> Select(string queryText)
    {
        return _modelRepository.Select(queryText);
    }

    //TODO: implement
    public void Update(T entity)
    {
        throw new NotImplementedException();
    }

    // ICachedRepository methods
    public void InvalidateCache()
    {
        HttpRuntime.Cache.Remove(CacheKey);
    }

    public void InsertIntoCache(T item)
    {
        ThreadSafeCacheAccessAction((list) =>
        {
            list.Add(item);
        });
    }
}

3) Setting up (bindings for Ninject).

It wires up all IRepository<> to Repository<>, but allows to specify some basic caching information for some types.

    // IRepository<T> should be solved using Repository<T>, by default
    kernel.Bind(typeof(IRepository<>)).To(typeof(Repository<>));

    // IRepository<T> must be solved to Repository<T>, if used in CachedRepository<T>
    kernel.Bind(typeof(IRepository<>)).To(typeof(Repository<>)).WhenInjectedInto(typeof(CachedRepository<>));

    // explicit repositories using caching: type, absolute expiration (in minutes), sliding expiration (in minutes)
    var cachedTypes = new List<Tuple<Type, int, int>>
    {
        new Tuple<Type, int, int>(typeof(ImportingSystem), 60, 0),
        new Tuple<Type, int, int>(typeof(ImportingSystemLoadInfo), 60, 0),
        new Tuple<Type, int, int>(typeof(Environment), 120, 0)
    };

    cachedTypes.ForEach(definition =>
    {
        Type cacheRepositoryType = typeof(CachedRepository<>).MakeGenericType(definition.Item1);
        var repoType = typeof(IRepository<>).MakeGenericType(definition.Item1);
        var resolvedRepoType = kernel.Get(repoType);

        // allow access as normal repository
        kernel
            .Bind(repoType)
            .ToMethod(_ => Activator.CreateInstance(cacheRepositoryType, resolvedRepoType, definition.Item2, definition.Item3));

        // allow access as a cached repository
        kernel
            .Bind(typeof(ICachedRepository<>).MakeGenericType(definition.Item1))
            .ToMethod(_ => Activator.CreateInstance(cacheRepositoryType, resolvedRepoType, definition.Item2, definition.Item3));
    });

4) Usage

For read operations, injecting IRepository<T> will work directly (if cache is enabled for that type, it will be used. Otherwise, it will behave as a regular repository).

However, for repository data changes, the consumer must be aware of caching mechanism and invalidate it in some cases (e.g. major changes should trigger a full reload of items).

Any thoughts? Any improvement suggestions are greatly welcomed.

\$\endgroup\$
4
\$\begingroup\$

Cached repository should work above a normal repository, that's why a cached repository requires a reference to a normal one.

This clearly indicates the Decorator Pattern but you are not using it right. The second ICachedRepository is a bad choice here (unless it really provides different methods but you didn't show it so I cannot really say anything about it).

You should have a normal repository implementing the IRepository<T> interface (let's assume T is int):

class MyRepository : IRepository<int> 
{
    ... 
}

and another one that will provide caching for the first one but being a IRepository<T> too:

class MyCachedRepository : IRepository<int> 
{
    private readonly IRepository<int> _repository;

    public MyCachedRepository(IRepository<int> repository)
    {
        _repository = repository;
    }

    ...
}
\$\endgroup\$
  • \$\begingroup\$ I understand how I should change it to use Decorator Pattern. For reading only, it works fine, but I should be able to explicitly handle cached repository for cache specific operations like invalidation. IRepository<TModel> should not be aware of cache specific operations. How should I proceed? \$\endgroup\$ – Alexei Sep 29 '16 at 8:59
  • \$\begingroup\$ @Alexei why do you want to invalidate the cache manully? It doesn't seem right. If you have some timeout for the cache in your application then you could move it to the cached-repository so it takes care of it itself. \$\endgroup\$ – t3chb0t Sep 29 '16 at 9:02
  • \$\begingroup\$ Normally, explicit invalidation are not required. But application administrators are able to change repository items and they should not wait for cache expiration to see their changes (these changes are rarely performed, though). \$\endgroup\$ – Alexei Sep 29 '16 at 9:05
  • \$\begingroup\$ @Alexei then the cache should be updated with InsertIntoCachethat could easily be replaced with a simple Add that would add the item both to the repository and update the cache when used from the cached repository. \$\endgroup\$ – t3chb0t Sep 29 '16 at 9:06
  • \$\begingroup\$ Oh, that makes sense. I will make the necessary changes and get rid of ICacheRepository. Thank you! \$\endgroup\$ – Alexei Sep 29 '16 at 9:08

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.