Залишати стан між запитами у ядрі ASP.NET: Практичний, Безмовний Посібник (MVC, Сторінки Razor, Мінімальний API) (Українська (Ukrainian))

Залишати стан між запитами у ядрі ASP.NET: Практичний, Безмовний Посібник (MVC, Сторінки Razor, Мінімальний API)

Sunday, 09 November 2025

//

51 minute read

UDATE (2005- 11- 10): Додали більш практичні приклади з моєї фактичної бази коду, що показує шаблони реального світу, компроміси у сфері торгівлі, отримання Чечі та еволюцію від простого до складного керування державою. Буде додано детальні шаблони IMemoryCache, Використання ViewBag (доброї і поганої), стратегії реагуванняCache/OutputCache, і уроки, отримані з виробництва.

Вступ

За допомогою HTTP ви вже не можете бути зареєстровані. Програма... не є такою. Користувачі підписують, додають елементи до кошиків, перестрибують між сторінками, повертаються завтра і очікують, що ви пам' ятатимете про це. У ядрі ASP. NET (MVC, Porrent Pages, Limum API), існує багато способів зберігати і передавати стан між запитами. Деякі з них стосуються лише одного запиту. Деякі з них є останніми для сеансу. Деякі з них живуть у клієнті. Деякі з них розподіляються і витримують перезапуски сервера. Кожен з варіантів має обмін між безпекою, швидкодією, масштабом і розробником ergoomicals.

Цей допис каталогізує варіанти, показує бетон, копіює придатні для копіювання приклади у всіх трьох стоях, і дає вам вказівки для прийняття рішення, щоб ви могли вибрати правильний інструмент для роботи.

ЗАУВАЖЕННЯ: це частина моїх експериментів з ШІ (абсолютним чернетом) + моє власне редагування. Той самий голос, той самий прагматізм, лише швидші пальці.

Альбомна на полі

flowchart LR
  subgraph Client
    Q[Query String]
    R[Route Values]
    H[Headers]
    F[Form/Hidden Fields]
    CK[Cookies]
    LS[Local/Session Storage]
  end
  subgraph Server
    I[HttpContext.Items<br/>\nper request only]
    TD[TempData<br/>\none redirect]
    SS[Session]
    MC[IMemoryCache]
    DC[IDistributedCache]
    AU[Auth Cookie / Claims]
    JT[JWT / Bearer]
    DB[Database / Durable Store]
    BUS[Outbox / Queue]
  end
  Q --> Model[Model Binding]
  R --> Model
  F --> Model
  H --> Model
  CK --> App[Your Code]
  Model --> App
  App -->|Set| CK
  App -->|Set| TD
  App -->|Set| SS
  App -->|Set| MC
  App -->|Set| DC
  App -->|Issue| AU
  App -->|Issue| JT
  App -->|Persist| DB
  • Клієнтська карта: запит, маршрут, заголовки, форми, куки, JWT. Масштабується горизонтально, але його буде показано клієнтові (мусить бути перевірений/ підписаний/ зашифрований, якщо потрібно).
  • Скарбовано на сервері: PmpData, Session, caches, DB. Потрібна стратегія прив' язки або дистрибутива.
  • Per- request: HtpContext. Items - корисний для внутрішньої передачі даних протягом одного запиту.

Золоті правила (до того, як ми зануримося в API)

  1. По-перше, витік інформації від держави - це реальна загроза. Стан на сервері (сеанс, в пам'яті) може текти між користувачами через помилки в кодуванні, стан раси, або неправильне управління життєвим циклом. Взірці без стороннього циклу не є просто більш об'ємними, ніж є більш безпечним.
  2. Надавайте перевагу шаблонам сервера без стану, якщо вам потрібно змінити розмір у горизонтальному (відповідний стан клієнтських жетонів, довготривалих крамниць або розподілених кешів).
  3. Ніколи не довіряйте клієнтському стану. Перевірте, підпишіть і/ або зашифруйте.
  4. Тримати великі дані поза куками і заголовками; вони зіб' ють всі запити.
  5. Скористайтеся SpmData лише для надсилання повідомлень після- redirect- Get (PRG) один раз, подібно до повідомлень flash.
  6. Використовуйте сеанс тільки тоді, коли ви повинні підтримувати сервер на стороні розмовний стан і ви запланували дистрибуцію і бути параної про безпеку.
  7. Твердження стосуються ідентичності і грубих уповноважень, а не загального стану програм.
  8. Кеш не є джерелом правди.

Стан перевірки: HtpContext.Items

  • Область видимості: лише поточний запит (в кінці трубопроводу)
  • Використовувати для: передавання обчислюваних значень між середньою та кінцевими точками/контролерами
  • Масштаб: без зміни масштабу
  • Безпека: лише сервер
sequenceDiagram
  participant M as Middleware
  participant E as Endpoint/Controller
  M->>M: Compute TenantId
  M->>E: HttpContext.Items["TenantId"] = 42
  E->>E: Read Items["TenantId"]

Приклад middleware (всі стоси):

app.Use(async (context, next) =>
{
    var tenantId = context.Request.Headers["X-TenantId"].FirstOrDefault() ?? "public";
    context.Items["TenantId"] = tenantId;
    await next(context);
});
  • Мінімальна кінцева точка API:
app.MapGet("/whoami", (HttpContext ctx) => new { Tenant = ctx.Items["TenantId"] });
  • Контролер MVC:
public IActionResult WhoAmI() => Json(new { Tenant = HttpContext.Items["TenantId"] });
  • Обробник сторінок Razor:
public IActionResult OnGet() => new JsonResult(new { Tenant = HttpContext.Items["TenantId"] });

Стан трансцендентного клієнта: значення маршруту і рядок запиту

  • Область видимості: поточний запит; клієнт несе його явно.
  • Використовувати для: навігації контексту, фільтрування, пагінії, профілю виконавців
  • Безпека: має перевіряти/ адмініструвати; не вбудовувати секрети

Приклади

  • Мінімальні API:
app.MapGet("/orders/{id:int}", (int id, int? page) => Results.Ok(new { id, page }));
// GET /orders/5?page=2
  • MVC:
[HttpGet("/orders/{id:int}")]
public IActionResult Details(int id, int? page)
  => View(new { id, page });
  • Сторінки Razor (Orders/Details. cshtml. cs):
public IActionResult OnGet(int id, int? page)
  => Page();

Створення посилань, які зберігають стан:

// Razor Pages
<a asp-page="/Orders/Details" asp-route-id="@Model.Id" asp-route-page="@Model.Page">Next</a>

// MVC
@Html.ActionLink("Next", "Details", "Orders", new { id = Model.Id, page = Model.Page }, null)

Заголовки: ІД кореляції, ключі ідепотентності, прапорці можливостей

  • Область дії: поточний запит, за бажання, відлуння у відповідях
  • Використовувати для: відстеження, повторної безпеки, прапорців A/B
  • Безпека: обробляти як ненадійні вхідні дані, valid/whitelist
app.Use(async (ctx, next) =>
{
    var correlationId = ctx.Request.Headers["X-Correlation-Id"].FirstOrDefault()
                      ?? Guid.NewGuid().ToString("n");
    ctx.Response.Headers["X-Correlation-Id"] = correlationId;
    await next(ctx);
});

Ідепотентність (взір):

flowchart TD
  C[Client POST /pay\nIdempotency-Key:k] --> S{Seen k?}
  S -- No --> E[Execute charge]
  E --> P[Persist result by k]
  P --> R[Return 200 + result]
  S -- Yes --> L[Load result by k]
  L --> R

Форми і приховані поля (PRG)

  • Область видимості: лише наступний запит (поштові дописи відсилаються до даних)
  • Використовувати для: майстерні кроки, анти- застарілі жести, зберігаючи невеликі біти стану через PRG
  • Безпека: завжди перевіряти; об' єднувати з антиforgery

Шаблон PRG на сторінках MVC/Razor:

sequenceDiagram
  participant U as User
  participant P as POST Action
  participant R as Redirect
  participant G as GET Action
  U->>P: POST form
  P-->>R: 302 Redirect
  U->>G: GET redirected
  G-->>U: Final page (no resubmits)
  • MVC:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Save(SettingsModel model)
{
    // validate & persist
    return RedirectToAction(nameof(Summary), new { tab = model.SelectedTab });
}
  • Сторінки Razor:
public IActionResult OnPost(SettingsModel model)
{
    return RedirectToPage("/Settings/Summary", new { tab = model.SelectedTab });
}

Куки: мала, підписана, іноді зашифрована

  • Область видимості: кожен запит від навігатора до завершення роботи
  • Використовувати для: параметри, прапорці без чутливості, дозвіл; куки розпізнавання (вимірювання розділу)
  • Підняття торгівлі: обмеження розміру (~4КБ для кук), вплив швидкодії, зобов' язання з законами згоди

Приклад мінімального API:

app.MapPost("/prefs/theme/{value}", (HttpContext ctx, string value) =>
{
    ctx.Response.Cookies.Append("theme", value, new CookieOptions
    {
        HttpOnly = false,
        Secure = true,
        SameSite = SameSiteMode.Lax,
        Expires = DateTimeOffset.UtcNow.AddYears(1)
    });
    return Results.Ok();
});

app.MapGet("/prefs/theme", (HttpContext ctx)
  => Results.Text(ctx.Request.Cookies["theme"] ?? "system"));

Використання сторінок MVC/Razor ідентичне за допомогою HttpContext.

Для того, щоб захистити вантажі, які ви поклали у куки, скористайтеся системою захисту даних ASP. NET.


PowerData: Однонапрямна шина повідомлення

  • Область: переживає одне переспрямування
  • Сховище повернення: Кука (типова) або Сеанс
  • Використовувати для: flash messages, резюме перевірки після PRG

Налаштування (Program.cs):

builder.Services.AddControllersWithViews().AddSessionStateTempDataProvider(); // optional
builder.Services.AddSession();
var app = builder.Build();
app.UseSession();

У контролері MVC:

TempData["StatusMessage"] = "Saved!";
return RedirectToAction("Index");

У обробнику сторінки Razor:

TempData["StatusMessage"] = "Saved!";
return RedirectToPage("/Index");

На панелі перегляду/ сторінки:

@if (TempData["StatusMessage"] is string msg) {
  <div class="alert alert-success">@msg</div>
}
flowchart LR
  A[POST /save] -->|TempData set| B[302 Redirect]
  B --> C[GET /index]
  C -->|TempData read consumed| D[Render message]

Сеанс: Стан спілкування на сервері

  • Область видимості: сеанс навігатора (клавіша клавіша + сховище серверів)
  • Використовувати для: багато- крокових чарівників, невеликих даних візків, трепетних лічильників
  • Підвищення торгівлі: вимагають липких сеансів або розподіленого запасу підтримки; може обмежувати масштаб

Налаштувати:

builder.Services.AddDistributedMemoryCache(); // or AddStackExchangeRedisCache
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(20);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});
var app = builder.Build();
app.UseSession();

