Как реализовать модель кэширования без нарушения шаблона MVC? - PullRequest
29 голосов
/ 07 февраля 2011

У меня есть веб-приложение ASP.NET MVC 3 (Razor) с определенной страницей, интенсивно использующей базу данных , и взаимодействие с пользователем имеет наивысший приоритет.

Итак, я ввожу кеширование на этой конкретной странице.

Я пытаюсь найти способ реализовать этот шаблон кэширования, сохраняя при этом мой контроллер тонкий , как в настоящее время без кэширования:

public PartialViewResult GetLocationStuff(SearchPreferences searchPreferences)
{
   var results = _locationService.FindStuffByCriteria(searchPreferences);
   return PartialView("SearchResults", results);
}

Как видите, контроллер очень тонкий, как и должно быть. Его не волнует, как и откуда он получает информацию - это работа службы.

Пара замечаний по потоку управления:

  1. Контроллеры получают определенную услугу , в зависимости от ее области. В этом примере этот контроллер получает LocationService
  2. Услуги Позвоните в IQueryable<T> Хранилище и материализуйте результаты в T или ICollection<T>.

Как я хочу реализовать кэширование:

  • Я не могу использовать кеширование вывода - по нескольким причинам. Прежде всего, этот метод действия вызывается со стороны клиента (jQuery / AJAX) через [HttpPost], который в соответствии со стандартами HTTP не должен кэшироваться как запрос. Во-вторых, я не хочу кешировать исключительно на основе аргументов HTTP-запроса - логика кеширования намного сложнее, чем это - на самом деле происходит двухуровневое кеширование.
  • Как я уже говорил выше, мне нужно использовать регулярное кэширование данных, например, Cache["somekey"] = someObj;.
  • Я не хочу реализовывать общий механизм кэширования, в котором все вызовы через службу сначала проходят через кэш - Я хочу кэшировать только для этого конкретного метода действия .

Первые мысли сказали бы мне создать другую службу (которая наследует LocationService ) и обеспечить там рабочий процесс кэширования (сначала проверьте кеш, если нет, вызовите db, добавьте в кеш, верните результат). 1053 *

У этого есть две проблемы:

  1. Услуги являются базовыми Библиотеки классов - никаких ссылок на что-либо дополнительное. Мне нужно добавить ссылку на System.Web здесь.
  2. Мне нужно было бы получить доступ к HTTP-контексту вне веб-приложения, что считается плохой практикой не только для тестируемости, но и вообще - верно?

Я также думал об использовании папки Models в веб-приложении (которую я в настоящее время использую только для ViewModels ), но использование службы кэширования в папке моделей просто не звучит правильно.

Итак - есть идеи? Есть ли что-то специфичное для MVC (например, Action Filter's), которое я могу использовать здесь?

Общие советы / советы будут с благодарностью.

Ответы [ 5 ]

25 голосов
/ 07 февраля 2011

Атрибут действия кажется хорошим способом для достижения этой цели. Вот пример (отказ от ответственности: я пишу это из головы: я выпил определенное количество пива, когда писал это, поэтому убедитесь, что вы тщательно его протестировали: -)):

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 предоставляет кэш второго уровня .

7 голосов
/ 07 февраля 2011

Я предоставлю общие советы, и, надеюсь, они укажут вам правильное направление.

  1. Если это ваша первая попытка кэширования в приложении, не кэшируйте HTTP-ответ, вместо этого кешируйте данные приложения. Обычно вы начинаете с кэширования данных и предоставления вашей базе данных передышки; затем, если этого недостаточно, а ваши приложения / веб-серверы находятся под огромным стрессом, вы можете подумать о кэшировании HTTP-ответов.

  2. Рассматривайте свой уровень кэширования данных как еще одну модель в парадигме MVC со всеми последующими последствиями.

  3. Что бы вы ни делали, не пишите свой собственный кеш. Это всегда выглядит проще, чем есть на самом деле. Используйте что-то вроде memcached.

6 голосов
/ 07 февраля 2011

Мой ответ основан на предположении, что ваши сервисы реализуют интерфейс, например, тип _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.

4 голосов
/ 07 февраля 2011

Я принял ответ @ Джоша, но подумал, что добавлю свой собственный ответ, потому что я не точно согласился с тем, что он предложил (близко), поэтому я подумал для полноты, я бы добавил что я на самом деле сделал.

Ключ, который я сейчас использую System.Runtime.Caching. Поскольку это существует в сборке, которая специфична для .NET, а не для ASP.NET, у меня нет проблем, ссылаясь на это в моей службе.

Итак, все, что я сделал, это поместил логику кэширования в конкретные методы уровня обслуживания, которые нуждаются в кэшировании.

И важный момент, я работаю с System.Runtime.Caching.ObjectCache классом - это то, что вставляется в конструктор службы.

Мой текущий DI вводит объект System.Runtime.Caching.MemoryCache. Преимущество класса ObjectCache в том, что он абстрактный и все основные методы являются виртуальными.

Это означает, что для моих модульных тестов я создал класс MockCache, переопределяющий все методы и реализующий базовый механизм кэширования с помощью простого Dictionary<TKey,TValue>.

Мы планируем вскоре перейти на Velocity - так что снова, все, что мне нужно сделать, это создать еще один ObjectCache производный класс, и я готов идти.

Спасибо за помощь всем!

4 голосов
/ 07 февраля 2011

Похоже, вы пытаетесь кэшировать данные, которые вы получаете из вашей базы данных .Вот как я справляюсь с этим (подход, который я видел во многих проектах с открытым исходным кодом 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);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...