Кеширование публичного ответа
Публичный список курсов часто читается повторно, а изменяется редко. Сервер может сохранить готовый ответ и временно отдавать его без повторного формирования. Но кеш должен различать варианты и переставать использовать устаревшие данные после изменения. Если в общую запись попадут данные редактора, ускорение превратится в утечку. Рассмотрим небольшой кеш именно публичного ответа.
lesson-28/after архива продолжения — отдельная loopback-ветка с четырьмя курсами в памяти. Она не продолжает cookies и приватные файлы уроков 21–24. Изменение данных выполняется только вручную через остановку и новый запуск, административный HTTP-endpoint не открыт. Сборка приложения, обращения и измерения скорости не выполнялись. Ожидаемый эффект касается повторного ответа, а не доказанного ускорения ProfessorWeb.
Middleware и политика решают разные части
Регистрация AddOutputCache добавляет сервисы, UseOutputCache включает middleware. Сами вызовы не означают, что каждый ответ автоматически кешируется: endpoint получает явную политику через CacheOutput. Это помогает видеть, какие ресурсы допустимо хранить.
builder.Services.AddOutputCache(options =>
options.AddPolicy("PublicCatalog", policy =>
policy.Expire(TimeSpan.FromSeconds(30))
.SetVaryByQuery("topic")
.Tag("catalog-public")));
Политика содержит время тридцать секунд, выбранный query topic и tag для связанных записей. Endpoint проверяет строго frontend, publishing или отсутствие темы. Неизвестная тема получает 400. Варианты фильтра не должны делить одну запись: frontend содержит три курса и 48 уроков, publishing — один и 12.
Порядок в приложении без authentication простой: routing, проверка query, output cache, endpoint. Если кеш подключается к приложению с authentication и authorization, middleware ставится после обеих границ. Иначе опасно отдавать сохранённое представление до проверки пользователя. Это требование конвейера не отменяет необходимости выбрать только действительно публичные endpoints.
Общий ответ не содержит личных полей
Ветка отдаёт только id, title, topic и lessons. В ответе нет owner, internal_notes, CSRF token, session cookie и персональных рекомендаций. Поэтому разные анонимные посетители должны получить одинаковый публичный результат для одинаковой темы. Полная модель из прошлых уроков не сериализуется случайно целиком.
По умолчанию output caching имеет ограничения для методов, статусов, cookies и authenticated requests. Однако изменение политики способно изменить эти условия; нельзя опираться только на название механизма. Перед добавлением кастомного policy нужно проследить, какие данные формирует ответ и по каким признакам он разделяется.
У нашего endpoint допустим один параметр topic; повторные значения и неизвестные query-поля отклоняются отдельной границей до cache lookup. Успешная запись не выдаётся до этой проверки. Разрешённый ключ содержит тему, а неправильный запрос не исправляется до допускающего успех значения.
Проверка привязана к выбранному endpoint, которому WithName("PublicCatalogList") назначает устойчивое имя. После UseRouting middleware получает это имя из context.GetEndpoint()?.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName; интерфейс находится в пространстве имён Microsoft.AspNetCore.Routing. Сравнение строки Request.Path здесь недостаточно: routing способен выбрать тот же обработчик для /api/courses и /api/courses/. Поэтому обе формы адреса проходят один query guard до кеша. Несуществующий маршрут без этого endpoint продолжает обычную обработку и не превращается в ошибку каталога. Имя должно оставаться уникальным в приложении; при переименовании endpoint одновременно меняется условие guard. Получение endpoint и его metadata описано в документации routing, контракт имени — в IEndpointNameMetadata.
В ответ включён учебный X-Catalog-Generation, создаваемый handler. Для двух одинаковых запросов внутри жизни записи ожидается одно значение, а для другого варианта — своя генерация. Это наблюдаемый способ отличить повторную выдачу от нового формирования без вымышленных секунд экономии. Значение не содержит ID процесса, секретов и идентичности редактора.
Инвалидирование относится к изменению данных
Tag catalog-public позволяет удалить все связанные варианты через IOutputCacheStore.EvictByTagAsync. Если приложение в будущем примет изменение курса, эвикция должна происходить после успешной фиксации предметной записи. Отказ валидации или rollback не должен изображаться как новое состояние каталога.
await store.EvictByTagAsync("catalog-public", token);
В полном проекте этот вызов находится в отдельном сервисе CatalogCacheInvalidation, но публичный административный endpoint его не вызывает. Файл сценария объясняет, где его подключать при интеграции с настоящей авторизованной командой. Мы не публикуем беззащитный /clear-cache ради лёгкого упражнения.
Даже правильная эвикция не является общей транзакцией с базой. Процесс может упасть после commit и до удаления кеша. Другой запрос может одновременно формировать старое представление. Для строгого требования свежести понадобится версия данных, распределённое событие или другая согласованная модель, а короткий TTL только ограничит часть периода устаревания.
Тридцать секунд — учебный срок. Он не означает, что любое приложение приемлет такую задержку. Публичный список материалов может допускать небольшое устаревание, а личный баланс или состояние доступа — иметь совсем другие требования. Важно сформулировать предел и проверить его с продуктом до выбора числа.
Кеш экземпляра и кеш браузера различаются
Output cache хранит представление на сервере. Это не обязательно тот же механизм, что браузерный HTTP-кеш или CDN. Заголовки Cache-Control определяют другую границу и не должны случайно разрешить публичную раздачу приватного ответа. Наличие серверного кеша не является инструкцией сохранять сессии в shared CDN.
Ветка использует обычное хранилище процесса. Два экземпляра сервера имеют отдельные записи и отдельную эвикцию. Если выбрать распределённый store, понадобится согласование доступности, ключей и удаления; установка пакета сама по себе не докажет корректность. Перезапуск нашей маленькой ветки очищает кеш, поэтому новое поколение ответа ожидается даже при неизменном наборе.
Resource locking в output cache помогает снизить одновременное формирование одной записи, но не заменяет предметные блокировки базы. Сохранение HTTP-ответа и принятие редакторской команды остаются разными действиями. Нельзя использовать cache key как optimistic-lock версию курса из урока 19.
Для последующего самостоятельного сценария запросите all дважды, затем frontend дважды, затем publishing. Сравните конкретные ID и X-Catalog-Generation. Ожидаются четыре, три и один курс; повтор совпадающего варианта до истечения TTL должен использовать сохранённую генерацию. После истечения или перезапуска handler формирует новую. Неправильные ?topic=unknown, ?unexpected=1 и ?topic=frontend&topic=publishing должны получить 400 как у /api/courses, так и у /api/courses/, при пустом и наполненном кеше. Отдельное поколение для каждой формы URL допустимо: общее правило валидации не обещает общий cache key для разных путей. Измерения производительности понадобятся отдельно, а сейчас результат — корректно разделённый публичный ответ.
Политики, default conditions и порядок middleware описаны в документации Output Caching ASP.NET Core 10.
При выборе query cache key особенно важно не пропустить неверный запрос до handler. В этой ветке отдельное middleware проверяет допустимые ключи и ровно одно значение темы перед UseOutputCache. Поэтому неизвестный параметр не получает сохранённый успех только из-за совпадения выбранного topic. Правильный ответ и неправильная форма запроса остаются различимыми даже после наполнения кеша. При добавлении нового фильтра нужно одновременно изменить эту валидацию, правило разделения и публичный endpoint. Иначе разные варианты способны попасть в одну запись. Это наблюдаемый контракт, который в будущем проверяется сначала при пустом кеше, затем при уже сохранённом ответе каждого разрешённого варианта.