Статьи

Кэширование вывода в ASP.NET MVC

Хотя в настоящее время большинство из нас имеют широкополосные соединения, кэширование ресурсов важно, поскольку загрузка ресурса с локального жесткого диска (на данный момент) все еще быстрее, чем его извлечение удаленно. В этой статье я хотел бы изучить, как управлять поведением кэширования ASP.net MVC и его последствиями при использовании запросов ajax для получения данных.

ASP.net MVC Кэширование поведения

Если вы вообще ничего не указали и у вас есть обычный метод действия, такой как

public JsonResult Details(long id)
{
    //snip snip
    return Json(theResult, JsonRequestBehavior.AllowGet);
}

тогда ASP.net MVC вернет ответ со следующими заголовками:

Cache-Control:private
Connection:Close
Content-Length:81836
Content-Type:application/json; charset=utf-8
Date:Mon, 29 Oct 2012 08:08:44 GMT
Server:ASP.NET Development Server/11.0.0.0
X-AspNet-Version:4.0.30319
X-AspNetMvc-Version:3.0

Согласно официальной документации W3.org, Cache-Control: частная…

… Указывает, что все или часть ответного сообщения предназначены для одного пользователя и НЕ ДОЛЖНЫ кэшироваться общим кешем. Это позволяет исходному серверу утверждать, что указанные части ответа предназначены только для одного пользователя и не являются действительным ответом на запросы других пользователей. Частный (не общий) кеш МОЖЕТ кешировать ответ.

В результате браузер может кэшировать такой запрос ajax, сделанный клиентом JavaScript, который в большинстве случаев может оказаться нежелательным. Особенно наш любимый IE использовал массивный подход к кешированию, приводящий к странным эффектам.

Отключение поведения кэширования

Давайте сначала посмотрим, как полностью отключить кэширование.

Глобально на сервере (индивидуальный подход)

Если мы хотим полностью отключить какое-либо поведение кэширования, мы могли бы использовать своего рода метод грубой силы путем реализации настраиваемого фильтра глобальных действий, который устанавливает соответствующие заголовки:

public class MvcApplication : System.Web.HttpApplication
{
    ...
     public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new NoCacheGlobalActionFilter());
    }
    ...
}

public class NoCacheGlobalActionFilter : ActionFilterAttribute
{
    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        ...
        HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
        cache.SetCacheability(HttpCacheability.NoCache);

        base.OnResultExecuted(filterContext);
    }
}

Заголовки ответа будут различаться следующим образом:

Cache-Control:no-cache
Connection:Close
Content-Length:81836
Content-Type:application/json; charset=utf-8
Date:Mon, 29 Oct 2012 08:48:40 GMT
Expires:-1
Pragma:no-cache
Server:ASP.NET Development Server/11.0.0.0
X-AspNet-Version:4.0.30319
X-AspNetMvc-Version:3.0

Обратите внимание, что заголовок Cache-Control и Expires был добавлен. Это предотвратит любое кэширование как на сервере, так и на стороне клиента.

Глобально на сервере (OutputCache)

Но вместо того, чтобы создавать собственный фильтр, почему бы просто не использовать что-то существующее. ASP.net уже имеет встроенный механизм кэширования, называемый OutputCache . Это довольно мощный инструмент, и я скоро расскажу подробнее. В результате вы можете аннотировать ваш контроллер следующим образом

[OutputCache(Duration = 0)]
public class SomeController : Controller  {

}

чтобы предотвратить кеширование. Следовательно, заголовки ответа содержат заголовок Cache-Control: public, max-age = 0.

Внимание, вы также можете указать следующее в вашем файле web.config:

<caching>
    <outputCache enableOutputCache="false" />
</caching>

Однако это не помешает кешированию. Вместо этого это просто означает, что никакой механизм кэширования не должен применяться Просто отключив выходной кеш, мы получаем заголовки кеша по умолчанию, используемые ASP.net MVC, которые возвращаются к Cache-Control: private, таким образом снова открывая браузер возможность кешировать запросы.

На стороне клиента с помощью jQuery

Помимо отключения кэша на стороне сервера, у вас также есть возможность контролировать поведение кэширования на стороне клиента. Например , ajax в jQuery позволяет вам указать флаг кеша. Давайте посмотрим на его последствия. Выполнение AJAX-запроса с

