Подтвердить что ты не робот

Как реализовать модель кэширования без нарушения шаблона 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, например), которую я могу использовать здесь?

Приветствуются общие советы/советы.

4b9b3361

Ответ 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, и мне хорошо идти.

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