Содержание
HTTP-кэширование (веб-кэширование), как и кэширование данных, является одним из способов повышения производительности веб-приложений. Управление HTTP-кэшированием ресурсов осуществляется с помощью заголовков запросов. Обычно выделяют два вида веб-кэшей – приватный (private cache) и кэш совместного использования (shared cache). Также, в различных источниках можно встретить такие названия как клиентский кэш, шлюзовый, прокси-кэш и так далее. Но, в любом случае, все эти виды кэшей так или иначе можно свести к первым двум видам: приватный (клиентский) кэш – это кэш, который доступен конкретному пользователю и хранится на его компьютере, а кэш совместного пользования, в свою очередь, может располагаться где-либо в Сети и быть доступным многим пользователям.
Что касается непосредственно ASP.NET Core, то здесь можно выделить два механизма HTTP-кэширования – это кэширование ответов (response caching) и кэширование выходных данных (output caching). Несмотря на то, что эти механизмы очень похожи между собой, всё же это два разных механизма кэширования, причем, если кэширование ответов — это довольно старый механизм кэширования, который появился ещё в ASP.NET Core 1.0 и используется до сих пор, то кэширование выходных данных — относительно новый механизм, появившийся в ASP.NET Core 7.0. И в этой статье мы попробуем разобраться в том, как работает http-кэширование в приложениях ASP.NET Core Web API и чем эти механизмы кэширования различаются друг от друга.
Кэширование ответов (response caching)
Кэширование ответов позволяет сократить количество дорогостоящих операций (например, чтение из базы данных) и реализует положения представленные в RFC 9111 «HTTP Caching». Как было сказано выше, управление HTTP-кэшированием осуществляется с использованием HTTP-заголовков и основной заголовок, используемый при кэшировании ответов – это Cache-Control, в котором указываются необходимые директивы кэширования. И прежде, чем мы приступим к непосредственной реализации механизма кэширования ответов в приложении Web API, стоит рассмотреть директивы этого заголовка и их значения. Основные директивы заголовка Cache-Control представлены в таблице ниже:
| Директива | Описание |
public |
Ответ может храниться в кэше |
private |
Ответ не должен храниться в кэше совместного использования. Частный кэш может сохранять и повторно использовать ответ. |
max-age |
Задаёт максимальное время (в секундах), в течение которого ресурс будет считаться актуальным. Примеры: max-age=60 (60 секунд), max-age=2592000 (1 месяц) |
no-cache |
Указывает на необходимость отправить запрос на сервер для валидации ресурса перед использованием закешированных данных. |
no-store |
Кеш не должен хранить никакую информацию о запросе и ответе |
Например, применительно к ASP.NET Core, заголовок: Cache-Control: public, max-age=10 означает, что ресурс кэшируется на сервере и время его «жизни» составляет 10 секунд, то есть по истечении 10 секунд кэшированная запись будет удалена и сервер вернет клиенту свежий ресурс API, например, полученный из базы данных.
Все представленные выше директивы заголовка мы можем использовать в приложении, используя специальный атрибут [ResponseCache], который имеет следующее описание:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple = false,
Inherited = true)]
public class ResponseCacheAttribute: Attribute, IFilterFactory, IOrderedFilter
{
…
}
То есть этот атрибут может применяться однократно как к отдельным методам, так и к классу (например, контроллера). У ResponseCacheAttribute определены следующие важные для нас свойства:
| Название | Тип | Описание |
CacheProfileName |
string? |
Имя профиля кэша |
Duration |
int |
Определяет значение директивы max-age заголовка Cache-Control сервера |
Location |
ResponseCacheLocation |
Определяет расположение кэша. Может принимать три значения:
|
NoStore |
bool |
Устанавливает для заголовка Cache-Control директиву no-store |
VaryByHeader |
string? |
Определяет значение для заголовка Vary ответа |
VaryByQueryKeys |
string[]? |
Принимает список имен параметров запроса, значения которых будут добавлены к ключу объекта в кеше. |
Чтобы продемонстрировать работу с response caching создадим новое шаблонное приложение ASP.NET Core Web API с использованием контроллеров. В созданном проекте имеется контроллер WeatherForecastController единственным действием Get(), которое возвращает массив объектов JSON. Вот результат этого действия мы и будем кэшировать.
Для начала добавим к действию контроллера необходимый атрибут:
[HttpGet]
[ResponseCache(Duration = 60, VaryByHeader ="User-Agent")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
Теперь перейдем в http-файл проекта и выполним имеющийся там запрос:
@WebApplication1_HostAddress = http://localhost:5171
GET {{WebApplication1_HostAddress}}/weatherforecast/
Accept: application/json
###
Если посмотреть на заголовки ответа, то мы увидим там следующие новые для нас заголовки:

На данный момент — это просто HTTP-заголовки, которые никак не изменяют работу нашего приложения. Для того, чтобы кэширование ответов заработало нам необходимо:
- добавить в приложение сервис кэширования ответов
- добавить в конвейер обработки запросов компонент middleware для кэширования ответов
Для этого перейдем в файл Program.cs проекта и внесем в него следующие изменения:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddResponseCaching();//добавляем сервис для кэширования ответов .... var app = builder.Build(); ... app.UseResponseCaching();//встраиваем в конвейер компонент middleware для кэширования ответов app.Run();
Так как на работу приложения влияет расположение компонентов middleware в конвейере обработки запросов, то, если в приложении используется CORS вызов UseResponseCaching() должен находиться после вызова метода UseCors(), например,
app.UseCors(); app.UseResponseCaching();
Теперь снова запустим приложение и дважды выполним запрос из http-файла. В результате выполнения первого запроса данные ответа будут записаны в кэш и сервер вернет в ответе заголовки как показано выше. При выполнении второго запроса мы увидим вот такой набор заголовков ответа
Заголовок Age содержит время в секундах, в течение которого объект находился в кэше, то есть, по наличию этого заголовка мы можем косвенно судить о том, что ответ действительно вернулся из кэша. Ради интереса, можете сравнить, например, данные второго ответа и третьего (если третий запрос произошел в течение 60 секунд после первого) и вы увидите, что данные ответов совпадают полностью, несмотря на то, что в действии Get() контроллера данные генерируются случайным образом.
Итак, мы добились от нашего приложения кэширования ответов. Представленный выше пример кэширования ответов демонстрирует модель управления кэшем, которая носит название «Модель истечения срока действия» (Expiration model). Это достаточно простая модель, которую можно представить следующим образом. Когда пользователь пытается первый раз получить доступ к ресурсу, который хранится, например, в базе данных, то выполняются следующие действия:

- пользователь выполняет запрос к приложению:
GET /api/tasks/1 - в этот момент в кэше нет записи с ключом, соответствующим методу и URL запроса, например,
https://localhost/api/tasks/1, поэтому приложение выполняет запрос к базе данных - полученные из БД данные записываются в кэш как пара «ключ-значение» где в качестве ключа по умолчанию выступает весь URL запроса, включая хост, порт, путь и метод запроса
- пользователю отправляется ответ, содержащий заголовок
Cache-Controlс директивами, которые мы определили в нашем приложении. В частности, заголовок содержит время жизни записи в кэше — на рисунке это значение равно 60 секунд
Если пользователь попытается выполнить точно такой же запрос в течение 60 секунд после первого запроса, то приложение будет работать следующим образом:

Как можно видеть по рисунку, приложение уже не обращается к базе данных и возвращает кэшированный ответ, отправляя клиенту также заголовок Age. Как только время существования записи в кэше истечет — весь процесс начнется заново — свежий ответ будет получен из БД, запишется в кэш и так далее. Теперь посмотрим на то, как мы можем настраивать кэширование ответов в ASP.NET Core
Настройка кэширования ответов
Для добавления сервисе кэширования ответов используется метод расширения IServiceCollection AddResponseCaching(), который имеет две версии
public static IServiceCollection AddResponseCaching (this IServiceCollection services); public static IServiceCollection AddResponseCaching (this IServiceCollection services, Action<ResponseCachingOptions> configureOptions);
Вторая версия метода принимает в качестве параметра делегат Action<ResponseCachingOptions>. У класса ResponseCachingOptions определены следующие свойства:
| Название | Тип | Описание |
MaximumBodySize |
long |
Максимальный размер кэшируемого тела ответа в байтах. По умолчанию установлено значение 64 Мб. Ответ с размером тела больше указанного размера не будет кэшироваться |
SizeLimit |
long |
Максимальный размер кэша, используемого компонентом middleware. По умолчанию установлено значение 100 Мб. При превышении этого размера ответы не будут кэшироваться до тех пор, пока не будут удалены старые записи. |
UseCaseSensitivePaths |
bool |
Указывает необходимо ли обрабатывать пути запроса с учётом регистра. По умолчанию пути обрабатываются без учёта регистра. |
Перейдем в файл Program.cs и настроим сервис кэширования ответов
builder.Services.AddResponseCaching(config =>
{
config.SizeLimit = 1024;
config.MaximumBodySize = 1024;
config.UseCaseSensitivePaths = false;
});
Теперь кэш может содержать не более 1 Кб данных.
Свойство VaryByHeader
Это свойство атрибута [ResponseCache]включает отправку заголовка Vary в ответе сервера. Этот заголовок часто используется для формирования ключа кэша. Для нас это означает следующее — пока указанный в Vary заголовок имеет одно и то же значение, клиент будет получать одну и ту же запись кэша. Как только заголовок запроса, указанный в Vary меняется — клиент получает новый ответ. Например, настроим кэширование ответа нашего действия следующим образом:
[HttpGet]
[ResponseCache(Duration =120, VaryByHeader ="User-Agent")]
public IEnumerable<WeatherForecast> Get()
{
//код метода
}
Теперь изменим запрос в http-файле проекта следующим образом:
GET {{WebApplication1_HostAddress}}/weatherforecast
Accept: application/json
User-Agent: MyProgram
Теперь, пока заголовок User-Agent имеет одно и то же значение — мы будем получать кэшированную запись ответа (до тех пор пока запись «жива»). Если же мы изменим значение заголовка, то получим новый не кэшированный ответ.
Свойство VaryByQueryKeys
Это свойство имеет аналогичный смысл, что и VaryByHeader, но используется применительно к параметрам запроса. Следует отметить, что для этого свойства не предусмотрено никакого HTTP-заголовка. Свойство представляет собой функцию HTTP, обрабатываемую компонентом middleware для кэширования ответов. Например, изменим параметры атрибута и действие Get() следующим образом:
[HttpGet]
[ResponseCache(Duration =120, VaryByQueryKeys = ["city"])]
public IEnumerable<WeatherForecast> Get(string? city)
{
//код метода
}
здесь мы определили и атрибута значение свойства VaryByQueryKeys, а у действия определили параметр city. Теперь, чтобы проверить работу приложения мы должны использовать в http-файле, например, такой запрос:
GET {{WebApplication1_HostAddress}}/weatherforecast?city=Moscow
Accept: application/json
Теперь для каждого значения параметра city будет создаваться своя копия в кэше. То есть приложение будет работать следующим образом:
| Запрос | Источник ответа |
| /weatherforecast?city=Moscow | Сервер |
| /weatherforecast?city=Moscow | Компонент middleware для кэширования ответов |
| /weatherforecast?city=Omsk | Сервер |
Свойство Location
Отдельное внимание стоит уделить свойству Location — местоположению кэша. Это свойство принимает одно из значений перечисления ResponseCacheLocation:
Any(0) – для заголовкаCache-Controlустанавливается директиваpublic(используется по умолчанию)Client(1) – для заголовкаCache-Controlустанавливается директиваprivateNone(2) – для заголовкаCache-Controlустанавливается директиваno-cache
По умолчанию используется значение Any, которое указывает, что кэш публичный, то есть несколько разных пользователей приложения на один и тот же запрос получают один и тот же кэшированный ответ. При работе с кэшированием ответов публичный кэш располагается в памяти сервера и изменить расположение кэша на сервере при этом мы не можем. Например, мы не можем сделать так, чтобы кэшированные ответы хранились не в памяти, а, например, в Redis. Этот момент стоит учитывать при кэшировании ответов.
Второе значение перечисления — Client говорит о том, что будет создаваться приватный кэш, то есть каждый пользователь будет иметь свою собственную версию кэша. И мы можем столкнуться с одним неприятным моментом, а именно — клиент может переопределить поведение сервера при кэшировании. Чтобы продемонстрировать, что имеется в виду, воспользуемся любым современным браузером, например, я воспользуюсь браузером Edge, который автоматически стартует при запуске приложения. Посмотрим на заголовки запроса и ответа:
Браузер автоматически отправляет на сервер заголовок Cache-Control: max-age=0, что приводит к тому, что сервер постоянно возвращает новый ответ клиенту даже, если в кэше будет содержаться актуальная версия ответа. Можете обновить несколько раз страницу в браузере и убедиться, что данные ответа меняются — мы не получаем кэшированную запись даже, если используем публичный кэш.
Третье значение перечисления — None говорит о том, что ответ не должен кэшироваться нигде.
Профили кэширования
Далеко не всегда удобно каждый раз переопределять одни и те же свойства атрибута [ResponseCache] для множества действий, особенно, если они располагаются в нескольких контроллерах. Профили кэширования позволяют объединять различные настройки кэширования и использовать в атрибуте вместо нескольких значений свойств одно — имя профиля.
Профили кэша настраиваются при добавлении сервиса для поддержки контроллеров следующим образом
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options =>
{
options.CacheProfiles.Add("Expire120", new CacheProfile()
{
Duration = 120,
Location = ResponseCacheLocation.Any,
VaryByHeader = "User-Agent"
});
});
...
здесь мы передали в метод расширения AddControllers() делегат Action<MvcOptions>. У MvcOptions имеется свойство CacheProfiles, используя которое мы добавляем новый профиль кэширования, который представляет из себя объект класса CacheProfile. Теперь, добавив новый профиль кэширования в приложение, мы можем использовать его имя в атрибуте кэширования:
[HttpGet]
[ResponseCache(CacheProfileName = "Expire120")]
public IEnumerable<WeatherForecast> Get()
{
//код метода
}
Если запустить приложение и выполнить запрос, то мы увидим следующие заголовки сервера:

Выводы по работе с кэшированием ответов
Итак, мы разобрались с тем как работает кэширование ответов в ASP.NET Core Web API. Как было сказано в самом начале, возможность кэширования ответов была ещё в ASP.NET Core 1.0 поэтому и сейчас кэширование ответов можно встретить в различных API. К сожалению, response caching не лишен недостатков, один из которых рассмотрен выше — браузер клиента может фактически отключить кэширование и серверу придётся выполнять фактически лишнюю работу по возвращению каждый раз свежего ответа. Второй недостаток — расположение кэша только в памяти. Поэтому при настройках кэширования ответов стоит внимательно относиться к значениям, определяющим размеры кэша и тела ответа для кэширования.
Кэширование выходных данных (output caching)
Кэширование выходных данных появилось в ASP.NET Core, начиная с .NET 7 и также используется для кэширования ответов сервера, но отличается от механизма response caching следующими важными для нас моментами:
- Поведение кэширования настраивается на сервере. При использовании кэширования выходных данных клиент не может переопределить поведение сервера: ресурс, который должен кэшироваться – будет записан в кэш вне зависимости от наличия в запросе заголовка
Cache-control: max-age=0. - Хранилище кэширования можно расширять. По умолчанию при кэшировании выходных данных используется кэширование в памяти, однако, при необходимости, мы можем изменить хранилище кэша и, например, хранить кэш в Redis. В случае же использования кэширования ответов (response caching) всегда используется память и изменить хранилище невозможно.
- Выбранные записи кэша можно программно сделать недействительными. При использовании кэширования выходных данных мы можем в любой момент вручную сбросить записи кэша.
- Блокировка ресурсов снижает риск массового скопления данных в кэше. Массовый отказ кэша возникает, когда часто используемая запись в кэше удаляется, и слишком много клиентов пытаются повторно добавить одну и ту же запись в кэш одновременно. Блокировка ресурсов гарантирует, что все запросы для данного ответа ожидают заполнения кэша первым запросом. В кэшировании ответов нет функции блокировки ресурсов.
- Повторная проверка кэша. Используя кэширование выходных данных, мы можем реализовать проверку элементов кэша с использованием заголовков
ETagиIf-No-Match. При этом такая проверка запускается при кэшировании выходных данных автоматически в ответ на запрос клиента, содержащего заголовокIf-No-Match.
Рассмотрим все эти преимущества подробнее. Что касается включения кэширования ответов, то на первом этапе, мы можем использовать настройки кэширования по умолчанию следующим образом. Вносим изменения в файл Program.cs
var builder = WebApplication.CreateBuilder(args);
//builder.Services.AddResponseCaching();
builder.Services.AddOutputCache();
builder.Services.AddControllers(/*options =>
{
options.CacheProfiles.Add("Expire120", new CacheProfile()
{
Duration = 120,
Location = ResponseCacheLocation.Any,
VaryByHeader = "User-Agent",
VaryByQueryKeys = ["city"]
});
}*/);
/*builder.Services.AddResponseCaching(config =>
{
config.SizeLimit = 1024;
config.MaximumBodySize = 1024;
config.UseCaseSensitivePaths = false;
});*/
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseCors();
//app.UseResponseCaching();
app.UseOutputCache();
app.UseAuthorization();
app.MapControllers();
app.Run();
В представленном коде наглядно показаны изменения в файле — мы закомментировали всё, что связано с response caching и добавили новый сервис и компонент middleware для кэширования ответов.
Теперь внесем необходимые изменения в контроллере
[HttpGet]
//[ResponseCache(Location =ResponseCacheLocation.Client, Duration =120, VaryByQueryKeys = ["city"])]
[OutputCache]
public IEnumerable<WeatherForecast> Get()
{
//код метода
}
Теперь можно запустить приложение и выполнить дважды запрос вида https://localhost[:port]/weatherforecast хоть в браузере, хоти из http-файла приложения и посмотреть снова на заголовки запроса и ответа. Вот как выглядят заголовки второго запроса и ответа в браузере:
В заголовках ответа сервера отсутствует заголовок Cache-Control, но, при этом присутствует заголовок Age. То есть, несмотря на то, что браузер попытался отключить кэширование — сервер всё же вернул запись из кэша. Так, немного изменив наше приложение, мы наглядно увидели первое преимущество кэширования выходных данных — поведение кэширования настраивается на сервере и браузер не может это поведение изменить. Теперь можно перейти к более детальному рассмотрению кэширования выходных данных.
Настройки кэширования выходных данных
Свойства OutputCacheOptions
Для настройки кэширования выходных данных мы можем воспользоваться второй версией метода AddOutputCache() в которую передать делегат вида Action<OutputCacheOptions>.
Класс OutputCacheOptions содержит больше возможностей настроек кэширования, чем ResponseCachingOptions. Используя OutputCacheOptions, мы можем также настраивать и политики кэширования. Вначале рассмотрим основные свойства этого класса
| Название | Тип | Описание |
DefaultExpirationTimeSpan |
TimeSpan |
Срок действия кэшированного ресурса. Если он не задан, то принимается по умолчанию равным 60 секундам. |
MaximumBodySize |
long |
Максимальный размер кэшируемого тела ответа в байтах. По умолчанию установлено значение 64 Мб. |
SizeLimit |
long |
Максимальный размер кэша, используемого компонентом middleware. По умолчанию установлено значение 100 Мб. При превышении этого размера ответы не будут кэшироваться до тех пор, пока не будут удалены старые записи. |
UseCaseSensitivePaths |
bool |
Указывает необходимо ли обрабатывать пути запроса с учётом регистра. По умолчанию пути обрабатываются без учёта регистра |
Как видите, практически все свойства этого класса имеют тот же самый смысл, что и аналогичные свойства у класса ResponseCachingOptions, за исключением свойства DefaultExpirationTimeSpan, которое для нас является новым. В своем приложении мы можем указать, например, такие настройки кэширования
builder.Services.AddOutputCache(options =>
{
options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(120);
options.MaximumBodySize = 600;
options.SizeLimit = 1024;
options.UseCaseSensitivePaths = false;
});
Теперь ресурсы будут сохраняться в кэше в течение двух минут, а максимальный размер кэша будет составлять 1 Кб. При этом время жизни ресурса в кэше будет применяться автоматически и нет необходимости указывать его в настройках атрибута [OutputCache].
Как было сказано ранее, используя OutputCacheOptions мы можем сразу же настраивать и политики кэширования, что позволяет не дублировать одни и те же настройки кэширования в атрибутах [OutputCache] действий контроллера, поэтому сразу же рассмотрим и методы класса OutputCacheOptions, чтобы в дальнейшем не отвлекаться на их описание
| Метод | Описание |
AddBasePolicy(Action<OutputCachePolicyBuilder>) |
Настраивает базовую политику кэширования. Если в атрибуте [OutputCache] не указана политика кэширования, то применяется базовая политика. |
AddPolicy(String, Action<OutputCachePolicyBuilder>) |
Добавляет именованную политику кэширования |
Как можно видеть, основным при настройке политики кэширования является класс OutputCachePolicyBuilder. Этот класс содержит довольно большое количество методов, с помощью которых настраивается конкретная политика кэширования. В основном эти методы управляют ключами записей в кэше и о них мы поговорим чуть позднее. Пока же рассмотрим пример настройки различных политик кэширования. Итак, добавим в наше приложение три политики кэширования:
builder.Services.AddOutputCache(options =>
{
options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(120);
options.MaximumBodySize = 600;
options.SizeLimit = 1024;
options.UseCaseSensitivePaths = false;
options.AddBasePolicy(policy =>
{
policy.Expire(TimeSpan.FromSeconds(30));
});
options.AddPolicy("Expire45", policy =>
{
policy.Expire(TimeSpan.FromSeconds(45));
});
options.AddPolicy("Expire120", policy =>
{
policy.Expire(TimeSpan.FromSeconds(120));
});
});
Здесь мы добавили три политики кэширования:
- Базовая политика: время существования записи в кэше ограничено 30-ю секундами
- Политика с именем «Expire45»: время существования записи в кэше ограничено 45-ю секундами
- Политика с именем «Expire120»: время существования записи в кэше ограничено 120-ю секундами
Во всех трех случаях мы воспользовались методом OutputCachePolicyBuilder.Expire()в параметрах которого указали интервал времени в течение которого запись в кэше считается актуальной. Теперь мы можем применить эти политики в нашем приложении. Например, применим политику «Expire45»
[OutputCache(PolicyName = "Expire45")]
public IEnumerable<WeatherForecast> Get()
{
//код метода
}
Теперь посмотрим более детально на методы класса OutputCachePolicyBuilder.
Методы OutputCachePolicyBuilder
Используя методы OutputCachePolicyBuilder, мы можем настроить ключи кэшированных записей. Ниже представлены основные методы OutputCachePolicyBuilder, которые мы можем использовать для настройки политик кэширования
| Метод | Описание |
SetVaryByHeader(String[]) |
для каждого набора заголовков, переданных в метод будет определяться своя версия кэша |
SetVaryByHost(Boolean) |
если в метод передается значение true, то для каждого хоста определяется своя версия кэша |
SetVaryByQuery(String[]) |
для каждого набора параметров строки запроса определяется своя версия кэша |
SetVaryByRouteValue(String[]) |
для каждого набора параметров маршрута определяется своя версия кэша |
Так, например, мы можем изменить политику кэширования в нашем приложении следующим образом:
builder.Services.AddOutputCache(options =>
{
options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(120);
options.MaximumBodySize = 600;
options.SizeLimit = 1024;
options.UseCaseSensitivePaths = false;
options.AddBasePolicy(policy =>
{
policy.Expire(TimeSpan.FromSeconds(30));
});
options.AddPolicy("Expire45", policy =>
{
policy.Expire(TimeSpan.FromSeconds(45));
policy.SetVaryByHeader(["User-Agent", "X-Version"]);
});
options.AddPolicy("Expire120", policy =>
{
policy.Expire(TimeSpan.FromSeconds(120));
});
});
Теперь при использовании политики кэширования с именем «Expire45» для каждого набора заголовков «User-Agent» и «X-Version» будет определяться своя версия кэша. Посмотрим как это будет работать в приложении. Для этого немного изменим http-файл проекта:
@WebApplication1_HostAddress = http://localhost:5171
GET {{WebApplication1_HostAddress}}/weatherforecast
Accept: application/json
User-Agent: MyProg
X-Version: 1
###
GET {{WebApplication1_HostAddress}}/weatherforecast
Accept: application/json
User-Agent: MyProg
X-Version: 2
###
GET {{WebApplication1_HostAddress}}/weatherforecast
Accept: application/json
User-Agent: MyProg_2
X-Version: 1
Здесь все три запроса имеют разные наборы заголовков. Теперь запустим приложение и проверим как будет работать кэширование. Обратите внимание на то, что для всех трех запросах будет создаваться своя версия кэша. Допустим у первого и третьего запроса полностью совпадает значение заголовка X-Version, но различается значение заголовка User-Agent — для этих запросов будет создано две версии кэша.
Аналогичным образом используются и другие методы класса OutputCachePolicyBuilder представленные в таблице выше.
Свойства класса OutputCacheAttribute
Как и в response caching, с помощью атрибута кэширования мы можем произвести аналогичные настройки кэширования используя свойства атрибута. Поэтому не будем заострять на этом моменте внимание, а просто перечислив свойства класса:
| Название | Тип | Описание |
Duration |
int | Срок хранения кэшированного ресурса. Аналогичное значение политики кэширования устанавливается с использованием метода Expire() |
VaryByHeaderNames |
string[]? | Для каждого набора заголовков, переданных в метод будет определяться своя версия кэша. Аналогичное значение политики кэширования устанавливается с использованием метода SetVaryByHeader() |
VaryByQueryKeys |
string[]? | Для каждого набора параметров строки запроса определяется своя версия кэша. Аналогичное значение политики кэширования устанавливается с использованием метода SetVaryByQuery() |
VaryByRouteValueNames |
string[]? | Для каждого набора параметров маршрута определяется своя версия кэша. Аналогичное значение политики кэширования устанавливается с использованием метода SetVaryByRouteValue() |
Пример того, как мы можем, используя свойства атрибута установить параметры кэширования:
[OutputCache(Duration = 45, VaryByHeaderNames = ["User-Agent", "X-Version"])]
public IEnumerable<WeatherForecast> Get()
{
//тут код метода
}
Удаление кэша вручную. Работа с метками
Одним из преимуществ кэширования выходных данных по сравнению с кэшированием ответов является возможность удаления выбранных записей кэша программно. Для того, чтобы это сделать, каждой записи кэша необходимо установить специальную метку (tag), используя либо метод класса OutputCachePolicyBuilder Tag(), либо свойством класса OutputCacheAttribute Tags. Воспользуемся свойством атрибута и перепишем наш контроллер WeatherForecastController следующим образом:
public class WeatherForecastController : ControllerBase
{
//здесь прочие поля класса
private readonly IOutputCacheStore _cache;
public WeatherForecastController(ILogger<WeatherForecastController> logger, IOutputCacheStore cache)
{
_logger = logger;
_cache = cache;
}
[HttpGet]
[OutputCache(Duration = 45, Tags =["forecast"], VaryByHeaderNames = ["User-Agent", "X-Version"])]
public async Task<IEnumerable<WeatherForecast>> Get()
{
//код метода
}
[HttpGet("reset")]
public async Task<IActionResult> ResetCache()
{
await _cache.EvictByTagAsync("forecast", new CancellationToken());
return Ok();
}
}
Во-первых, чтобы воспользоваться удалением кэша вручную мы должны каким-либо образом запросить сервис кэширования. В данном случае мы запросили сервис IOutputCacheStore, используя конструктор контроллера. Во-вторых, в атрибуте [OutputCache] мы определили метку кэшируемой записи — «forecast»
[OutputCache(Duration = 45, Tags =["forecast"], VaryByHeaderNames = ["User-Agent", "X-Version"])]
И, наконец, в-третьих, для удаления записей кэша с установленной меткой, мы добавили новое действие контроллера и воспользовались методом сервиса — EvictByTagAsync().
[HttpGet("/reset")]
public async Task<IActionResult> ResetCache()
{
await _cache.EvictByTagAsync("forecast", new CancellationToken());
return Ok();
}
Теперь можно добавить новый запрос в http-файл проекта для сброса кэша и проверить работу приложения. Вначале выполним несколько раз любой запрос на получение данных о прогнозе. Начиная со второго раза, мы будем получать ответ из кэша о чем нам будет говорить заголовок Age ответа сервера:
Теперь, если в течение жизни кэша (у нас это 45 секунд) мы выполним наш новый запрос и сбросим весь кэш с меткой «forecast», то при повторной попытке получения данных мы получим уже данные непосредственно из действия контроллера. а не кэшированную копию ответа.
Повторная проверка кэша
В отличие от Response Caching, при использовании кэширования выходных данных мы получаем возможность использовать вторую модель управления кэшем — модель валидации (validation model). В отличие от модели истечения срока действия (Expiration model) эта модель реализована в HTTP немного сложнее, поэтому вначале разберем её смысл.
При использовании модели валидации, первый запрос можно представить в виде следующей схемы:
Здесь для упрощения схемы я не стал показывать запрос приложения к БД для получения данных, но смысл от этого не теряется. Когда пользователь пробует запросить какой-либо ресурс API, которого ещё нет в кэше, то сервер возвращает его в своем ответе, который включает в себя специальные заголовки: ETag и/или Last-Modified.
Заголовок ETag является идентификатором определенной версии ресурса. Обычно, это каким-либо способом сгенерированный хэш ресура. Использование этого заголовка позволяет более эффективно использовать кеш и сохраняет пропускную способность, позволяя серверу отправлять не весь ответ, если содержимое не изменилось.
Заголовок Last-Modified определяет дату и время последнего изменения ресурса и также может использоваться для эффективного использования кэша и пропускной способности сервера.
После того, как пользователь получил ресурс, повторный запрос этого же ресурса должен выглядит следующим образом:
Пользователь в своем запросе использует новые заголовки — If-None-Match и/или If-Modified-Since.
Заголовок If-None-Match является условным. То есть сервер вернет ресурс только в том случае, если значение переданное в этом заголовке не соответствует значению, полученному для ресурса ранее. Грубо говоря, запрос пользователя можно интерпретировать так: «Дай мне ресурс Х, если его ETag отличается от моего». Если же ETag ресурса и значение в заголовке If-None-Match совпадает, то сервер возвращает пустой ответ с кодом статуса 304 Not Modified.
Что касается реализации этой модели при кэшировании выходных данных, то сервер автоматически реагирует на заголовки If-None-Match и If-Modified-Since. Всё, что от нас требуется — эт предусмотреть механизм генерации значения заголовка ETag. Например, изменим код действия Get() контроллера следующим образом:
[HttpGet]
[OutputCache(Duration = 45, Tags =["forecast"], VaryByHeaderNames = ["User-Agent", "X-Version"])]
public IEnumerable<WeatherForecast> Get()
{
var etag = $"\"{Guid.NewGuid():n}\"";
HttpContext.Response.Headers.ETag = etag;
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
Здесь для формирования значения для ETag мы используем обычный GUID и отформатированную строку передаем в заголовок:
var etag = $"\"{Guid.NewGuid():n}\"";
HttpContext.Response.Headers.ETag = etag;
Теперь добавим в http-файл проекта запрос следующего вида:
@etag = 8dfd1ed96b684c0288c8b643a5f2affd
GET {{WebApplication1_HostAddress}}/weatherforecast
Accept: application/json
If-None-Match: "{{etag}}"
здесь в переменную etag мы должны будем записывать значение, полученное при первом запросе ресурса. Теперь запустим приложение и запросим новый ресурс. Заголовки ответа сервера должны выглядеть следующим образом:
Теперь мы должны скопировать значение заголовка ETag (без кавычек) в http-файл, то есть текст запроса теперь должен быть таким:
Если выполнить этот запрос, то мы получим вот такой ответ сервера:
Учитывая то, что у нас также настроено время истечения срока кэша (см. атрибут OutputCache), получать ответ с кодом 304 в течение 45 секунд после чего запись кэша обновится, мы получим новый ответ с новым значением ETag.
Использование Redis для кэширования данных
Одним из неоспоримых преимуществ output caching является возможность расширения хранилища. В отличие от response caching мы уже не ограничены только памятью сервера — мы можем перенести весь кэш, например, в Redis.
Установка Redis на Windows
Для установки Redis на Windows необходимо перейти по этой ссылке на репозиторий в GitHub и скачать последний релиз. На момент написания этой статьи последней версией была Redis 5.0.14.1. При установке Redis можно оставить все настройки по умолчанию.
Установка nuget-пакета для работы с Redis
Чтобы использовать Redis для кэширования в output caching, необходимо установить в проекте пакет Microsoft.AspNetCore.OutputCaching.StackExchangeRedis.

Теперь настроим кэширование выходных данных на работу с Redis.
Настройка и использование Redis для кэширования выходных данных
Перейдем в файл Program.cs и настроим работу нового сервиса:
var builder = WebApplication.CreateBuilder(args);
// добавление кэширования
builder.Services.AddStackExchangeRedisOutputCache(options => {
options.Configuration = "localhost";
options.InstanceName = "local";
});
Теперь перейдем в папку установки Redis (по умолчанию — это C:\Program Files\Redis), запустим сервер (файл redis-server.exe)

Теперь запустим файл redis-cli.exe и выполним там команду keys * , чтобы убедиться, что на данный момент хранилище пусто
Проверим работу нашего приложения с Redis. Запустим приложение и выполним любой запрос на получение данных, которые у нас определены в http-файле. В результате этого запроса результаты выполнения запроса попадут в кэш. Снова перейдем в redis-cli и выполним операцию keys *

Как можно видеть, в кэше оказалось несколько записей. Чтобы узнать какой тип данных содержится в той или иной записи, необходимо выполнить операцию type [key], например
type local__MSOCT
в результате мы получим тип данных, который содержится в записи с ключом local__MSOCT. В зависимости от того, какой тип вернет результат выполнения этой операции, мы можем посмотреть конкретное содержимое той или иной записи в кэше, используя следующие операции в redis-cli:
- для «string»:
get <key> - для «hash»:
hgetall <key> - для «list»:
lrange <key> 0 -1 - для «set»:
smembers <key> - для «zset»:
zrange <key> 0 -1 withscores
Итого
В этой статье мы рассмотрели два варианта кэширования ответов сервера, которые мы можем использовать в ASP.NET Core Web API — это кэширование ответов (response caching) и кэширование выходных данных (output caching). Первый вариант кэширования используется, начиная с версии ASP.NET Core 1.0 и нашел широкое применение при разработке приложений в ASP.NET Core, несмотря на то, что по сравнение с output caching имеет ряд недостатков. Одним же из неоспоримых преимуществ output caching является возможность изменения хранилища кэша, что позволяет нам хранить кэш не только в памяти сервера, но и, например, организовать хранение в Redis. Кроме этого, в output caching реализован автоматический отклик сервера на запросы клиента, содержащие заголовки If-None-Match и/или If-Modified-Since.






