Minimal API

Управління станом: HttpContext.Items та Cookies

Веб працює поверх протоколу HTTP, який за своєю природою не має стану (Stateless). Це означає, що сервер не пам'ятає вас між першим і другим запитом. Для сервера кожен запит — ніби від абсолютно нового користувача.

Управління станом: HttpContext.Items та Cookies

Веб працює поверх протоколу HTTP, який за своєю природою не має стану (Stateless). Це означає, що сервер не пам'ятає вас між першим і другим запитом. Для сервера кожен запит — ніби від абсолютно нового користувача.

Але реальні додатки потребують "пам'яті". Кошик покупок, статус авторизації, вибрана мова — усе це Стан додатку (Application State).

В ASP.NET Core існують різні способи зберігати дані, залежно від того, як довго вони мають жити: від мілісекунд (протягом одного запиту) до місяців (на комп'ютері клієнта).

У цьому розділі ми розглянемо ультракороткострокове зберігання (HttpContext.Items) та довгострокове зберігання на клієнті (Cookies).

1. Короткостроковий стан: HttpContext.Items

Життєвий цикл колекції Items дорівнює життевому циклу одного HTTP-запиту. Як тільки сервер відправив фінальну відповідь клієнту — колекція знищується назавжди.

Навіщо це потрібно?

Уявіть, що у вас є складний конвеєр (Pipeline) із кількох Middleware і одного Endpoint. Перший Middleware перевіряє JWT токен і дістає з бази даних об'єкт User (що займає час). Endpoint також потребує цього об'єкта User, щоб зрозуміти, яку інформацію повертати. Щоб не йти в базу даних вдруге, Middleware просто "кладе" об'єкт User у спільну кишеню поточного запиту — HttpContext.Items, а Endpoint його звідти "дістає".

Приклад використання

Program.cs
var app = WebApplication.Create();

// 1. Middleware "видобуває" дані
app.Use(async (context, next) =>
{
    // Симуляція запиту до БД або якоїсь важкої логіки
    var correlationId = Guid.NewGuid().ToString();

    // Зберігаємо дані в Items. Ключ - це рядок (або Type), Значення - будь-який object.
    context.Items["TrackingId"] = correlationId;

    await next(); // Передаємо керування далі
});

// 2. Інший Middleware може це прочитати
app.Use(async (context, next) =>
{
    if (context.Items.TryGetValue("TrackingId", out var trackingId))
    {
        Console.WriteLine($"Проходимо Middleware 2 з ID: {trackingId}");
    }
    await next();
});

// 3. Endpoint отримує дані і відправляє клієнту
app.MapGet("/my-id", (HttpContext context) =>
{
    // Завжди перевіряємо, чи є дані (out var) або кастуємо (as string)
    var myId = context.Items["TrackingId"] as string;

    return $"Ваш унікальний ID для цього запиту: {myId}";
});

app.Run();

Анатомія коду:

  • Колекція Items — це стандартний IDictionary<object, object?>. Оскільки значення є звичайним object, під час зчитування ми завжди маємо використовувати приведення типів (casting): as string або (string).
  • Цей словник не потокобезпечний (Not Thread-Safe) для запису з кількох паралельних потоків одного запиту, хоча такі ситуації трапляються рідко.

2. Довгостроковий стан на клієнті: Cookies (Куки)

Якщо Items вмирає через мілісекунди, то Cookies можуть жити місяцями. Це маленькі (до 4 КБ) фрагменти тексту, які сервер просить браузер зберегти у себе і потім відправляти назад з кожним наступним запитом.

Cookies ідеально підходять для налаштувань користувача (тема: світла/темна) або токенів сесії/аутентифікації.

Як працюють Cookies?

  1. Запит: Клієнт просить сторінку.
  2. Відповідь сервера: Сервер повертає сторінку + HTTP-заголовок Set-Cookie: theme=dark.
  3. Браузер: Зберігає у себе theme=dark.
  4. Наступний запит: Клієнт хоче зайти на іншу сторінку. Браузер сам додає HTTP-заголовок Cookie: theme=dark.
  5. Сервер: Читає його і малює темну сторінку.

Встановлення та читання Cookies

В ASP.NET Core ми працюємо з Cookies через об'єкти context.Response.Cookies (для запису) та context.Request.Cookies (для читання).

Program.cs
var app = WebApplication.Create();

// Зчитування
app.MapGet("/preferences", (HttpContext context) =>
{
    // Намагаємося прочитати куку "theme". Якщо її немає, беремо "light"
    string theme = context.Request.Cookies["theme"] ?? "light";
    return $"Поточна тема: {theme}";
});

// Встановлення
app.MapGet("/preferences/{theme}", (HttpContext context, string theme) =>
{
    // Створюємо налаштування для безпеки (важливо!)
    var cookieOptions = new CookieOptions
    {
        Expires = DateTimeOffset.UtcNow.AddMonths(1), // Життєвий цикл 1 місяць
        HttpOnly = true,  // JavaScript на клієнті не зможе прочитати куку
        Secure = true,    // Кука передаватиметься лише через HTTPS
        SameSite = SameSiteMode.Strict // Захист від CSRF атак
    };

    // Відправляємо браузеру команду зберегти куку
    context.Response.Cookies.Append("theme", theme, cookieOptions);

    return $"Тему успішно змінено на {theme}";
});

// Видалення
app.MapGet("/forget", (HttpContext context) =>
{
    // Сервер не може фізично видалити файл на комп'ютері клієнта.
    // Він просто відправляє куку з такою ж назвою і терміном дії "в минулому".
    context.Response.Cookies.Delete("theme");
    return "Тему скинуто до стандартної";
});

app.Run();

Анатомія коду: У класі CookieOptions є критично важливі налаштування безпеки:

  • HttpOnly: Встановлюйте завжди true, якщо ця кука потрібна тільки серверу (наприклад, токен входу). Якщо хакери знайдуть XSS вразливість (зможуть виконати свій JS код на вашому сайті), вони не вкрадуть токен через document.cookie.
  • Secure: Гарантує, що браузер взагалі не відправить вашу куку, якщо користувач сидить у публічному Wi-Fi кафе і підключився до вас через незахищений http://.
  • SameSite: Забороняє браузеру надсилати куку, якщо запит до вашого сервера був ініційований з іншого домену (наприклад, хтось створив кнопку-пастку "Тицьни сюди" на своєму сайті). Це захист від Cross-Site Request Forgery.
Законодавство європейського союзу (GDPR) і багатьох інших країн вимагає, щоб ви питали згоду (Cookie Banner) у користувача перед тим, як зберегти в його браузері будь-які куки, окрім суворо необхідних для роботи сайту (наприклад, ідентифікатор входу до системи). Куки аналітики (Google Analytics) чи налаштувань теми потребують згоди.

Практичні завдання

Таймер у Items Створіть кастомний Middleware, який на старті фіксує і записує DateTime.UtcNow у context.Items["StartTime"]. Створіть роут /time, який читає це значення з Items, вираховує скільки мілісекунд пройшло від StartTime до моменту генерації відповіді, і повертає цей час клієнту.
Copyright © 2026