LaptopHeaven
LaptopHeaven

Reputation: 968

Caching Data Objects when using Repository/Service Pattern and MVC

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, and web). Currently, the controllers communicate directly to the services. This has limited the ability to apply proper caching.

I see my options as the following:

I can see the pros and cons of each. What is/should the best practice be for caching with Repository/Service

Upvotes: 47

Views: 34460

Answers (3)

Alexei - check Codidact
Alexei - check Codidact

Reputation: 23078

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<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 Update(T entity);
}

2. Normal/non-cached repository

public class Repository<T> : IRepository<T> where T : class, new()
{
    private readonly IEfDbContext _context;

    public Repository(IEfDbContext context)
    {
        _context = context;
    }

    public IQueryable<T> All => _context.Set<T>().AsQueryable();

    public IQueryable<T> AllNoTracking => _context.Set<T>().AsNoTracking();

    public IQueryable AllNoTrackingGeneric(Type t)
    {
        return _context.GetSet(t).AsNoTracking();
    }

    public DbSet<T> GetSet => _context.Set<T>();

    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<T>().Find(id);
    }

    public void Delete(T entity)
    {
        if (_context.Entry(entity).State == EntityState.Detached)
            _context.Set<T>().Attach(entity);

        _context.Set<T>().Remove(entity);
    }

    public void RemoveRange(IEnumerable<T> range)
    {
        _context.Set<T>().RemoveRange(range);
    }

    public void Insert(T entity)
    {
        _context.Set<T>().Add(entity);
    }

    public void BulkInsert(IEnumerable<T> entities)
    {
        _context.BulkInsert(entities);
    }

    public void Update(T entity)
    {
        _context.Set<T>().Attach(entity);
        _context.Entry(entity).State = EntityState.Modified;
    }

}

3. Generic cached repository is based on non-cached one

public interface ICachedRepository<T> where T : class, new()
{
    string CacheKey { get; }

    void InvalidateCache();
    void InsertIntoCache(T item);
}

public class CachedRepository<T> : ICachedRepository<T>, IRepository<T> where T : class, new()
{
    private readonly IRepository<T> _modelRepository;
    private static readonly object CacheLockObject = new object();

    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();
                    //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<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();
        }
    }

    // other methods come here
    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);
        });
    }
}

Using a DI framework (I am using Ninject), one can easily define if a repository should be cached or not:

// 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
 var cachedTypes = new List<Type>
 {
    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.

Upvotes: 9

Brendan Enrick
Brendan Enrick

Reputation: 4297

Steve Smith did two great blog posts which demonstrate how to use his CachedRepository pattern to achieve the result you're looking for.

Introducing the CachedRepository Pattern

Building a CachedRepository via Strategy Pattern

In these two posts he shows you how to set up this pattern and also explains why it is useful. By using this pattern you get caching without your existing code seeing any of the caching logic. Essentially you use the cached repository as if it were any other repository.

public class CachedAlbumRepository : IAlbumRepository
{
    private readonly IAlbumRepository _albumRepository;

    public CachedAlbumRepository(IAlbumRepository albumRepository)
    {
        _albumRepository = albumRepository;
    }

    private static readonly object CacheLockObject = new object();

    public IEnumerable<Album> GetTopSellingAlbums(int count)
    {
        Debug.Print("CachedAlbumRepository:GetTopSellingAlbums");
        string cacheKey = "TopSellingAlbums-" + count;
        var result = HttpRuntime.Cache[cacheKey] as List<Album>;
        if (result == null)
        {
            lock (CacheLockObject)
            {
                result = HttpRuntime.Cache[cacheKey] as List<Album>;
                if (result == null)
                {
                    result = _albumRepository.GetTopSellingAlbums(count).ToList();
                    HttpRuntime.Cache.Insert(cacheKey, result, null, 
                        DateTime.Now.AddSeconds(60), TimeSpan.Zero);
                }
            }
        }
        return result;
    }
}

Upvotes: 42

user1228
user1228

Reputation:

The easiest way would be to handle caching in your repository provider. That way you don't have to change out any code in the rest of your app; it will be oblivious to the fact that the data was served out of a cache rather than the repository.

So, I'd create an interface that the controllers use to communicate with the backend, and in the implementation of this I'd add the caching logic. Wrap it all up in a nice bow with some DI, and your app will be set for easy testing.

Upvotes: 26

Related Questions