Как реализовать модель кэширования без нарушения шаблона MVC?
У меня есть веб-приложение ASP.NET MVC 3 (Razor) с конкретной страницей с высокой интенсивностью базы данных, а пользовательский опыт имеет самый высокий приоритет.
Таким образом, я представляю кэширование на этой конкретной странице.
Я пытаюсь выяснить способ реализации этого шаблона кэширования, сохраняя мой контроллер тонким, например, в настоящее время он не кэшируется:
public PartialViewResult GetLocationStuff(SearchPreferences searchPreferences)
{
var results = _locationService.FindStuffByCriteria(searchPreferences);
return PartialView("SearchResults", results);
}
Как вы можете видеть, контроллер очень тонкий, как и должно быть. Он не заботится о том, как/откуда он получает информацию - это работа службы.
Несколько заметок о потоке управления:
- Контроллеры получают DI'ed определенную Сервис, в зависимости от ее области. В этом примере этот контроллер получает LocationService
- Службы перейдите в
IQueryable<T>
Репозиторий и внесите результаты в T
или ICollection<T>
.
Как я хочу реализовать кэширование:
- Я не могу использовать кэширование вывода - по нескольким причинам. Прежде всего, этот метод действия вызывается с клиентской стороны (jQuery/AJAX) через
[HttpPost]
, который согласно стандартам HTTP не должен кэшироваться как запрос. Во-вторых, я не хочу кэшировать исключительно на основе аргументов HTTP-запроса - логика кэша намного сложнее, чем есть. На самом деле происходит двухуровневое кэширование.
- Как я намекаю выше, мне нужно использовать обычное кэширование данных, например
Cache["somekey"] = someObj;
.
- Я не хочу реализовывать общий механизм кэширования, когда все вызовы через службу сначала проходят через кеш - Я хочу только кэшировать этот метод действий.
Сначала я подумал, что мне нужно создать другую службу (которая наследует LocationService) и обеспечивает там рабочий процесс кеширования (сначала проверьте кеш, если не там, вызовите db, добавьте в кеш, верните результат).
Это имеет две проблемы:
- Услуги являются базовыми Библиотеками классов - никаких ссылок на что-либо дополнительное. Мне нужно добавить ссылку на
System.Web
здесь.
- Мне нужно будет получить доступ к Контексту HTTP за пределами веб-приложения, которое считается плохой практикой, а не только для проверки, но в целом - правильно?
Я также подумал об использовании папки Models
в веб-приложении (которую я сейчас использую только для ViewModels), но наличие службы кеша в папке с моделями просто не звучит правильно.
Итак - какие-нибудь идеи? Есть ли какая-то MVC-специфическая вещь (например, Action Filter, например), которую я могу использовать здесь?
Приветствуются общие советы/советы.
Ответы
Ответ 1
Мой ответ основан на предположении, что ваши сервисы реализуют интерфейс, например, тип _locationService - это фактически ILocationService, но ему вводится конкретный LocationService. Создайте CachingLocationService, который реализует интерфейс ILocationService и изменит конфигурацию вашего контейнера, чтобы внедрить эту кеширующую версию службы этому контроллеру. CachingLocationService сам будет иметь зависимость от ILocationService, которая будет введена в исходный класс LocationService. Он будет использовать это для выполнения реальной бизнес-логики и заботиться только о том, чтобы вытащить и вытолкнуть из кеша.
Вам не нужно создавать CachingLocationService в той же сборке, что и исходный LocationService. Это может быть в вашей веб-сборке. Однако лично я поместил его в исходную сборку и добавлю новую ссылку.
Что касается добавления зависимости от HttpContext; вы можете удалить это, принимая зависимость от
Func<HttpContextBase>
и вводя это во время выполнения с чем-то вроде
() => HttpContext.Current
Затем в ваших тестах вы можете издеваться над HttpContextBase, но у вас могут возникнуть проблемы с издевательством над объектом Cache, не используя что-то вроде TypeMock.
Изменить: при дальнейшем чтении в пространстве имен .NET 4 System.Runtime.Caching ваш CachingLocationService должен зависеть от ObjectCache. Это абстрактный базовый класс для реализации кеша. Затем вы можете ввести это с помощью System.Runtime.Caching.MemoryCache.Default, например.
Ответ 2
Атрибут действия кажется хорошим способом достижения этого. Вот пример (отказ от ответственности: я пишу это с головы доски: я пил определенное количество пива при написании этого, поэтому обязательно проверяйте его: -)):
public class CacheModelAttribute : ActionFilterAttribute
{
private readonly string[] _paramNames;
public CacheModelAttribute(params string[] paramNames)
{
// The request parameter names that will be used
// to constitute the cache key.
_paramNames = paramNames;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
var cache = filterContext.HttpContext.Cache;
var model = cache[GetCacheKey(filterContext.HttpContext)];
if (model != null)
{
// If the cache contains a model, fetch this model
// from the cache and short-circuit the execution of the action
// to avoid hitting the repository
var result = new ViewResult
{
ViewData = new ViewDataDictionary(model)
};
filterContext.Result = result;
}
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
base.OnResultExecuted(filterContext);
var result = filterContext.Result as ViewResultBase;
var cacheKey = GetCacheKey(filterContext.HttpContext);
var cache = filterContext.HttpContext.Cache;
if (result != null && result.Model != null && cache[key] == null)
{
// If the action returned some model,
// store this model into the cache
cache[key] = result.Model;
}
}
private string GetCacheKey(HttpContextBase context)
{
// Use the request values of the parameter names passed
// in the attribute to calculate the cache key.
// This function could be adapted based on the requirements.
return string.Join(
"_",
(_paramNames ?? Enumerable.Empty<string>())
.Select(pn => (context.Request[pn] ?? string.Empty).ToString())
.ToArray()
);
}
}
И тогда действие вашего контроллера может выглядеть так:
[CacheModel("id", "name")]
public PartialViewResult GetLocationStuff(SearchPreferences searchPreferences)
{
var results = _locationService.FindStuffByCriteria(searchPreferences);
return View(results);
}
И что касается вашей проблемы со ссылкой на сборку System.Web
на уровне службы, это уже не проблема в .NET 4.0. Там есть совершенно новая сборка, которая предоставляет расширяемые возможности кеширования: System.Runtime.Caching, поэтому вы можете использовать это для реализации кэширования в своем сервисном уровне.
Или даже лучше, если вы используете ORM на своем уровне сервиса, возможно, этот ORM предоставляет возможности кэширования? Надеюсь, так оно и есть. Например, NHibernate предоставляет кеш второго уровня.
Ответ 3
Я предоставлю общие советы и надеюсь, что они укажут вас в правильном направлении.
-
Если это ваш первый удар в кешировании в вашем приложении, тогда не кэшируйте HTTP-ответ, затем кешируйте данные приложения. Обычно вы начинаете с кэширования данных и предоставления вашей базы данных в какой-то передышку; то, если этого недостаточно, и ваши приложения/веб-серверы находятся под большим стрессом, вы можете подумать о кешировании ответов HTTP.
-
Рассматривайте свой уровень кэша данных как другую модель в парадигме MVC со всеми последующими последствиями.
-
Независимо от того, что вы делаете, не пишите свой собственный кеш. Это всегда выглядит проще, чем на самом деле. Используйте что-то вроде memcached.
Ответ 4
Похоже, вы пытаетесь кэшировать данные, которые вы получаете из своей базы данных. Вот как я справляюсь с этим (подход, который я видел во многих проектах MVC с открытым исходным кодом):
/// <summary>
/// remove a cached object from the HttpRuntime.Cache
/// </summary>
public static void RemoveCachedObject(string key)
{
HttpRuntime.Cache.Remove(key);
}
/// <summary>
/// retrieve an object from the HttpRuntime.Cache
/// </summary>
public static object GetCachedObject(string key)
{
return HttpRuntime.Cache[key];
}
/// <summary>
/// add an object to the HttpRuntime.Cache with an absolute expiration time
/// </summary>
public static void SetCachedObject(string key, object o, int durationSecs)
{
HttpRuntime.Cache.Add(
key,
o,
null,
DateTime.Now.AddSeconds(durationSecs),
Cache.NoSlidingExpiration,
CacheItemPriority.High,
null);
}
/// <summary>
/// add an object to the HttpRuntime.Cache with a sliding expiration time. sliding means the expiration timer is reset each time the object is accessed, so it expires 20 minutes, for example, after it is last accessed.
/// </summary>
public static void SetCachedObjectSliding(string key, object o, int slidingSecs)
{
HttpRuntime.Cache.Add(
key,
o,
null,
Cache.NoAbsoluteExpiration,
new TimeSpan(0, 0, slidingSecs),
CacheItemPriority.High,
null);
}
/// <summary>
/// add a non-removable, non-expiring object to the HttpRuntime.Cache
/// </summary>
public static void SetCachedObjectPermanent(string key, object o)
{
HttpRuntime.Cache.Remove(key);
HttpRuntime.Cache.Add(
key,
o,
null,
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration,
CacheItemPriority.NotRemovable,
null);
}
У меня есть эти методы в статическом классе с именем Current.cs
. Здесь вы можете применить эти методы к действию вашего контроллера:
public PartialViewResult GetLocationStuff(SearchPreferences searchPreferences)
{
var prefs = (object)searchPreferences;
var cachedObject = Current.GetCachedObject(prefs); // check cache
if(cachedObject != null) return PartialView("SearchResults", cachedObject);
var results = _locationService.FindStuffByCriteria(searchPreferences);
Current.SetCachedObject(prefs, results, 60); // add to cache for 60 seconds
return PartialView("SearchResults", results);
}
Ответ 5
Я принял ответ @Josh, но подумал, что добавлю свой собственный ответ, потому что я точно не пошел с тем, что он предложил (закрыл), поэтому подумал о полноте, чтобы добавить то, что я действительно сделал.
Ключ я теперь использую System.Runtime.Caching
. Поскольку это существует в сборке, которая является специфичной для .NET, а не специфичной для ASP.NET, у меня нет проблем с ссылкой на нее в моей службе.
Итак, все, что я сделал, помещает логику кэширования в определенные методы уровня сервиса, которые требуют кэширования.
И важный момент: im отключение класса System.Runtime.Caching.ObjectCache
- это то, что вводится в конструктор службы.
Мой текущий DI вводит объект System.Runtime.Caching.MemoryCache
. Хорошая вещь в классе ObjectCache
заключается в том, что она абстрактна и все основные методы являются виртуальными.
Что означает мои модульные тесты, я создал класс MockCache
, переопределяя все методы и реализуя механизм кэширования с помощью простого Dictionary<TKey,TValue>
.
Мы планируем скоро перейти на Velocity - так что все, что мне нужно сделать, это создать еще один класс ObjectCache
, и мне хорошо идти.
Спасибо за помощь всем!