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 with ConcurrentBag<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 the bool
). 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
- For much of the usage I'm simply loading a query and if any part of it needs refreshed then it all needs refreshed. So the base case is
RefreshIfExpired()
.
- For a small set of cases I need to be able to refresh a sub-set of the cache (e.g. by user for user permissions) but have it expire at the same time all other permissions expire. Hence the function
RefreshSubset(..)
.