Використання списку сеансів:

app.MapPost("/cart/add/{id:int}", (HttpContext ctx, int id) =>
{
    var key = "cart";
    var bytes = ctx.Session.Get(key);
    var list = bytes is null ? new List<int>() : System.Text.Json.JsonSerializer.Deserialize<List<int>>(bytes)!;
    list.Add(id);
    ctx.Session.Set(key, System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(list));
    return Results.Ok(list);
});

Допоміжні засоби сеансу:

public static class SessionExtensions
{
    public static void Set<T>(this ISession session, string key, T value)
      => session.SetString(key, System.Text.Json.JsonSerializer.Serialize(value));

    public static T? Get<T>(this ISession session, string key)
      => session.TryGetValue(key, out var data)
         ? System.Text.Json.JsonSerializer.Deserialize<T>(data)
         : default;
}

Застережлива історія: коли сеанс стає вузьким,

Колись я працював над масивним проектом уряду Великобританії. все до сеансу: параметри користувача, дані про багато- крокові форми, результати пошуку, тимчасові обчислення, навіть кешовані пошуки, які повинні бути у належному кеші або базі даних.

Проблема: Стан сеансу було збережено у процесі (ASP. NET session state in web. config, це були попередні дні). Всі запити повинні були усувати масивні об' єкти сеансів. З збільшенням навантаження, стан сеансу зріс до десятків мегабайтів на користувача. З тисячами регулярних користувачів сервери закінчувалися пам' яттю.

Відчайдушне рішення: Ми полетіли до офісу HP в Штутгарті, щоб провести випробування вантажу на їхній супер-фунтовий час, Найпотужніша європейська Windows-машинаЦе був звір: десятки ітанічних процесорів, сотні гігабайтів оперативної пам'яті.

Результат: Навіть на Superdomon, ми не могли влучити в ціль поточного користувача. Архітектура стану сесії була фундаментально зламана. Вертикальне масштабування не може зберегти поганий дизайн. Сеанс Серіалізація/деперація над головою, поєднана з тиском пам' яті від масивних об' єктів сеансів, це означало, що система просто не може обійтися без будь- яких розумних витрат.

Кошмар в охране: Гірше ніж проблеми з швидкодією, ми виявили помилку у кодуванні, яка спричинила стан сеансу до витікання між користувачами. Користувача Дані сеансу A час від часу з' являтимуться у сеансі користувача B. Це не було просто смішно. Користувачі були у катастрофічному стані. Обладнання NHS Ми випадково створили механізм захисту даних, який міг викривати чутливу медичну інформацію на різних сеансах охорони здоров'я.

Що мало статися:

  1. Типово, без збереження стану: Більшість даних сеансу ніколи не повинно було існувати
  2. База даних для тривалого стану: Поступ форми з декількома кроками має бути в базі даних з ідентифікатором процесу обробки
  3. Кеш для пошуку: Спільне пошукування належало IMoryCache або розподіленому кеші
  4. Поруч з клієнтом для уподобань: Користувачем може бути куки або локальне сховище
  5. Розподілити сеанс, якщо потрібно: Якби сеанс був справді потрібен, то сеанс з Redis був би спільним для виконання.

Урок: Стан сеансу не масштабується вертикально і заледве горизонтально (навіть з липкими сеансами або розподіленими крамницями, ви все ще синхронізуєте або вилучаєте всі запити). Експерименти Superdomon довели, що використання обладнання у архітектурних проблемах є дорогим і часто марним.

Але важливіше: Жуки стану сеансу стають вразливими до безпекиУ середовищі охорони здоров'я (або банківських банків, або будь-якої регульованої індустрії) ці дані є жахом і потенційними кримінальними зобов'язаннями.

Сучасна порада:

  • Якщо вам потрібно більше, ніж кілька KB сеансів даних, ви, ймовірно, маєте проблему з дизайном
  • Якщо ви зберігаєте чутливі дані в сеансі, ви створюєте поверхню атаки безпеки.
  • Нетерпляча архітектура не просто шкала краще означає, що вона більш захищена, тому що немає стану сервера, щоб протікати.
  • Обдумай свою стратегію управління державою перед тим, як полети до Штутгарта (або объясниш порушення даних комісару інформаційної інформації)

Caching: IMemoryCache і IDistribedCache

  • Область видимості: процес сервера (IMemoryCache) або розподілений (IDistribedCache)
  • Використовувати для: похід/компонований дані, пошук, стан короткочасності
  • Торгівля: нечинність кешу, послідовність для розподілення

IMemoryCache:

builder.Services.AddMemoryCache();

app.MapGet("/rates", (IMemoryCache cache) =>
{
    var key = "fx:usd:eur";
    if (!cache.TryGetValue(key, out decimal rate))
    {
        rate = 0.92m; // pretend fetch
        cache.Set(key, rate, TimeSpan.FromMinutes(5));
    }
    return Results.Ok(rate);
});

IDistribedCache (наприклад, Reredis):

builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");

app.MapGet("/feature/{name}", async (IDistributedCache cache, string name) =>
{
    var val = await cache.GetStringAsync($"feat:{name}");
    return Results.Text(val ?? "off");
});

Приклад кешу: якщо він має бути тривалим або допустимим, зберігати його у базі даних і за бажання кешувати його.

Вибір між IMemoryCache і IDistribedCache

  • IMemoryCache:
    • Швидка розгладжування, в процесі, об' єкти залишаються як об' єкти (без послідовності).
    • В' язкість за тиском пам'яті, обмеженням розміру, абсолютним або залишковим терміном і пріоритетом.
    • Не об' єднані між вузлами; очищені під час рецидиву/розробки програм.
    • Чудово підходить для окремих гарячих наборів, обчислених пошуків, коротких TTL.
  • IDistribedCache (Redis/SQL/etc.):
    • Спільне використання на фермі; переживає перезапуск програм; потребує послідовної зміни (рядки/ байти).
    • Легке застарівання; облік часу залежить від мережі і сервера.
    • Підтримує абсолютну або застарілу термінологію (залежну від програми; Залежну від постачальника Redis оновлює TTL під час доступу до просування).
    • Ідеально для послідовності поперемінних, великого читання віяла, функцій прапорців та сеансу.

Загальні стратегії кешування

  • Кеш- aide (більшість поширених):

    1. Спробуйте виконати пошук кешу; 2) у разі помилки, завантаження з вихідних кодів; 3) запису до кешу; 4) повернення до кешу.
    • Pros: проста; джерело правди залишається базою даних.
    • Cons: перший запит після завершення строку дії є повільним; можливі штампи.
  • Прочитане (за допомогою засобу керування бібліотекою/ оптимізатором): засіб для роботи з кешем під час завантаження промахи.

  • Запис- через: запис йде до кешу і резервного сховища синхронно.

  • Запис- behind: запис до кешу, злив, щоб зберегти асинхронно (ймовірно: втрата/ несумісність).

  • Оновити: освіжити гарячі ключі до того, як вони застаріють, щоб уникнути холодних промахів.

Вигнання, виселення та сечі

  • Абсолютна строк дії: завжди закінчується після фіксованої тривалості (добре для свіжості зовнішніх даних).
  • Стирання: збільшує строк доступу до TTL (корисно для сеансів/ специфічних для користувача даних).
  • Засноване на розмірах виселення (IMemoryCache): встановити запис. Змінити розмір і налаштувати розмір об' єднаної пам' яті.
  • Пріоритет (IMemoryCache): cacheItemPriority. Expect/Normal/Low/ Never Delete впливає на виселення під тиском.
  • Jitter: додати невеликі випадкові зміщення до TTL, щоб уникнути синхронізованої застарілості (stampedes).

Запобігання штампам кешу (обмеження стада)

  • Скористайтеся пунктом меню GetOrCreate/GetOrCreateAsync (IMemoryCache) для забезпечення індивідуального пересування на вузол.
  • Розподіл: скористайтеся ключем блокування (SET NX EX) або підтримкою бібліотеки; додайте TTL Runction; подумайте про оновлення тла.
  • Подача застарілого налаштування: зберегти вторинний ключ зі застарілим значенням і коротким суфіксом, під час обчислення буде обчислено нове значення.

Компонування і визначення ключів

  • Малі клавіші, розділені двокрапками: app:entity: 123 або 10ant: us: users: 42.
  • Включити сегмент версії до невикористаних цілих класів ключів без вилучення: v2: продукти: 123.
  • Tantant-aure: ключі префікса з тентантом або ідентифікатором організації, щоб уникнути зіткнень і полегшення очищення.
  • Залишати ключі малими, але описовими; уникати використання сирих вхідних даних без нормалізації.

Виділення наборів ключів (tags/ groups)

