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.