$.ajax({
    type: 'GET',
    url: '/nation',
    ...
});

использует следующие заголовки:

GET /nation HTTP/1.1
Host: localhost:4120
Connection: keep-alive
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4
Referer: http://localhost:4120/frontend/accounts/index.html
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en,de;q=0.8,en-US;q=0.6,it;q=0.4
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

и ответы с

HTTP/1.1 200 OK
Server: ASP.NET Development Server/11.0.0.0
Date: Mon, 29 Oct 2012 08:54:35 GMT
X-AspNet-Version: 4.0.30319
X-AspNetMvc-Version: 3.0
Cache-Control: private
Content-Type: application/json; charset=utf-8
Content-Length: 81836
Connection: Close

Вместо этого, используя настройку cache: false like

$.ajax({
    type: 'GET',
    cache: false,
    url: '/nation',
    ...
});

заголовки запроса выглядят следующим образом

GET /nation?_=1351500913222 HTTP/1.1
Host: localhost:4120
Connection: keep-alive
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4
Referer: http://localhost:4120/frontend/accounts/index.html
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en,de;q=0.8,en-US;q=0.6,it;q=0.4
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

Параметры ответа не меняются. Но обратите внимание, что jQuery добавляет дополнительный случайно сгенерированный параметр запроса _ = …, который используется для принудительного аннулирования кэша.

Включить кеширование

Прежде чем я иллюстрировал, как отключить поведение кэширования. Теперь я хотел бы посмотреть, как это сделать.

На стороне сервера

Хотя вы можете легко написать свой собственный атрибут кэширования, используя фильтры действий, в этом нет необходимости. ASP.net имеет механизм OutputCache , который также предоставляет атрибут OutputCacheAttribute, который можно применять к действиям контроллера. Например, мы могли бы украсить наше действие контроллера как

[OutputCache(Duration=3, VaryByParam="*")]
public JsonResult Details(long id)
{
    //snip snip
    return Json(theResult, JsonRequestBehavior.AllowGet);
}

Это приводит к вводу следующих заголовков ответа:

HTTP/1.1 200 OK
Server: ASP.NET Development Server/11.0.0.0
Date: Mon, 29 Oct 2012 09:18:35 GMT
X-AspNet-Version: 4.0.30319
X-AspNetMvc-Version: 3.0
Cache-Control: public, max-age=3
Expires: Mon, 29 Oct 2012 09:18:37 GMT
Last-Modified: Mon, 29 Oct 2012 09:18:34 GMT
Vary: *
Content-Type: application/json; charset=utf-8
Content-Length: 81836
Connection: Close

Здесь интересны Cache-Control, Expires и Last-Modified.

Кэширование на стороне клиента

Мы могли бы также реализовать механизмы кэширования на стороне клиента. Я думаю об использовании методов, которые были сделаны доступными с HTML5, как

Что касается подхода localStorage, на GitHub есть хороший плагин jQuery, на который стоит обратить внимание: jQuery-ajax-jstorage-cache (также с форком от Paul Irish ).

Вывод: кеширование где и когда мне это нужно

В реальных приложениях вы, очевидно, ожидаете правильной комбинации отключения / включения кэша, где это необходимо. Это подразумевает возможность использовать подход, при котором вы определяете глобальное значение по умолчанию, которое может быть перезаписано при необходимости.

Подход, который я применил, состоит в том, чтобы принудительно отключить кэш на глобальном уровне, а затем включить его по мере необходимости. То есть все наши контроллеры наследуются от пользовательского класса BaseController, который выполняет некоторую дополнительную общую работу. Затем я аннотировал этот класс следующим образом

[OutputCache(Duration=0, VaryByParam="*")]
public class BaseController : Controller
{ ... }

В тех методах действия, где я явно желаю, чтобы поведение кэширования теперь я могу добавить атрибут OutputCache, чтобы обеспечить его:

public class SomeController : BaseController
{
    public JsonResult UnCachedAction(){ ... }

    [OutputCache(Duration=60, VaryByParam="id")]
    public JsonResult CachedAction(long id){ ... }
}

Это оказывается довольно мощным, так как количество пользовательского кода сохраняется на минимуме. Если вы заинтересованы в использовании механизма OutputCache, я бы посоветовал вам также взглянуть на пользовательские профили кэша .