Якщо вам потрібно заборонити багато пов' язаних записів:

  • Префікси з версіями (м' якіше поновлення): виштовхніть глобальну версію у малій клавіші і створіть з нею клавіші.

    // version key: "v:products"; keys like $"{version}:product:{id}"
    var version = await cache.GetStringAsync("v:products") ?? "1";
    var key = $"{version}:product:{id}";
    

    Щоб скасувати роботу всіх продуктів, виконайте команду total v: Productions (природно, клієнти не матимуть старих з префіксами клавіш).

  • Набір міток на групу (Redis): зберігає набір ключів на мітку; при нечинності, звантажте членів і вилучіть їх.

    // using StackExchange.Redis directly for sets + efficient deletes
    var mux = await ConnectionMultiplexer.ConnectAsync("localhost:6379");
    var db = mux.GetDatabase();
    var tag = "tag:category:42";
    var key = $"prod:{prodId}";
    await db.StringSetAsync(key, serialized, expiry: TimeSpan.FromMinutes(30));
    await db.SetAddAsync(tag, key); // remember membership
    
    // later, invalidate the whole tag
    var members = await db.SetMembersAsync(tag);
    if (members.Length > 0)
    {
        var keys = Array.ConvertAll(members, m => (RedisKey)m);
        await db.KeyDeleteAsync(keys);
    }
    await db.KeyDeleteAsync(tag);
    
  • Пошкодження Pub/Sub: опублікування " невивіреного: ключа "; кожен з вузлів вилучає ключ з локального повідомлення IMemoryCache.

  • Сканувати за шаблонами: SCAN/KEYS слід уникати у прострочених гарячих шляхах. Гаразд для адміністративних інструментів у малих просторах ключів.

Практичні помічники

  • IMemoryCache get- or- set з параметрами:
    T GetOrAdd<T>(IMemoryCache cache, string key, Func<ICacheEntry, T> factory)
      => cache.GetOrCreate(key, e =>
      {
          e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
          e.SlidingExpiration = TimeSpan.FromMinutes(2);
          e.Priority = CacheItemPriority.Normal;
          e.Size = 1;
          return factory(e);
      });
    
  • IDistribedCache з JSON і строк дії:
    static async Task<T?> GetOrSetJsonAsync<T>(IDistributedCache cache, string key, Func<Task<T>> factory, TimeSpan ttl)
    {
        var json = await cache.GetStringAsync(key);
        if (json is not null)
            return System.Text.Json.JsonSerializer.Deserialize<T>(json);
    
        var value = await factory();
        var opts = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = ttl };
        await cache.SetStringAsync(key,
            System.Text.Json.JsonSerializer.Serialize(value),
            opts);
        return value;
    }
    

Спостереження і видимість

  • Доріжка влучила/міксує швидкість і середній час завантаження; виявлення вимірів (прометейських лічильників) за групою ключів.
  • Додайте журналювання навколо кількості кешу та зворотних викликів для IMemoryCache.
  • Для Redis переглядайте простір ключів things/misses, pendncy і фрагментація пам' яті; встановіть правила maxmemory як відповідну.

Шаблони IMeryCache з розробки у реальному світі

Ось як я насправді використовую IMemoryCache на моїй платформі блогу, еволюціонував шляхом спроб і помилок. я покажу вам три справжні шаблони від простого до складного.

Візерунок 1: Просте місце для кешу для даних, які рідко змінюються (категорії)

Це була моя перша реалізація кешування. Категорії блогу не часто змінюються, отже кешувати їх протягом 30 хвилин:

// From BaseController.cs
private const string CacheKey = "Categories";

private async Task<List<string>> GetCategories()
{
    baseControllerService.MemoryCache.TryGetValue(CacheKey, out var value);

    if (value is List<string> categories) return categories;

    logger.LogInformation("Fetching categories from BlogService");
    categories = (await BlogViewService.GetCategories(true)).OrderBy(x => x).ToList();
    baseControllerService.MemoryCache.Set(CacheKey, categories, TimeSpan.FromMinutes(30));
    return categories;
}

Чому це працює:

  • Категорії читаються на кожній сторінці (показано у навігації)
  • Вони рідко змінюються (лише якщо я додаю нові дописи блогу з новими категоріями)
  • 30- хвилинний TTL непоганий; якщо з' являтимуться нові категорії, користувачі бачитимуть їх протягом 30 хвилин
  • Абсолютний термін (без ковзання), тому що нам байдуже, як часто він доступний.

Попався: Спочатку я використав клавішу рядка " Categories ." Працює добре, аж доки у вас не з' явиться декілька контролерів і один з них випадково повторно використовує той самий ключ. Тепер я користуюся сталими або потужними клавішами (див. попередній розділ щодо уникнення зіткнень).

Шаблон 2: стан кожного користувача з обмеженнями розміру і закінченням строку дії (завершення завдань)

За допомогою цього пункту можна закрити стан перекладу для кожного з користувачів. Цей стан є складнішим, оскільки він потребує обмеження і повинен залишатися живим, якщо користувач є активним:

// From TranslateCacheService.cs - tracks translation tasks per user
public void AddTask(string userId, TranslateTask task)
{
    CachedTasks CachedTasks() => new()
    {
        Tasks = new List<TranslateTask> { task },
        AbsoluteExpiration = DateTime.Now.AddHours(6)
    };

    if (memoryCache.TryGetValue(userId, out CachedTasks? tasks))
    {
        tasks ??= CachedTasks();
        var currentTasks = tasks.Tasks;

        // Keep only the 5 most recent tasks
        currentTasks = currentTasks.OrderByDescending(x => x.StartTime).ToList();
        if (currentTasks.Count >= 5)
        {
            var lastTask = currentTasks.Last();
            currentTasks.Remove(lastTask);
        }

        currentTasks.Add(task);
        currentTasks = currentTasks.OrderByDescending(x => x.StartTime).ToList();
        tasks.Tasks = currentTasks;

        memoryCache.Set(userId, tasks, new MemoryCacheEntryOptions
        {
            AbsoluteExpiration = tasks.AbsoluteExpiration,
            SlidingExpiration = TimeSpan.FromHours(1) // Extends on access
        });
    }
    else
    {
        var absoluteExpiration = DateTime.Now.AddHours(6);
        var cachedTasks = CachedTasks();
        memoryCache.Set(userId, cachedTasks, new MemoryCacheEntryOptions
        {
            AbsoluteExpiration = absoluteExpiration,
            SlidingExpiration = TimeSpan.FromHours(1)
        });
    }
}

Чому це є інакше:

  • Нахиляючий термін: Якщо користувач продовжуватиме перевіряти стан перекладу, утримування кешу у пам' яті до 6 годин макс.
  • Обмеження розміру: Зберігати лише 5 найсвіжіших завдань для кожного користувача, щоб запобігти зростанню пам' яті
  • Ручне керування: я явно обмежую розмір, оскільки MemoryCache не нав' язує обмеження на кількість елементів рівня входу (тільки загальний розмір кешу)

Помилка, яку я зробив: Спочатку я не обмежував кількість завдань: користувач з живленням увімкнув 50+перекладів, а у мене витік пам' яті. Тепер я зберігаю максимальну 5 на кожного користувача.

Зменшення торгівлі: Це не вплине на стан мільйонів користувачів. Якщо це станеться проблемою, я перейду до IDistribedCache (Redis) або зберігатиму у базі даних покажчик на userId + startTime.

Візерунок 3: Кеш з об' ємністю (масштаб Метритів)

Це кешує аналітичні вихідні дані і ефективність кешу доріжок за допомогою слідкування за серілогами:

// From UmamiDataSortService.cs - caches Umami analytics metrics
public async Task<List<MetricsResponseModels>?> GetMetrics(DateTime startAt, DateTime endAt, string prefix = "")
{
    using var activity = Log.Logger.StartActivity("GetMetricsWithPrefix");
    try
    {
        var cacheKey = $"Metrics_{startAt:yyyyMMdd}_{endAt:yyyyMMdd}_{prefix}";

        if (cache.TryGetValue(cacheKey, out List<MetricsResponseModels>? metrics))
        {
            activity?.AddProperty("CacheHit", true);
            return metrics;
        }

        activity?.AddProperty("CacheHit", false);
        var metricsRequest = new MetricsRequest
        {
            StartAtDate = startAt,
            EndAtDate = endAt,
            Type = MetricType.url,
            Limit = 500
        };

        var metricRequest = await dataService.GetMetrics(metricsRequest);
        if (metricRequest.Status != HttpStatusCode.OK) return null;

        var filteredMetrics = metricRequest.Data
            .Where(x => x.x.StartsWith(prefix))
            .ToList();

        cache.Set(cacheKey, filteredMetrics, TimeSpan.FromHours(1));

        activity?.AddProperty("MetricsCount", filteredMetrics?.Count() ?? 0);
        activity?.Complete();
        return filteredMetrics;
    }
    catch (Exception e)
    {
        activity?.Complete(LogEventLevel.Error, e);
        return null;
    }
}

Що робить цю продукцію готовою:

  • Спостереження: Кількість даних у кеші та метри запису
  • Композитний ключ: включити діапазон дат і префікс, щоб уникнути зіткнень ключів
  • Форматування дати: Використання yyyyMMdd форматування у ключі так різні часи в одному спільному дні кешу
  • Нульова обробка: Повертає нульове значення, якщо зовнішня служба зазнає невдачі; не зазнає невдачі у кеші
  • Кечування з фільтрами: Кеш фільтрується результат, а не сира відповідь

Еволюція: Спочатку я кешував протягом 10 хвилин, але вихідні дані у Умамі є важкими для отримання і не міняється багато, тому 1 година - це нормально.

Спостереження у дії: У Seq (my log aggreator) можна надсилати запити:

ActivityName = "GetMetricsWithPrefix" and CacheHit = false

За допомогою цього пункту можна вказати рівень промахів у кеші. Якщо він високий, я підлаштую TTL або стратегію клавіатурних скорочень.

Порівнюючи три підходи

|---------|----------|------------|--------------|---------------| | Категорії ♪Worklobal, dily-changing} 30 minute ♪ Not required (male) apple} mose more more more's more more more more more more more movious movious movious movious movious movious movious more movious movious movi | Завдання з перекладу ♪ Per-користувач, прив'язаний до шестиг абсолютний + 1h сяг ♪ Рушний (5 елементів) ♪ None (повинно додати!) * | Метрики Передбачається зовнішнє з'єднання, тобто абсолютне (часовий-відкритий) ведьйо

