惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hugging Face - Blog
Hugging Face - Blog
V
Visual Studio Blog
Google DeepMind News
Google DeepMind News
爱范儿
爱范儿
博客园 - 叶小钗
WordPress大学
WordPress大学
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Apple Machine Learning Research
Apple Machine Learning Research
V
V2EX
有赞技术团队
有赞技术团队
腾讯CDC
Last Week in AI
Last Week in AI
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
S
SegmentFault 最新的问题
Jina AI
Jina AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
雷峰网
雷峰网
Recent Announcements
Recent Announcements
GbyAI
GbyAI
博客园 - Franky
IT之家
IT之家
Help Net Security
Help Net Security
The Register - Security
The Register - Security
C
Check Point Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
N
News | PayPal Newsroom
博客园_首页
L
LINUX DO - 最新话题
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Security Affairs
Y
Y Combinator Blog
MongoDB | Blog
MongoDB | Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LINUX DO - 热门话题
J
Java Code Geeks
O
OpenAI News
H
Help Net Security
Project Zero
Project Zero
S
Security @ Cisco Blogs
H
Hackread – Cybersecurity News, Data Breaches, AI and More
P
Palo Alto Networks Blog
Webroot Blog
Webroot Blog
The Hacker News
The Hacker News
人人都是产品经理
人人都是产品经理
T
The Blog of Author Tim Ferriss
AWS News Blog
AWS News Blog
M
MIT News - Artificial intelligence

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Кешируем отдачу картинок в .NET MVC Core
Алексей Садомов · 2026-06-17 · via Все публикации подряд на Хабре

Всем привет. В этой статье хочу поделиться опытом добавления клиентского кеширования картинок в ASP.NET MVC Core приложении. В мире SaaS экономия машинных ресурсов - актуальная задача, которая тем актуальнее, чем больше клиентов обслуживается на "единицу железа" (если можно так выразиться). Традиционно, генерация и отдача картинок на бэкенде - достаточно CPU и memory-емкие операции, и добавление клиентского кеша с помощью HTTP заголовков Cache-Control помогает снизить нагрузку на железо.

Допустим, у нас есть контроллер ImageController с действием View, которое умеет отдавать запрошенное изображение из бд, на лету изменяя его размеры, чтобы они не превышали переданных maxWidth и maxHeight:

public class ImageController : Controller
{
    [HttpGet]
    public ActionResult ViewResized(int id, int maxWidth, int maxHeight)
    {
        ...
        return new ImageResult { FileName = fileName, Content = memoryStream.ToArray() };
    }
}

(конкретную реализацию приводить не буду, т.к. статья в первую очередь о кэшировании, а не о работе с изображениями)

Мы хотим добавить клиентский кэш, добавив в ответ сервера заголовок Cache-Control. Это можно сделать несколькими способами:

  1. Декларативно, добавив аттрибут на действие

    // Добавляет заголовок: Cache-Control: public, max-age=60
    [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any)]
    public ActionResult ViewResized(int id, int maxWidth, int maxHeight)
  2. Изменив код самого действия

    public ActionResult ViewResized(int id, int maxWidth, int maxHeight)
    {
        Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue
        {
            Public = true,
            MaxAge = 60
        };
        ...
    }
  3. Добавив промежуточный слой (middleware) для кэширования

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

Код промежуточного слоя выглядит следующим образом:

internal class ImageCacheMiddleware: MiddlewareWithService
{
    private readonly RequestDelegate next;

    public ImageCacheMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        var settingsProvider = context.RequestServices.GetService<ISettingsProvider>();
        int imageCacheIntervalInSeconds = settingsProvider.Get("ImageCacheIntervalInSeconds");

        if (imageCacheIntervalInSeconds > 0)
        {
            context.Response.OnStarting(() =>
            {
                // add the header only if it hasn't been set by a controller already
                if (!context.Response.Headers.ContainsKey("Cache-Control"))
                {
                    context.Response.Headers.Append("Cache-Control", $"public, max-age={imageCacheIntervalInSeconds}");
                }
                return Task.CompletedTask;
            });
        }

        await next.Invoke(context);
    }
}

Сначала мы получаем экземпляр ISettingsProvider (наш интерфейс, абстрагирующий работу с хранилищем настроек. Конкретная реализация зависит от специфики приложения, поэтому его реализацию я здесь не буду приводить. Как уже было сказано выше, он может читать настроки из бд, файла, переменных окружения и т.д.) и получаем значение параметра ImageCacheIntervalInSeconds. Если он больше 0, то в обработчик начала отдачи ответа (Response.OnStarting) добавляем заголовок Cache-Control со значением

public, max-age={imageCacheIntervalInSeconds}

В этом примере мы используем директиву public, с которой ответ в дополнении к кэшированию на стороне браузера будет кешироваться также в промежуточных прокси и cdn. Если в вашем сценарии это не подходит, рассмотрите использование других директив (no-cache, no-store, private).

Далее нам понадобится метод расширения для добавления промежуточного слоя при старте приложения:

internal static class ImageCacheMiddlewareExtension
{
    public static IApplicationBuilder UseImageCacheMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ImageCacheMiddleware>();
    }
}

