Кэширование объектов данных при использовании Repository/Service Pattern и MVC
У меня есть сайт на основе MVC, который использует шаблон Repository/Service для доступа к данным. Сервисы написаны для использования в большинстве приложений (консоль, winform и веб). В настоящее время контроллеры общаются напрямую с сервисами. Это ограничивает возможности применения правильного кэширования.
Я вижу свои варианты как следующие:
- Напишите оболочку для веб-приложения, которое реализует IWhatEverService, который выполняет кэширование.
- Примените кеширование в каждом контроллере, кэшируя ViewData для каждого действия.
- Не беспокойтесь о кэшировании данных и просто реализуйте OutputCaching для каждого действия.
Я вижу плюсы и минусы каждого. Какова / должна быть лучшая практика для кэширования с Repository/Service
4 ответа
Самый простой способ - обработать кэширование в вашем поставщике репозитория. Таким образом, вам не нужно менять код в остальной части вашего приложения; это будет игнорировать тот факт, что данные обслуживались из кэша, а не из хранилища.
Итак, я бы создал интерфейс, который контроллеры будут использовать для взаимодействия с бэкэндом, и в реализации этого я бы добавил логику кэширования. Заверните все это в хороший поклон с небольшим количеством DI, и ваше приложение будет настроено для простого тестирования.
Стив Смит сделал два отличных поста в блоге, которые демонстрируют, как использовать его шаблон CachedRepository для достижения желаемого результата.
Представляем шаблон CachedRepository
Создание CachedRepository через шаблон стратегии
В этих двух постах он показывает вам, как настроить этот шаблон, а также объясняет, почему он полезен. Используя этот шаблон, вы получаете кеширование, при котором существующий код не видит какой-либо логики кеширования. По сути, вы используете кэшированный репозиторий, как если бы это был любой другой репозиторий.
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;
}
}
Основываясь на ответе Brendan Enrick, я определил общий кэшированный репозиторий для особого случая относительно небольших списков, которые редко изменяются, но интенсивно читаются.
1. Интерфейс
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. Нормальный / не кэшированный репозиторий
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. Общий кэшированный репозиторий основан на некэшированном
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);
});
}
}
Используя DI-фреймворк (я использую Ninject), можно легко определить, следует ли кэшировать репозиторий:
// 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));
});
Таким образом, чтение из кэшированных репозиториев выполняется без знания кэширования. Однако для их изменения требуется ввести из ICacheRepository<>
и вызывая соответствующие методы.
Проверьте мою реализацию службы кэширования:
Как кешировать данные в приложении MVC
(Я не хочу повторять ответ здесь...)
Не стесняйтесь комментировать!