Уроки з ключів:

  1. Почати простий (взір 1) додати складність лише за потреби
  2. Завжди думати про використання пам' яті кешу - додавати межі розміру для окремих кешів користувача
  3. Для дорогих операцій з першого дня додайте економність.
  4. Скоригувати TTL на основі справжньої зміни частоти даних, а не відгадування

Якщо я не користуюся IMoryCache:

  • Для стану розпізнавання користувача (замкнено визначений у кукі- автентифікації)
  • Для візка з шопінгом (використовуватиме DB + розподілений кеш)
  • Для допису блогу (також у DB, завантажено один раз на запит)
  • Стан cross- server (потребує IDistributedCache/Redis)

Розпізнавання кук і оголошення

  • Область видимості: через запити до вилучення
  • Використовувати для: профілювання, грубих ролей/повторів, невеликий об' єм даних профілювання
  • Продаж: розмір печива; дот надштаб. Заяви повинні бути стабільними.

Автентифікація кук:

builder.Services.AddAuthentication("Cookies")
    .AddCookie("Cookies", o =>
    {
        o.LoginPath = "/login";
        o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        o.SlidingExpiration = true;
    });
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

Підписування з умовами (MVC/minimal):

app.MapPost("/login", async (HttpContext ctx) =>
{
    var claims = new[]
    {
        new Claim(ClaimTypes.NameIdentifier, "123"),
        new Claim(ClaimTypes.Name, "Alice"),
        new Claim(ClaimTypes.Role, "Admin")
    };
    var identity = new ClaimsIdentity(claims, "Cookies");
    await ctx.SignInAsync("Cookies", new ClaimsPrincipal(identity));
    return Results.Redirect("/");
});

Прочитати позови (у будь- якому стосі):

[Authorize]
app.MapGet("/me", (ClaimsPrincipal user)
  => Results.Ok(new { user.Identity!.Name, Roles = user.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value) }));

JWT / BearTones

  • Область видимості: клієнт несе ключ; сервер без стану
  • Використовувати для: SPEAs/ Mobile/APIs, crossdom- doain, microservices
  • Підвищення торгівлі: розмір ключа; обертання/ відновлення; збереження мінімальних заяв, за потреби використовувати інтроспектор
builder.Services.AddAuthentication("Bearer")
   .AddJwtBearer("Bearer", o =>
   {
       o.Authority = "https://demo.identityserver.io"; // example
       o.Audience = "api";
       o.RequireHttpsMetadata = true;
   });

Користування:

[Authorize(AuthenticationSchemes = "Bearer")]
app.MapGet("/secure", () => "ok");

Огляд донощика:

sequenceDiagram
  participant C as Client
  participant STS as Token Service
  participant API as API
  C->>STS: Authenticate (username/password)
  STS-->>C: JWT (signed)
  C->>API: GET /secure (Authorization: Bearer <jwt>)
  API->>API: Validate signature, expiry, audience
  API-->>C: 200

Стан придатності: база даних і друзі

  • Область: назавжди (до тих пір, доки ви не вилучите її)
  • Використовувати для: все, що не може бути втрачено: вози, замовлення, профілі, робочі потоки з довгими потоками
  • Шаблони: стандартний CRUD з ядром EF; CQRS; шаблон розподілу подій; шаблон вихідного сховища для надійності

EF Core ескіз:

builder.Services.AddDbContext<AppDb>(o => o.UseSqlServer(cs));

app.MapPost("/cart/items", async (AppDb db, AddItem cmd) =>
{
    var cart = await db.Carts.FindAsync(cmd.CartId) ?? new Cart(cmd.CartId);
    cart.Add(cmd.ProductId, cmd.Qty);
    await db.SaveChangesAsync();
    return Results.Created($"/cart/{cart.Id}", cart);
});

Кечування відповідей, ETags і умовні запити

  • Не те саме, що й пересилання, а те, що повертає повторну роботу, дозволяючи клієнту/ проксі повторно використовувати попередні відповіді. Часто вона складається з стану query/route.
builder.Services.AddResponseCaching();
var app = builder.Build();
app.UseResponseCaching();

app.MapGet("/products", (HttpContext ctx) =>
{
    ctx.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue { Public = true, MaxAge = TimeSpan.FromSeconds(30) };
    return Results.Ok(new[] { new { Id = 1, Name = "Widget" } });
}).CacheOutput();

Приклад ETag:

app.MapGet("/resource", (HttpContext ctx) =>
{
    var version = "W/\"abc123\""; // compute based on data hash
    ctx.Response.Headers.ETag = version;
    if (ctx.Request.Headers.IfNoneMatch == version)
        return Results.StatusCode(StatusCodes.Status304NotModified);
    return Results.Text("payload");
});

AspectCache visputCache: Використання реального світу у виробі

Ось чому вам потрібні обидва варіанти, і те, як вони відрізняються.

Спантеличення.

Ядро ASP.NET має два подібні системи кешування:

  1. AptionCache (HTTP caching): Встановлює заголовки HTTP (Cache-Control, Vary) повідомити навігаторам і CDN про те, як кешувати e
  2. ВивідCache (сервер- сторона): Кешів показаний вивід на сервері, щоб пропустити виконання дії повністю

Вони доповнюють один одного. ReferenceCache керує клієнтом/CDN cacheching; інструментами роботи з виводомCache для роботи з сервером зі сторони кешування.

Дія над дописом мого блогу (застосована обидва кеші)

// From BlogController.cs
[Route("{slug}")]
[HttpGet]
[ResponseCache(Duration = 300, VaryByHeader = "hx-request",
    VaryByQueryKeys = new[] { nameof(slug), nameof(language) },
    Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 3600, VaryByHeaderNames = new[] { "hx-request" },
    VaryByQueryKeys = new[] { nameof(slug), nameof(language) })]
public async Task<IActionResult> Show(string slug, string language = "en")
{
    var post = await blogViewService.GetPost(slug, language);
    if (post == null) return NotFound();

    // ... populate user info, comments, etc ...

    if (Request.IsHtmx()) return PartialView("_PostPartial", post);
    return View("Post", post);
}

Що відбувається, коли хтось просить /blog/my-post:

  1. Спочатку перевіряються вивідCache: Чи є у мене кешована відповідь my-post + en мовою?

    • Влучення: Return cacheed HTML, спосіб дій ніколи не запущено (швидка! ~1 мс)
    • Miss: Виконати дію, показати вміст кешу за 3600 секунд (1 годину)
  2. ReferenceCache встановлює заголовки: Після того, як Cache створить відповідь, AspectCache додає:

    Cache-Control: public, max-age=300
    Vary: hx-request
    
  3. Кечування навігатора: У переглядачі зберігається відповідь на 300 секунд (5 хвилин). Підприємницькі запити одного користувача навіть не нападають на сервер.

  4. Кечування CDN (якщо ви використовуєте Glowflare/Fastly): кешування CDN протягом 5 хвилин. Користувачі по всьому світі вмикають CDN, а не мій сервер.

Чому різні тривалості? (300 проти 3600)

[ResponseCache(Duration = 300)]    // 5 minutes client/CDN cache
[OutputCache(Duration = 3600)]     // 1 hour server cache

Розмірковуйте:

  • Кеш сервера довший (1 годину): я контролюю свій сервер; я можу очистити кеш, якщо оновлю допис
  • Кеш клієнта коротший (5 хвилин): Я не можу просто очистити переглядачі або CDN; 5 хвилин - це вікно поміркованої неточності
  • Торгівля: Якщо ви редагуєте допис, з' явиться новий вміст:
    • Сторона сервера: негайно (я можу зробити кеш нечинним)
    • CDN/browsers: через 5 хвилин (або я вручну очищую CDN)

Еволюція: Спочатку у мене було обом за 5 хвилин, але це означало, що мій сервер знову здавав кожні 5 хвилин, хоча зміст рідко міняється.

  • Сервер з радістю обслуговує HTML протягом 1 години
  • Клієнти отримують свіжий вміст кожні 5 хвилин

VaryByHeader для HTMX

VaryByHeader = "hx-request"  // ResponseCache
VaryByHeaderNames = new[] { "hx-request" }  // OutputCache

Чому це є важливим: Включено запити HTMX hx-request: true заголовок. Я повертаю різні відповіді:

  • Повний запит: Заповнити сторінку HTML з компонуванням
  • Запит HTMX: Частковий перегляд без компонування

Без VaryBy, кеш поверне неправильний формат. VaryByЯ фіксую дві версії кожної сторінки.

Приклад:

User requests /blog/my-post → Cache key: "blog/my-post:en:hx=false" → Full HTML cached
HTMX requests /blog/my-post → Cache key: "blog/my-post:en:hx=true" → Partial HTML cached

VaryByKoinyKeys для мови і парування

[ResponseCache(VaryByQueryKeys = new[] { nameof(slug), nameof(language) })]
[OutputCache(VaryByQueryKeys = new[] { nameof(slug), nameof(language) })]

Проблема без цього: /blog/my-post?language=fr буде обслуговано під кешованою версією англійської мови.

З VaryByKoinyKeys: Розділити записи кешу:

  • /blog/my-post?language=en → Ключ кешу включає "en"
  • /blog/my-post?language=fr → Ключ кешу включає " fr "

Справжнє використання з мого списку блогів:

[Route("blog")]
[ResponseCache(Duration = 300, VaryByHeader = "hx-request",
    VaryByQueryKeys = new[] { "page", "pageSize", "startDate", "endDate", "language", "orderBy", "orderDir" })]
[OutputCache(Duration = 3600, VaryByHeaderNames = new[] { "hx-request" },
    VaryByQueryKeys = new[] { "page", "pageSize", "startDate", "endDate", "language", "orderBy", "orderDir" })]
public async Task<IActionResult> Index(int page = 1, int pageSize = 20, /* ... */)