и собственно его добавление в Program.cs:

var builder = WebApplication.CreateBuilder(args);
...
var app = builder.Build();
...
app.UseWhen(
    context => context.Request.Path.ToString().ToLower().Contains("/image/view"),
    appBranch => {
        appBranch.UseImageCacheMiddleware();
    });

Отлично, слой кэширования добавлен и работает. Можно проверить это на вкладке Network браузера - первый ответ придет с устнавленным Cache-Control, последующие запросы и ответы будут отображаться со статусом cached.

Осталось покрыть его юнит-тестами. Для этого используем NUnit и популярную открытую библиотеку для создания заглушек тестирования Moq.

Для тестирования у нас два проблемных момента:

  1. Замокать получение времени действия кэша из бд

  2. Инициировать обработчик начала отдачи ответа с сервера

Для решения первой проблемы, нужно замокать цепочку вызовов, из которых последний вызов к тому же является методом-расширением:

HttpContext.RequestServices.GetService<T>()

Чтобы облегчить себе жизнь, немного изменим код промежуточного слоя - выделим получение экземпляра ISettingsProvider в отдельный виртуальный метод:

protected virtual T getService<T>(HttpContext ctx)
{
    return ctx.RequestServices.GetService<T>();
}

затем при тестировании создадим класс-наследник ImageCacheMiddlewareForTesting с переопределенной реализацией этого метода:

internal class ImageCacheMiddlewareForTesting: ImageCacheMiddleware
{
    private ISettingsProvider settingsProvider;
    public ImageCacheMiddlewareForTesting(RequestDelegate next, ISettingsProvider settingsProvider) : base(next)
    {
        this.settingsProvider = settingsProvider;
    }

    protected override T getService<T>(HttpContext context)
    {
        if (typeof(T) == typeof(ISettingsProvider))
            return (T)this.settingsProvider;
        throw new NotSupportedException();
    }
}

В конструктор этого класса будем передавать объект-заглушку (mock) с нужным нам поведением.

Для решения второй проблемы, используем средства библиотеки Moq - при добавлении делегата, запишем его в локальную переменную capturedCallback, и затем сэмулируем начало отдачи ответа прямым вызовом этого делегата. Выглядит это следующим образом:

[TestFixture]
public class TestImageCacheMiddleware
{
    [Test]
    public void test_WHEN_cache_lifetime_specified_THEN_it_is_added_to_headers()
    {
        // настройка - время кэша 1 сек
        var settingsProvider = new Mock<ISettingsProvider>();
        settingsProvider.Setup(x => x.Get(It.IsAny<string>())).Returns(1);

        var headers = new HeaderDictionary();
        var response = new Mock<HttpResponse>();
        response.Setup(x => x.Headers).Returns(headers);

        // записываем обработчик в локальную переменную capturedCallback
        Func<Task> capturedCallback = null;
        response.Setup(r => r.OnStarting(It.IsAny<Func<Task>>()))
            .Callback<Func<Task>>(callback => capturedCallback = callback);

        var ctx = new Mock<HttpContext>();
        ctx.Setup(x => x.Response).Returns(response.Object);

        var requestDelegate = new Mock<RequestDelegate>();
        var middleware = new ImageCacheMiddlewareForTesting(requestDelegate.Object, settingsProvider.Object);
        middleware.Invoke(ctx.Object).GetAwaiter().GetResult();

        // симуляция начала отправки ответа сервером
        if (capturedCallback != null)
        {
            capturedCallback().GetAwaiter().GetResult();
        }

        // проверяем, что заголовок кэширования был добавлен в заголовки ответа
        ClassicAssert.AreEqual(1, headers.Count);
        ClassicAssert.AreEqual("public, max-age=1", headers["Cache-Control"]);
    }

    [Test]
    public void test_WHEN_cache_lifetime_not_specified_THEN_it_is_not_added_to_headers()
    {
        // настройка - время кэша не указано
        var settingsProvider = new Mock<ISettingsProvider>();
        settingsProvider.Setup(x => x.GetValue(It.IsAny<string>(), 0)).Returns(0);

        var headers = new HeaderDictionary();
        var response = new Mock<HttpResponse>();
        response.Setup(x => x.Headers).Returns(headers);

        // записываем обработчик в локальную переменную capturedCallback
        Func<Task> capturedCallback = null;
        response.Setup(r => r.OnStarting(It.IsAny<Func<Task>>()))
            .Callback<Func<Task>>(callback => capturedCallback = callback);

        var ctx = new Mock<HttpContext>();
        ctx.Setup(x => x.Response).Returns(response.Object);

        var requestDelegate = new Mock<RequestDelegate>();
        var middleware = new ImageCacheMiddlewareForTesting(requestDelegate.Object, settingsProvider.Object);
        middleware.Invoke(ctx.Object).GetAwaiter().GetResult();

        // симуляция начала отправки ответа сервером
        if (capturedCallback != null)
        {
            capturedCallback().GetAwaiter().GetResult();
        }

        // проверяем, что заголовок кэширования не был добавлен в заголовки ответа
        ClassicAssert.AreEqual(0, headers.Count);
    }
}

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