Skip to main content
Updated the purpose/usage section to be a bit more clear for the refresh functions.
Source Link
Shelby115
  • 2k
  • 11
  • 20
  • 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() 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).
  • ForRefreshSubset() forces a small set of cases I need to be able to refresh but only to the subset of data selected. It does not update the expired timestamp since it is only a sub-setsubset of the cachedata, so there is a chance that it'll expire immediately after it is refreshed (e.gtaking that minor risk in favor of simpler implementation). by user forThis is also exposed so that the actions that update user permissions) but have can call it expire at the same time all otherfor a particular username to always keep permissions expire. Hence the function RefreshSubset(..)cache up to date.
  • 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(..).
  • 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.
Source Link
Shelby115
  • 2k
  • 11
  • 20

Cached Repository that can refresh all or a subset of the data

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(..).