Попередження про вибух кешу: Кожна унікальна комбінація параметрів = окремий запис кешу:

  • page=1&pageSize=20&language=en → Один запис
  • page=2&pageSize=20&language=en → Інший запис
  • page=1&pageSize=10&language=en → Інший запис

Мітифікація:

  • Поміркований максимальний розмір кешу (автоматичний режим визначення кешу без обмежень)
  • Кеш звичайних параметрів (сторінка 1, типова сторінкаSize)
  • Незагальні комбінації можуть пропустити кеш (прийнято)

Потрібне налаштування

AspectCache працює поза ящиком, але для ExputCache вам слід налаштувати:

// Program.cs
builder.Services.AddOutputCache(options =>
{
    options.MaximumBodySize = 64 * 1024 * 1024; // 64 MB max response size
    options.SizeLimit = 100 * 1024 * 1024; // 100 MB total cache size
});

var app = builder.Build();
app.UseOutputCache(); // Must be in middleware pipeline

Якщо вивідКаче не допоможе

ВивідCache пропущено для:

  • Автентифіковані запити (різні користувачі бачать різні дані)
  • POST/PUT/DELETE (лише кеш GET/ HEAD)
  • Відповідь за допомогою Set-Cookie заголовок
  • Відповідь, яку явно встановлено Cache-Control: no-store

Приклад, коли я ним не користуюся:

// Comment submission - authenticated, POST, and per-user
[HttpPost]
[Authorize]
public async Task<IActionResult> AddComment(CommentModel model)
{
    // No caching attributes - this is user-specific and changes state
}

Ефективність оцінки

Для стеження я використовую Прометейські виміри (викладені за допомогою програми):

// Pseudo-code for metrics
cache_hits_total{cache="output"} 45230
cache_misses_total{cache="output"} 892

Частота проходження кешу: ~98% для дописів блогів (більшість адрес даних неодноразово потрапляє у ті самі популярні дописи).

Вплив:

  • Без кешування: ~50ms середній час відповіді (попит DB + Відтворення розмітки)
  • З виводомCache: ~1- 2мс для кешованих відповідей
  • 25x speedup

Коли я тимчасово вимкнув кешування

Іноді я знеохочую і потребую свіжих відповідей кожного разу:

// During development, comment out caching
// [ResponseCache(Duration = 300, ...)]
// [OutputCache(Duration = 3600, ...)]
public async Task<IActionResult> Show(string slug, string language = "en")

Або використовуйте специфічні для середовища параметри:

#if DEBUG
    // No caching in development
#else
    [ResponseCache(Duration = 300, ...)]
    [OutputCache(Duration = 3600, ...)]
#endif

Кращий підхід: Використовувати налаштування:

[ResponseCache(Duration = responseCacheDuration, ...)]

where responseCacheDuration дорівнює 0 в розробці, 300 у виробництві.

Pros і консульство цієї стратегії подвійного вуса

Прос:

  • Ефективність сервера: ВивідCache зменшує навантаження процесора/DB на 98%
  • Користі клієнта/CDN: Шахрай у відповідь зменшує пропускну здатність і поліпшує стан спізнення по всьому світі.
  • Гнучкість: Різні TTLs для клієнта сервера проти
  • Заощадження вартості: Менше запитів до DB, менше пропускної здатності

Збори:

  • Невтомність: Оновлення вмісту займає до 5 хвилин для поширення клієнтам.
  • Складність фіксації кешу: Для оновлення допису потрібно усування вад як сервера, так і кешу CDN
  • Використання пам' яті: ВивідКаче тримає HTML у пам' яті сервера
  • Зневадження плутанини: Деколи забувають про кеш і дивуються, чому не з'являються зміни

Коли я пропускав катування:

  • Дані щодо реального часу (ціни на сто відсотків, прожиті види спорту)
  • Особистий вміст (рекомендації, специфічні для користувача)
  • Низька кількість сторінок (розташування над головою > перевага)
  • Сторінки, що змінюються дуже часто

Мій вирок: Для блогу, який має здебільшого статистичний зміст і високий рівень read/write, подвійне кешування - це велика перемога. Я б не використовував його на адміністраціях або панельх приладів з швидкою зміною даних.


ViewBag, ViewData і BpmData: Controller- to- Перегляд Стан (і чому здебільшого я уникаю двох з них)

Ці три часто плутаються. ось як вони відрізняються і що я насправді використовую для виробництва.

Порівняно з трьома аміго

// ViewData: string-keyed dictionary
ViewData["Title"] = "Blog";
ViewData["Categories"] = new List<string> { "ASP.NET", "C#" };

// ViewBag: dynamic wrapper around ViewData
ViewBag.Title = "Blog";
ViewBag.Categories = new List<string> { "ASP.NET", "C#" };

// TempData: survives one redirect (backed by session or cookie)
TempData["Message"] = "Post saved!";
return RedirectToAction("Index");

ViewData} ViewBag} stepData}

--------- ---------- --------- ----------
Життєвий час Поточна просьба ♪ Поточна просьба ♪ Одна черга ♪
Доступ до ключа String keys Синтаксис + String keys}
Безпека типів ♪ None (casts need) ♪ None (dynamic) ♪ None (casts need) ♪
Перевірка часу компіляції Ні
Перемотування пережитку Ні.

Те, що я насправді використовую: ViewBag для загальних даних розкладки

У своєму блозі я використовую ViewBag виключно для передавання даних від контролерів до спільного компонування (аналітики, категорії тощо):

// From BaseController.cs - runs before every action
public override async Task OnActionExecutionAsync(ActionExecutingContext filterContext,
    ActionExecutionDelegate next)
{
    logger.LogInformation("OnActionExecutionAsync");

    if (!Request.IsHtmx())
    {
        // Analytics settings for layout
        ViewBag.UmamiPath = AnalyticsSettings.UmamiPath;
        ViewBag.UmamiWebsiteId = AnalyticsSettings.WebsiteId;
        ViewBag.UmamiScript = AnalyticsSettings.UmamiScript;
    }

    logger.LogInformation("Adding categories to viewbag");
    ViewBag.Categories = await GetCategories(); // Cached list

    await base.OnActionExecutionAsync(filterContext, next);
}

Потім у моєму компонуванні (_Layout.cshtml):

@if (ViewBag.Categories is List<string> categories)
{
    <nav>
        @foreach (var cat in categories)
        {
            <a asp-controller="Blog" asp-action="Category" asp-route-category="@cat">@cat</a>
        }
    </nav>
}

@if (!string.IsNullOrEmpty(ViewBag.UmamiPath))
{
    <script async src="@ViewBag.UmamiScript"
            data-website-id="@ViewBag.UmamiWebsiteId"></script>
}

Чому цей зразок діє:

  • Загальні: Кожна сторінка потребує категорій нав і аналітичних
  • Обчислений один раз: BaseController працює перед кожною дією
  • Оптимізація HTMX: Пропускати скрипт аналітики на частих запитах (HTMX не потребує повторного підтвердження)
  • Кешовано: Категорії кешуються (див. мій шаблон IMemoryCache вище), отже не натискайте на DB з кожним запитом

Помилка, яку я зробив раніше: Я складав. ViewBag.Categories У кожному способі дій. Порушення покарання і просто забути. OnActionExecutionAsync в базовому контролері вирішив це.

Перегляд Баг для даних, специфічних для сторінки (прийнятний шаблон)

// From BlogController.cs
[Route("category/{category}")]
public async Task<IActionResult> Category(string category, int page = 1, int pageSize = 10)
{
    ViewBag.Category = category; // Used in view for heading
    ViewBag.Title = category + " - Blog"; // Used in layout <title>

    var posts = await blogViewService.GetPostsByCategory(category, page, pageSize);
    // ... populate posts model ...

    if (Request.IsHtmx()) return PartialView("_BlogSummaryList", posts);
    return View("Index", posts);
}

З перегляду:

@{
    ViewData["Title"] = ViewBag.Title; // Standard MVC convention for <title>
}

<h1>Category: @ViewBag.Category</h1>

Це нормально, тому що:

  • Просте масштабоване значення (рядок, int)
  • Використовується лише у області перегляду, а не проходить повз
  • Альтернативним варіантом буде додавання Title і Category Властивості для кожної моделі перегляду

Чого мені потрібно уникати: складні об' єкти у ViewBag

Зразок:

// DON'T DO THIS
ViewBag.User = new UserViewModel { Name = "Scott", IsAdmin = true };
ViewBag.Posts = new List<Post> { ... };
ViewBag.Metadata = new { Tags = new[] { "a", "b" }, Date = DateTime.Now };

Проблеми:

  • Без безпеки компіляції (typo) ViewBag.Usr не вдалося під час виконання)
  • Складно відстежити доступні для перегляду дані
  • Робить тестування важчим (потрібно перевірити словник ViewBag)
  • Без інстинктивної інформації

Кращі: потужні моделі перегляду:

// DO THIS instead
public class BlogIndexViewModel : BaseViewModel
{
    public string Category { get; set; }
    public List<PostSummary> Posts { get; set; }
    public PaginationInfo Pagination { get; set; }
}

public IActionResult Category(string category, int page = 1)
{
    var model = new BlogIndexViewModel
    {
        Category = category,
        Posts = await GetPosts(category, page),
        // Inherited from BaseViewModel:
        Authenticated = user.LoggedIn,
        Name = user.Name,
        AvatarUrl = user.AvatarUrl
    };
    return View("Index", model);
}

Тимчасові дані: я не використовую його (і ось чому)

Стандартний регістр використання PMData:

[HttpPost]
public IActionResult SavePost(PostModel model)
{
    // Save post...
    TempData["SuccessMessage"] = "Post saved successfully!";
    return RedirectToAction("Index");
}

public IActionResult Index()
{
    // TempData["SuccessMessage"] available here (consumed on read)
    return View();
}

