I have an MVC-based site, which is using a Repository/Service pattern for data access. The Services are written to be using in a majority of applications (console, winform,
Based on answer provided by Brendan, I defined a generic cached repository for the special case of relatively small lists that are rarely changed, but heavily read.
1. The interface
public interface IRepository : IRepository
where T : class
{
IQueryable AllNoTracking { get; }
IQueryable All { get; }
DbSet GetSet { get; }
T Get(int id);
void Insert(T entity);
void BulkInsert(IEnumerable entities);
void Delete(T entity);
void RemoveRange(IEnumerable range);
void Update(T entity);
}
2. Normal/non-cached repository
public class Repository : IRepository where T : class, new()
{
private readonly IEfDbContext _context;
public Repository(IEfDbContext context)
{
_context = context;
}
public IQueryable All => _context.Set().AsQueryable();
public IQueryable AllNoTracking => _context.Set().AsNoTracking();
public IQueryable AllNoTrackingGeneric(Type t)
{
return _context.GetSet(t).AsNoTracking();
}
public DbSet GetSet => _context.Set();
public DbSet GetSetNonGeneric(Type t)
{
return _context.GetSet(t);
}
public IQueryable AllNonGeneric(Type t)
{
return _context.GetSet(t);
}
public T Get(int id)
{
return _context.Set().Find(id);
}
public void Delete(T entity)
{
if (_context.Entry(entity).State == EntityState.Detached)
_context.Set().Attach(entity);
_context.Set().Remove(entity);
}
public void RemoveRange(IEnumerable range)
{
_context.Set().RemoveRange(range);
}
public void Insert(T entity)
{
_context.Set().Add(entity);
}
public void BulkInsert(IEnumerable entities)
{
_context.BulkInsert(entities);
}
public void Update(T entity)
{
_context.Set().Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
}
}
3. Generic cached repository is based on non-cached one
public interface ICachedRepository where T : class, new()
{
string CacheKey { get; }
void InvalidateCache();
void InsertIntoCache(T item);
}
public class CachedRepository : ICachedRepository, IRepository where T : class, new()
{
private readonly IRepository _modelRepository;
private static readonly object CacheLockObject = new object();
private IList ThreadSafeCacheAccessAction(Action> action = null)
{
// refresh cache if necessary
var list = HttpRuntime.Cache[CacheKey] as IList;
if (list == null)
{
lock (CacheLockObject)
{
list = HttpRuntime.Cache[CacheKey] as IList;
if (list == null)
{
list = _modelRepository.All.ToList();
//TODO: remove hardcoding
HttpRuntime.Cache.Insert(CacheKey, list, null, DateTime.UtcNow.AddMinutes(10), Cache.NoSlidingExpiration);
}
}
}
// execute custom action, if one is required
if (action != null)
{
lock (CacheLockObject)
{
action(list);
}
}
return list;
}
public IList GetCachedItems()
{
IList ret = ThreadSafeCacheAccessAction();
return ret;
}
///
/// returns value without using cache, to allow Queryable usage
///
public IQueryable All => _modelRepository.All;
public IQueryable AllNoTracking
{
get
{
var cachedItems = GetCachedItems();
return cachedItems.AsQueryable();
}
}
// other methods come here
public void BulkInsert(IEnumerable entities)
{
var enumerable = entities as IList ?? 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);
});
}
}
Using a DI framework (I am using Ninject), one can easily define if a repository should be cached or not:
// IRepository should be solved using Repository, by default
kernel.Bind(typeof(IRepository<>)).To(typeof(Repository<>));
// IRepository must be solved to Repository, if used in CachedRepository
kernel.Bind(typeof(IRepository<>)).To(typeof(Repository<>)).WhenInjectedInto(typeof(CachedRepository<>));
// explicit repositories using caching
var cachedTypes = new List
{
typeof(ImportingSystem), typeof(ImportingSystemLoadInfo), typeof(Environment)
};
cachedTypes.ForEach(type =>
{
// allow access as normal repository
kernel
.Bind(typeof(IRepository<>).MakeGenericType(type))
.To(typeof(CachedRepository<>).MakeGenericType(type));
// allow access as a cached repository
kernel
.Bind(typeof(ICachedRepository<>).MakeGenericType(type))
.To(typeof(CachedRepository<>).MakeGenericType(type));
});
So, reading from cached repositories is done without knowing about the caching. However, changing them requires to inject from ICacheRepository<> and calling the appropriate methods.