IReadOnlyCachedRepository<T>
public interface IReadOnlyCachedRepository<T>
{
void RefreshIfExpired();
void RefreshSubset(Expression<Func<T, bool>> selector);
IEnumerable<T> All();
T Get(Expression<Func<T, bool>> selector);
bool Exists(Expression<Func<T, bool>> selector);
int Count(Expression<Func<T, bool>> selector);
}
ReadOnlyCachedRepository<T>
public class ReadOnlyCachedRepository<T> : IReadOnlyCachedRepository<T> where T : class
{
protected static readonly TimeSpan DefaultCacheRefreshInterval = TimeSpan.FromMinutes(15);
protected static ConcurrentDictionary<T, bool> CachedData;
protected static DateTime EntireCacheLastUpdatedOn = DateTime.MinValue;
protected static DateTime NextCacheRefreshOn = DateTime.MinValue;
protected static ILogger Logger;
protected DbContext Db;
protected TimeSpan CacheRefreshInterval;
public ReadOnlyCachedRepository(DbContext db)
{
Logger = new Log4NetWrapper(this.GetType());
Db = db;
CacheRefreshInterval = DefaultCacheRefreshInterval;
}
/// <summary>
/// Determines the entire set of data to cache.
/// Override this to change the scope of the cache.
/// </summary>
protected virtual IEnumerable<T> GetDataToCache(Expression<Func<T, bool>> selector = null)
{
if (selector == null)
{
return Db.Set<T>().AsNoTracking<T>().ToList();
}
else
{
return Db.Set<T>().AsNoTracking<T>().Where(selector.Compile()).ToList();
}
}
/// <summary>
/// Refreshes the entire cached data set if it has expired.
/// </summary>
public void RefreshIfExpired()
{
if (DateTime.Now >= NextCacheRefreshOn)
{
var dataToCache = GetDataToCache().ToDictionary(
keySelector: x => x,
elementSelector: x => false
);
CachedData = new ConcurrentDictionary<T, bool>(dataToCache);
EntireCacheLastUpdatedOn = DateTime.Now;
NextCacheRefreshOn = EntireCacheLastUpdatedOn.Add(CacheRefreshInterval);
}
}
/// <summary>
/// Removes any cached data matching the selector and pulls data to be cached matching the selector.
/// </summary>
public void RefreshSubset(Expression<Func<T, bool>> selector)
{
var dataToRemove = CachedData.Select(x => x.Key).Where(selector.Compile());
foreach (var data in dataToRemove)
{
CachedData.TryRemove(data, out _);
}
var dataFailedToRefresh = new List<T>();
var dataToRefresh = GetDataToCache(selector);
foreach (var data in dataToRefresh)
{
var wasAdded = CachedData.TryAdd(data, false);
if (wasAdded == false)
{
dataFailedToRefresh.Add(data);
}
}
Logger.Log(Level.Debug, $"{dataToRemove.Count()} items were removed from the cache. {dataFailedToRefresh.Count} of {dataToRefresh.Count()} items of the cache subset failed to be added to the cache.");
}
/// <summary>
/// Retrieves all cached data.
/// </summary>
public virtual IEnumerable<T> All()
{
RefreshIfExpired();
return CachedData.Keys;
}
/// <summary>
/// Retrieves the first item matching the selector in the cache or null.
/// </summary>
public virtual T Get(Expression<Func<T, bool>> selector)
{
RefreshIfExpired();
return CachedData.Keys.FirstOrDefault(selector.Compile());
}
/// <summary>
/// Checks if at least one item matching the selector exists in the cache.
/// </summary>
public virtual bool Exists(Expression<Func<T, bool>> selector)
{
RefreshIfExpired();
return CachedData.Keys.Any(selector.Compile());
}
/// <summary>
/// Retrieves the number items matching the selector in the cache.
/// </summary>
public virtual int Count(Expression<Func<T, bool>> selector)
{
RefreshIfExpired();
return CachedData.Keys.Count(selector.Compile());
}
}
CustomerCachedRepository
An example of an override of ReadOnlyCachedRepository<T>.GetDataToCache(..)
.
public class CustomerCachedRepository : ReadOnlyCachedRepository<Customer>
{
public CustomerCachedRepository(DbContext db) : base(db)
{
CacheRefreshInterval = TimeSpan.FromHours(16);
}
protected override IEnumerable<Customer> GetDataToCache(Expression<Func<Customer, bool>> selector = null)
{
var query = Db.Set<Customer>()
.AsNoTracking<Customer>()
.Where(x => x.IsBlocked == false);
if (selector != null)
{
query = query.Where(selector);
}
return query.ToList();
}
}
Concerns
- Is there a better choice than
ConcurrentDictionary<T, bool>
? I started withConcurrentBag<T>
but it didn't have the ability to add/remove specific items from the cache. I then switched to dictionary since it had the ability to add/remove items but I'm not using the value (aka thebool
). Which feels bad/hackish. - Thread-Safety - The whole reason I am using concurrent collections is because I'm not very knowledgeable in the area of thread-safety and I just want to make sure that in an MVC environment where each controller instance is going to have access to this repository that it doesn't try to refresh the repository twice at the same time.
- Performance - I wanted to cache the data to reduce the number of connections between the application server and database, which isn't necessarily going to increase performance but I would like to definitely have performance as optimal as possible with this implementation.
Purpose/Usage
RefreshIfExpired()
only refreshes the cache if it has expired. Allowing this to be called on every repository action (and in administrator related actions if necessary; which is why it's exposed).RefreshSubset()
forces a refresh but only to the subset of data selected. It does not update the expired timestamp since it is only a subset of the data, so there is a chance that it'll expire immediately after it is refreshed (taking that minor risk in favor of simpler implementation). This is also exposed so that the actions that update user permissions can call it for a particular username to always keep permissions cache up to date.