Чому я не користуюся тимчасовою базою даних у своєму блозі:

  1. Я використовую HTMX замість переспрямування: Мої форми надсилаються за допомогою HTMX і повертати часткові перегляди з повідомленнями про успіх/ помилку. Без переспрямування = не потрібно для BrupData.
[HttpPost]
public async Task<IActionResult> Submit(ContactViewModel model)
{
    if (!ModelState.IsValid)
        return PartialView("_ContactForm", model); // Show errors inline

    await sender.SendEmailAsync(contactModel);

    // Return success view directly (no redirect)
    return PartialView("_Response", new ContactViewModel
    {
        Email = model.Email,
        Name = model.Name,
        Comment = "Message sent!"
    });
}
  1. **Для традиційної PRG (Post- Redirect- Get)**Но я предпочитаю избегать перенаправки, если возможно, для лучшего UX.

Якщо RupData має сенс:

  • Традиційні програми MVC з повним переспрямуванням сторінок після POST
  • Майстер з декількома кроками, за допомогою яких ви переспрямовуєте стан між кроками
  • Спалах повідомлень після переспрямування автентифікації

Getchata getcha: Типово, підтримка кук (від імені ядра ASP. NET: 2. 2). Якщо ви додасте великі об' єкти до ТемпData, ви згладжуєте куку, відіслану з усіма запитами. Для великого стану скористайтеся пунктом Сеанс з резервним сховищем або DB.

Дерево швидкого прийняття рішень

flowchart TD
    A[Need to pass data to view?] --> B{What kind?}
    B -->|Global layout data| C[ViewBag in BaseController]
    B -->|Simple page-specific| D[ViewBag in action]
    B -->|Complex model| E[Strongly-typed ViewModel]
    B -->|Survive redirect| F{Using HTMX?}
    F -->|Yes| G[Return partial with message]
    F -->|No| H[TempData for flash message]

Мої правила:

  1. ViewBag лише для загальних елементів розкладки (аналітики, нав, хлібні крихти)
  2. ViewBag для простих заголовків/головок сторінок (необов' язковий; може використовувати ViewModel)
  3. Ніколи не ViewBag для складних об' єктів (використовуйте ViewModels)
  4. Ніколи не переглядати Data (ViewBag має кращий синтаксис)
  5. Тимчасові дані, лише якщо вам дійсно потрібен PRG (Я не можу, завдяки HTMX)

Візерунок: Майстри і багатосторонні потоки

Який власник штату?

flowchart TD
  A[Start Wizard] --> B{Short-lived?\nSingle browser?}
  B -- Yes --> S[Session/TempData]
  B -- No/Complex --> D[DB + key in route]
  S --> PRG[Use PRG between steps]
  D --> PRG
  • Малий, односеансовий: Session або BrectData між кроками.
  • Поперечний/ довгий запуск: story to DB, has a key in the URL.

Приклад (клавіша DB + route):

app.MapPost("/wizard/{id}", async (AppDb db, Guid id, StepInput input) =>
{
    var flow = await db.Flows.FindAsync(id) ?? new Flow(id);
    flow.Apply(input);
    await db.SaveChangesAsync();
    return Results.Redirect($"/wizard/{id}/next");
});

Візерунок: Flash Повідомлення з PubData

  • Встановити у POST; прочитати один раз після переспрямування.
TempData["Flash"] = "Profile saved";
return RedirectToAction("Index");

Перегляд Razor:

@if (TempData["Flash"] is string flash) {
  <div class="alert alert-info">@flash</div>
}

Шаблон: крамничне авто.

  • Маленькі візки: Сеанс (якщо ваш розмір є скромним і у вас є сеанси з липкими/ настирливими).
  • Більші візки/ multi- device: DB + card- id у кукі або адресі URL. Кеш для швидкості.
flowchart LR
  U[User] -- cart-id cookie --> S[Server]
  S --> DB[(Cart Table)]
  S <--> Cache[Distributed Cache]

Список перевірки безпеки, конфіденційності та відповідності

  • Перевірити стан всіх клієнтів: запит, заголовки, форми, куки, твердження JWT.
  • Захистити стан конфіденційного клієнта: використовувати захист даних для кук, які ви використовуєте; ніколи не зберігати секрети у рядках запитів.
  • Встановити прапорці кук: Secure, HttpOnly, SameSite, IsEssential (якщо потрібно для згоди/ функціонування).
  • Повторно створити куки розпізнавання за зміненими правами доступу; залиште вимоги мінімальними.
  • За потреби, зашифрувати у відпочинку для магазинів на сервері, переконатися у повороті ключів (клавіатура захисту даних, ключі для підписування JWT).
  • COUNTR/ CCPA: надає у ваше розпорядження шляхи експорту/ з' єднання з даними користувача; мінімізувати збереження.

Матриця рішень (заголовок аркуша)

classDiagram
  class Items {
    +Per-request only
    +Great for middleware->endpoint handoff
  }
  class QueryRoute {
    +Explicit, linkable
    -User-controlled
  }
  class Headers {
    +Tracing, idempotency
    -Noisy, untrusted
  }
  class Cookies {
    +Persist small prefs
    -Size, perf, consent
  }
  class TempData {
    +One-redirect messages
    -Ephemeral
  }
  class Session {
    +Conversational state
    -Scaling complexity
  }
  class MemoryCache {
    +Fast, in-proc
    -Not shared across instances
  }
  class DistributedCache {
    +Shared across servers
    -Serialization, ops
  }
  class AuthCookieClaims {
    +Identity, roles
    -Don’t overstuff
  }
  class JWT {
    +Stateless, cross-domain
    -Revocation/rotation
  }
  class DB {
    +Durable, authoritative
    -Latency, complexity
  }

Швидкий вибір:

  • Потрібне одно-пряме спалахування? PMData.
  • Потрібний майстер з декількома запитами у одному сеансі? Сеанс (або клавіша DB +, якщо багатоприкладний/ мультипристройний).
  • Потрібні коефіцієнти масштабування і без станів API? JWT для профілю, DB/DistribedCache для стану.
  • Потрібно передати дані лише всередині трубопроводу? HtpContext.Items.
  • Потрібний кеш для обчислених пошуків? IMemoryCache локально; IDistribedCache на фермі.

Сторінки MVC vs Razor vs Мінімальний API: однакові фундації, різні форми

Всі три стоси знаходяться на однакових примітивах (HtpContext, модель прив' язки, розпізнавання, захист даних). Наведені вище приклади показують, що API здебільшого відрізняються у ергономіці:

  • Мінімальний API: прив' язка параметрів з маршруту/ query/ body/ returns; повернення Results.*.
  • MVC: атрибути, фільтри, прив' язка моделей до параметрів дії/ перегляду моделей.
  • Сторінки Razor: інструмент обробки сторінок з обмеженими властивостями і помічниками міток для створення посилань/ форм.

Вони всі мають однакові державні механізми, про які тут говориться.


Пастки і антипрокатники

  • Стирання великих або чутливих даних у кукі або PubsData.
  • Залежно від того, чи є у кеші пам'яті правильність (це стосується кешу, а не правди).
  • Побудова уповноваження на основі маршрутів/ запитів, надісланих клієнтом, без перевірки сервера.
  • Перенаправляю печивки или JWT с чрезвычайными обвинениями.
  • Сеанс без стратегії розподілу (працює локально, перерв на шкалі).

Керування реальними державами: те, що я насправді використовую (і не використовувати)

Після того, як я покажу вам усі ці варіанти, ось моя чесна оцінка того, що працює в постановці для моєї платформи в блозі.

Мій стос керування станами (за порядком частоти)

♪ Mechanismission Використовуй субстанції Satisf} |-----------|-----------|-----------|--------------| | IMemoryCache * ♪ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | ExputCache Д - р Ґейттерс дзвінок у блогі, далі: | AspectCache Дзвінок: | ViewBag Д. д. д. д. д. д. д. д. д. д. д. д. д. д. | Автентифікаційні оголошення }Ідентифікація користувача, адміністративний прапор' ї Правий інструмент для auth | Маршрут/ Запитання Дівка, фільтрація, без сторонньої лінії та зв'язок | База даних Д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. | Куки Д. д. д. д. д. д. д. д. д. д. д. д. д. д. | Сеанс ♪ Never * - ♪ Not security picture ♪ | Тимчасові дані ♪ HTMX стирає потребу ♪ | HttpContext.Items Неужели не было воспользоваться делом | IDistributedCache ♪Never ♪ - ♪ Одинокий сервер (на даний час) ♪

Детальні профі/кони з досвіду виробництва

IMemoryCache ♪

Для чого я використовую його:

  • Список категорій (global, 30 хв TTL)
  • Відстеження завдання від перекладу користувача (6h абсолютне + 1h ковзання)
  • Відповіді зовнішнього API (Ummami метрики, 1h TTL)

Процедура в практиці:

  • Швидка розгладжування (процесія)
  • Без послідовності
  • Зменшений завантаження DB/API на ~95%
  • Легка для реалізації та розуміння
  • Спостереження за з' єднанням з серіологом

Спонсори, які я збив:

  • Пропускання пам' яті, якщо ви не обмежите кеш для одного користувача
  • Спрощено під час перезапуску програм (прийнятний для мого випадку використання)
  • Не є спільними через сервери (для окремого екземпляра)
  • Пошкодження кешу - це інструкція (потрібна вилучення) явно

Обмеження масштабування: Якщо я добираюся до декількох серверів, мені знадобиться програма IDistributedCache (Redis) для спільного стану. У поточній версії один сервер + кеш пам' яті є досконалим.

AVERCache + ReamentCache ♪

Для чого я використовую його:

  • Допис до блогу (1 годинний сервер, 5 хв клієнт/CDN)
  • Сторінки списку/ категорії блогу
  • Дані календаря

Процедура в практиці:

  • 25x speedup (50 мс → 2 мс)
  • Масштабує до інтенсивного руху без поту
  • Відокремлювати TTLs для клієнта сервера проти
  • Робота безперешкодно з HTMX (VaryByHeader)
  • Вимірний вплив за допомогою вимірювачів Prometheus

Спонсори, які я збив:

  • Зневадження плутанини (забрати кеш увімкнено)
  • Вибух кешу з багатьма комбінаціями параметрів запиту
  • Вікно сталості (5 хв для клієнтів)
  • Слід вручну заборонити оновлення вмісту

Найкраще для: Втома для читання програма з здебільшого статичним вмістом. Не придатна для персоналізованих або реальних даних.

ViewBag ♪

Для чого я використовую його:

  • Загальні дані розкладки (аналітичні дані, категорії)
  • Назви сторінок

Процедура в практиці:

  • Простий і швидкий для даних рівня компонування
  • Встановити один раз у BaseController, доступ до якого можна отримати всюди
  • Працює у вигляді кешу списку категорій

Спонсори, які я збив:

  • Без безпеки типу (відсутня помилка під час виконання)
  • Перевищення використання для складних даних
  • Тяжке тестування

Правило, яким я дотримуюсь: ViewBag лише для простих скалярів. Складні об' єкти можна знайти у пунктах ViewModels.

Автентифікація

Для чого я використовую його:

  • ІД користувача, ім' я, електронна пошта, адреса URL аватару
  • Прапорець адміністратора (перевірка sub претендувати на налаштування)

Процедура в практиці:

  • Безпечна (значена кука, захист даних)
  • Автоматично з основним профілем/ OAout (ASP. NET)
  • Доступний через User.Claims всюди
  • Застарівання з наведенням накладки не дозволяє користувачам входити до системи

Спонсори, які я збив:

  • Обмеження розміру кук (не вимагає перевищення)
  • Оголошення є статичними до повторного входу
  • Якщо я додам заяву (напр., " IsEditor "), вам слід повторно змінити визначення кук

Найкраща вправа: Зберігати дані мінімальними і стабільними. Не вкладайте дані, що часто змінюються.

Речі, які я не використовую (і чому)

Сеанс (ніколи не використовується):

  • Потрібно розподілити магазин (Redis)
  • Додає складність для мінімальної вигоди
  • Мої випадки використання краще використовуються:
    • Автентифікація (ідентифікація) Noun, a currency
    • IMemoryCache (короткий стан)
    • База даних (змінний стан)

Тимчасові дані (ніколи не використовується):

  • Шаблон Post- Redirect-Get з вилучення HTMX
  • Форми повернути часткові перегляди з вбудованими повідомленнями
  • Не нужно выживать перенаправки

HttpContext. Items (ніколи не використовується):

  • У нього не було випадку використання для окремого стану
  • Моя середня робота не обчислює значення для контролерів
  • Якби мені було потрібне визначення, я б використав його

IDistributedCache (ніколи не використовується):

  • Впровадження окремих серверів
  • IMemoryCache задовольняє всі потреби
  • Чи буде використовуватися Redis, якщо я масштабуватиму на декілька серверів

Еволюція мого підходу

Фаза 1 (ініціальний): Жодного кешування. Всі запити входили до бази даних, а потім відтворювали позначку з поля зору. Робота була нормальною для низьких трафіків.

Фаза 2 (перша оптимізація): Додано IMemoryCache для категорій. Див. негайне зменшення навантаження на DB. Зберіг його простим: 30 хв. TTL, без вишуканої логіки.

Фаза 3 (розсіювання вгору): Доданий ВвідКаш для дописів блогів під час сплеску даних. Покращення швидкодії даних. Початкова помилка: кешується лише протягом 5 хвилин. Збільшується до 1 години після спостереження за вмістом рідко змінюється.

Фаза 4 (зручність): Додано перетворення серілогаTracing до кешу вимірів. Відкриті помилки у кеші були високими через форматування дати у ключах. Фіксований формат ключів до yyyyMMdd Замість повного часового штампа. Рівень ударів знизився з 60% до 95%.

Фаза 5 (інтеграція з HTMX): Додано VaryByHeader for hx-requestСпочатку забув про це, а потім подав цілі сторінки запитам HTMX, що знеохочувало мене до того, як я це зрозумів.

Поточний стан: Щаслива зі стосом. IMoryCache + ExputCache + ReferenceCache manage 98% моїх потреб у керуванні державою. База даних для тривалого стану. Auth претендує на особу.

Поради для вашої програми

Почати тут:

  1. Маршрути/ панелі для навігації/ фільтрування (спочатку без стану)
  2. Автентифікація особи
  3. База даних для будь- чого, що має продовжуватися
  4. IMemoryCache для обчислюваних даних зчитування
  5. ExputCache для сторінок з дорогими паперами

За потреби додавати: 6. Сеанс (лише якщо вам потрібен стан спілкування на сервері) 7. IDistribedCache (лише у разі зміни масштабу на декілька серверів) 8. Куки (для уподобань клієнтів, згода)

Уникати:

  • Складні об' єкти у ViewBag/TempData
  • Сеанс без розподіленого запасу підтримки
  • Оболонка специфічних для користувача даних або даних, які часто змінюються
  • Передчасна оптимізація (спочатку вимірюйте!)

Уроки з ключів:

  • Почніть з простого, додайте складність, лише якщо вимірювання показують, що вона вам потрібна.
  • Важливим є спостереження (знайте коефіцієнти влучання у кеші!)
  • Думайте про попередні обмеження масштабу (потрібні обмеження для користувачів)
  • Ретельна фіксація кешу (обмеження - це реальна проблема)
  • Запишіть ваші рішення на TTL (якщо ви запитаєте "чому це 30 хвилин?"

Переносити вгору

Стан у веб- програмах не є одним розміром, який пасує всім. Виберіть найсвіжіший параметр, який задовольняє ваші потреби, надаєте перевагу несуттєвим візерункам, якщо ви можете, і будьте конкретними щодо безпеки та життєвого циклу.

Якщо ви хочете пройти глибше про те, як ці частинки проходять через трубопровод, подивіться, як починається моя серія з Частина 1: Огляд і основа а особливо середні і працевлаштовані частини.

Счастливого здания.


Додаток: більш придатні для копіювання приклади (приміри)

Ці приклади поглиблюють попередні розділи за допомогою подробиць, які можна вставити до net9 мінімальних шаблонів, MVC або сторінок Razor.

Куки: Захист значень за допомогою захисту даних

using Microsoft.AspNetCore.DataProtection;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDataProtection();
var app = builder.Build();

app.MapPost("/prefs/secure/{value}", (HttpContext ctx, string value, IDataProtectionProvider dp) =>
{
    var protector = dp.CreateProtector("prefs.theme");
    var protectedValue = protector.Protect(value);
    ctx.Response.Cookies.Append("pref.theme.p", protectedValue, new CookieOptions
    {
        HttpOnly = true,
        Secure = true,
        SameSite = SameSiteMode.Lax,
        Expires = DateTimeOffset.UtcNow.AddYears(1)
    });
    return Results.Ok();
});

app.MapGet("/prefs/secure", (HttpContext ctx, IDataProtectionProvider dp) =>
{
    if (ctx.Request.Cookies.TryGetValue("pref.theme.p", out var v))
    {
        var protector = dp.CreateProtector("prefs.theme");
        return Results.Text(protector.Unprotect(v));
    }
    return Results.NotFound();
});

Підказка: у декількох рядках з комбінацією, постійні клавіші Захисту даних (наприклад, до спільної файлової системи, Reredis або Azure key Vault), отже куки можна читати у різних екземплярах.

Антифоргерія у MVC, Сторінки Razor і Мінімальні API

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddAntiforgery(o => o.HeaderName = "X-CSRF-TOKEN");
var app = builder.Build();

app.MapGet("/antiforgery/token", (IAntiforgery af, HttpContext ctx) =>
{
    var tokens = af.GetAndStoreTokens(ctx);
    return Results.Json(new { token = tokens.RequestToken });
});

app.MapPost("/submit", (HttpContext ctx) => Results.Ok("posted"))
   .AddEndpointFilter(async (efiContext, next) =>
   {
       var af = efiContext.HttpContext.RequestServices.GetRequiredService<IAntiforgery>();
       await af.ValidateRequestAsync(efiContext.HttpContext);
       return await next(efiContext);
   });

app.MapControllers();
app.MapRazorPages();
  • MVC: декорації дій за допомогою [ValidateAntiForgeryToken] і використання @Html.AntiForgeryToken() у формах.
  • Сторінки Razor: типово увімкнено для дописів форм; використовується asp-antiforgery="true" якщо потрібно.
  • Мінімальний: перевірити за допомогою IAntiforgery як показано.

Сеанс з Redis і ковзання проти абсолютного строку дії

builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");
builder.Services.AddSession(o =>
{
    o.IdleTimeout = TimeSpan.FromMinutes(20); // sliding
    o.IOTimeout = TimeSpan.FromSeconds(2);
    o.Cookie.HttpOnly = true;
    o.Cookie.IsEssential = true;
});
var app = builder.Build();
app.UseSession();

Зберігати лише малі, придатні для стискання дані. Настійні справжні вози або порядок у DB.

IMemoryCache з параметрами запису та зворотним викликом

builder.Services.AddMemoryCache();

app.MapGet("/fx/{pair}", (IMemoryCache cache, string pair) =>
{
    var key = $"fx:{pair.ToLowerInvariant()}";
    return Results.Ok(cache.GetOrCreate(key, entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
        entry.SlidingExpiration = TimeSpan.FromMinutes(2);
        entry.Size = 1; // enable size-based eviction if configured
        entry.RegisterPostEvictionCallback((k, v, reason, state) =>
        {
            Console.WriteLine($"Evicted {k} because {reason}");
        });
        return 0.92m; // fetch from external service in real life
    }));
});

IDistribedCache get'orced with special, щоб уникнути tags

builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");

app.MapGet("/feature/{name}", async (IDistributedCache cache, string name) =>
{
    var key = $"feat:{name}";
    var cached = await cache.GetStringAsync(key);
    if (cached is not null) return Results.Text(cached);

    // Lock key to prevent thundering herd (very simple approach)
    var lockKey = key + ":lock";
    var gotLock = await cache.SetStringAsync(lockKey, "1", new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5)
    });

    try
    {
        cached = await cache.GetStringAsync(key);
        if (cached is null)
        {
            var computed = "on"; // expensive work
            var rnd = Random.Shared.Next(0, 15); // jitter
            await cache.SetStringAsync(key, computed, new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5).Add(TimeSpan.FromSeconds(rnd))
            });
            cached = computed;
        }
    }
    finally
    {
        await cache.RemoveAsync(lockKey);
    }

    return Results.Text(cached);
});

Для надійного блокування надайте перевагу примітивам Redis (SET NX EX) за допомогою StackExchange. Redis.

Спірне питання і перевірка JWTs локально (демо)

using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;

var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("super-secret-key-please-rotate"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", o =>
    {
        o.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,
            ValidateAudience = false,
            IssuerSigningKey = key,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true
        };
    });
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

app.MapPost("/token", () =>
{
    var claims = new[] { new Claim(ClaimTypes.Name, "alice") };
    var jwt = new JwtSecurityToken(claims: claims, expires: DateTime.UtcNow.AddMinutes(30), signingCredentials: creds);
    var token = new JwtSecurityTokenHandler().WriteToken(jwt);
    return Results.Json(new { access_token = token });
});

app.MapGet("/who", [Microsoft.AspNetCore.Authorization.Authorize] () => "ok");

Умовні оновлення з ETags (якщо}Match)

record Todo(int Id, string Title, string Version);
var store = new Dictionary<int, Todo> { [1] = new(1, "Ship", "v1") };

app.MapGet("/todo/{id:int}", (int id, HttpContext ctx) =>
{
    if (!store.TryGetValue(id, out var t)) return Results.NotFound();
    ctx.Response.Headers.ETag = t.Version;
    return Results.Json(t);
});

app.MapPut("/todo/{id:int}", (int id, HttpContext ctx, Todo input) =>
{
    if (!store.TryGetValue(id, out var current)) return Results.NotFound();
    var ifMatch = ctx.Request.Headers["If-Match"].ToString();
    if (string.IsNullOrEmpty(ifMatch) || ifMatch != current.Version)
        return Results.StatusCode(StatusCodes.Status412PreconditionFailed);

    var next = current with { Title = input.Title, Version = $"v{DateTime.UtcNow.Ticks}" };
    store[id] = next;
    ctx.Response.Headers.ETag = next.Version;
    return Results.Ok(next);
});

Тимчасові дані: складні об' єкти за допомогою JSON

public static class TempDataJsonExtensions
{
    public static void Put<T>(this ITempDataDictionary tempData, string key, T value)
        => tempData[key] = System.Text.Json.JsonSerializer.Serialize(value);

    public static T? Get<T>(this ITempDataDictionary tempData, string key)
        => tempData.TryGetValue(key, out var o) && o is string s
           ? System.Text.Json.JsonSerializer.Deserialize<T>(s)
           : default;
}

// Usage in MVC action
TempData.Put("WizardState", new { Step = 2, Name = "Alice" });
var state = TempData.Get<dynamic>("WizardState");

Коефіцієнт сумісності ECF

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    [Timestamp] public byte[] RowVersion { get; set; } = default!;
}

// On update
try
{
    await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
    return Results.StatusCode(StatusCodes.Status412PreconditionFailed);
}

Освіження претендентів за допомогою re бісаsuing куку- Розпізнавача

app.MapPost("/promote", async (HttpContext ctx) =>
{
    var u = ctx.User;
    var claims = u.Claims.ToList();
    claims.Add(new Claim(ClaimTypes.Role, "Editor"));
    var id = new ClaimsIdentity(claims, "Cookies");
    await ctx.SignInAsync("Cookies", new ClaimsPrincipal(id));
    return Results.Ok();
});

Це повинно покрити прогалини: претенденти на кращу безпеку, дедалі більшу готовність та реальні шаблони для кешування, жетонів та умовних запитів.


Глибоке відхилення: HttpContext. Items (Практичні візерунки і помічники)

HttpContext. Items є пакетом для sper- request (IDictionary <object, object? >), який живе лише протягом життя лише одного запиту. It is applicationly for termed values from middleware/ filters to your end points, controls, and Razor Pages без торкання глобального стану або довготи.

  • Життєвий цикл: створений на початку запиту; відкинений, коли відповідь закінчується.
  • Обсяг: поточний запит не проходить переспрямування або фонової роботи.
  • Швидкодія: O'1) пошук; ідеальна для кожного кешування.
  • Безпека: лише поруч з сервером, його не буде показано клієнтові.

Чому елементи замість

  • Session/TempData: ці запити на хрест і представлення проблем розподілу. Елементи є ефемеральними і масштабними.
  • Служби з масштабуванням DI: використовуйте ці служби для поведінки і спільних залежностей. Елементи краще використовувати для кешу ad- hoc, обчислених значень (перерахуваних, локалі користувача, прапорців функцій) і окремих кешів.
  • HttpContext.Feratures: for prompt/ Transport- level (IEnd pointFeature, IHtpupfinationFerature). Елементи призначено для даних про рівень програм.

Уникайте зіткнень клавіш: сильно введені ключі

Оскільки елементи використовують ключі об' єктів, надайте перевагу приватним статичним ключам об' єкта або визначеному типу ключа, щоб уникнути зіткнень назв.

public static class ItemKeys
{
    public static readonly object TenantId = new();
    public static readonly object UserLocale = new();
    public static readonly object PerRequestCache = new();
}

Або створити введену обгортку з суфіксами назв:

public static class HttpContextItemsExtensions
{
    public static void Set<T>(this HttpContext ctx, object key, T value)
        => ctx.Items[key] = value!;

    public static T? Get<T>(this HttpContext ctx, object key)
        => ctx.Items.TryGetValue(key, out var v) ? (T?)v : default;

    public static T GetOrCreate<T>(this HttpContext ctx, object key, Func<T> factory)
    {
        if (ctx.Items.TryGetValue(key, out var existing) && existing is T typed)
            return typed;
        var created = factory();
        ctx.Items[key] = created!;
        return created;
    }
}

Шаблон: обчислювати у середній програмі, використовувати в кінцевих точках/контролерах/сторінках

// Program.cs
app.Use(async (ctx, next) =>
{
    var tenant = ctx.Request.Headers["X-TenantId"].FirstOrDefault() ?? "public";
    ctx.Set(ItemKeys.TenantId, tenant); // using extension above

    // Per-request cache holder (optional)
    ctx.Set(ItemKeys.PerRequestCache, new Dictionary<string, object?>());

    await next(ctx);
});

// Minimal API
app.MapGet("/whoami", (HttpContext ctx) => new
{
    Tenant = ctx.Get<string>(ItemKeys.TenantId),
});

// MVC Controller
public IActionResult WhoAmI()
    => Json(new { Tenant = HttpContext.Get<string>(ItemKeys.TenantId) });

// Razor Page handler
public IActionResult OnGet()
    => new JsonResult(new { Tenant = HttpContext.Get<string>(ItemKeys.TenantId) });

Шаблон: для уникнення повторної роботи кешем

Використовуйте об'єкти як мініатюрний кеш, отже повторно читається у межах тієї самої просьби. Не використовуйте бази даних re-hit/services.

public static class PerRequestCacheExtensions
{
    public static async Task<T> GetOrAddAsync<T>(this HttpContext ctx, string key, Func<Task<T>> factory)
    {
        var bag = ctx.Get<Dictionary<string, object?>>(ItemKeys.PerRequestCache)
                  ?? ctx.GetOrCreate(ItemKeys.PerRequestCache, () => new Dictionary<string, object?>());

        if (bag.TryGetValue(key, out var val) && val is T hit)
            return hit;

        var created = await factory();
        bag[key] = created!;
        return created;
    }
}

// Usage in endpoint
app.MapGet("/profile", async (HttpContext ctx, IUserRepo repo) =>
{
    var userId = ctx.User.Identity?.Name ?? "anon";
    var profile = await ctx.GetOrAddAsync($"profile:{userId}", () => repo.LoadAsync(userId));
    return Results.Json(profile);
});

Примітки:

  • Розгалуження гілок: один запит типово виконується на одному логічному шляху; Its' tin- safe для паралельних записів. Якщо ви розпочнете паралельні завдання з спільними завданнями, додайте вашу власну синхронізацію.
  • Розмір: Зберігати малі і дешеві для обчислення/ перевищення. Його значення в- пам' яті за запитом.

Шаблон: фільтри, що заповнюють елементи (сторінки MVC/Razor)

public class TenantFilter : IAsyncResourceFilter
{
    public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
    {
        var tenant = context.HttpContext.Request.Headers["X-TenantId"].FirstOrDefault() ?? "public";
        context.HttpContext.Set(ItemKeys.TenantId, tenant);
        await next();
    }
}

// Register filter globally
services.AddControllersWithViews(o => o.Filters.Add<TenantFilter>());

Шаблон: багаті колоди без кореспонденції всюди.

Обчислює один раз, а потім читається в областях лісозаготівлі або в середньому програмному забезпеченні.

app.Use(async (ctx, next) =>
{
    var correlationId = ctx.Request.Headers["X-Correlation-Id"].FirstOrDefault() ?? Guid.NewGuid().ToString("n");
    ctx.Items["CorrelationId"] = correlationId; // string key acceptable for app-local use

    using (logger.BeginScope(new { CorrelationId = correlationId }))
    {
        await next(ctx);
    }
});

Якщо не використовувати елементи

  • Дані, потрібні після переспрямування або через запити (скористайтесь PurchData/Session/DB).
  • Широкі однотони або кеш з перехресними запитами (використовуйте IMemoryCache/IDistribatedCache).
  • Значення, які належать до особи/ уповноваження (використовуйте претенденти/політики).

Швидке правило: якщо воно обчислюється під час цього прохання і читається у межах цього прохання вашим власним кодом, елементи є ідеальними.

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.