This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Sunday, 09 November 2025
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
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);
});
app.MapGet("/whoami", (HttpContext ctx) => new { Tenant = ctx.Items["TenantId"] });
public IActionResult WhoAmI() => Json(new { Tenant = HttpContext.Items["TenantId"] });
public IActionResult OnGet() => new JsonResult(new { Tenant = HttpContext.Items["TenantId"] });
Приклади
app.MapGet("/orders/{id:int}", (int id, int? page) => Results.Ok(new { id, page }));
// GET /orders/5?page=2
[HttpGet("/orders/{id:int}")]
public IActionResult Details(int id, int? page)
=> View(new { id, page });
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)
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 на сторінках 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)
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Save(SettingsModel model)
{
// validate & persist
return RedirectToAction(nameof(Summary), new { tab = model.SelectedTab });
}
public IActionResult OnPost(SettingsModel model)
{
return RedirectToPage("/Settings/Summary", new { tab = model.SelectedTab });
}
Приклад мінімального 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.
Налаштування (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 Ми випадково створили механізм захисту даних, який міг викривати чутливу медичну інформацію на різних сеансах охорони здоров'я.
Що мало статися:
Урок: Стан сеансу не масштабується вертикально і заледве горизонтально (навіть з липкими сеансами або розподіленими крамницями, ви все ще синхронізуєте або вилучаєте всі запити). Експерименти Superdomon довели, що використання обладнання у архітектурних проблемах є дорогим і часто марним.
Але важливіше: Жуки стану сеансу стають вразливими до безпекиУ середовищі охорони здоров'я (або банківських банків, або будь-якої регульованої індустрії) ці дані є жахом і потенційними кримінальними зобов'язаннями.
Сучасна порада:
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");
});
Приклад кешу: якщо він має бути тривалим або допустимим, зберігати його у базі даних і за бажання кешувати його.
Кеш- aide (більшість поширених):
Прочитане (за допомогою засобу керування бібліотекою/ оптимізатором): засіб для роботи з кешем під час завантаження промахи.
Запис- через: запис йде до кешу і резервного сховища синхронно.
Запис- behind: запис до кешу, злив, щоб зберегти асинхронно (ймовірно: втрата/ несумісність).
Оновити: освіжити гарячі ключі до того, як вони застаріють, щоб уникнути холодних промахів.
Якщо вам потрібно заборонити багато пов' язаних записів:
Префікси з версіями (м' якіше поновлення): виштовхніть глобальну версію у малій клавіші і створіть з нею клавіші.
// 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 слід уникати у прострочених гарячих шляхах. Гаразд для адміністративних інструментів у малих просторах ключів.
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);
});
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 на моїй платформі блогу, еволюціонував шляхом спроб і помилок. я покажу вам три справжні шаблони від простого до складного.
Це була моя перша реалізація кешування. Категорії блогу не часто змінюються, отже кешувати їх протягом 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;
}
Чому це працює:
Попався: Спочатку я використав клавішу рядка " Categories ." Працює добре, аж доки у вас не з' явиться декілька контролерів і один з них випадково повторно використовує той самий ключ. Тепер я користуюся сталими або потужними клавішами (див. попередній розділ щодо уникнення зіткнень).
За допомогою цього пункту можна закрити стан перекладу для кожного з користувачів. Цей стан є складнішим, оскільки він потребує обмеження і повинен залишатися живим, якщо користувач є активним:
// 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)
});
}
}
Чому це є інакше:
Помилка, яку я зробив: Спочатку я не обмежував кількість завдань: користувач з живленням увімкнув 50+перекладів, а у мене витік пам' яті. Тепер я зберігаю максимальну 5 на кожного користувача.
Зменшення торгівлі: Це не вплине на стан мільйонів користувачів. Якщо це станеться проблемою, я перейду до IDistribedCache (Redis) або зберігатиму у базі даних покажчик на userId + startTime.
Це кешує аналітичні вихідні дані і ефективність кешу доріжок за допомогою слідкування за серілогами:
// 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 (повинно додати!) * | Метрики Передбачається зовнішнє з'єднання, тобто абсолютне (часовий-відкритий) ведьйо
Уроки з ключів:
Якщо я не користуюся IMoryCache:
Автентифікація кук:
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) }));
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
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);
});
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");
});
Ось чому вам потрібні обидва варіанти, і те, як вони відрізняються.
Ядро ASP.NET має два подібні системи кешування:
Cache-Control, Vary) повідомити навігаторам і CDN про те, як кешувати
eВони доповнюють один одного. 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:
Спочатку перевіряються вивідCache: Чи є у мене кешована відповідь my-post + en мовою?
ReferenceCache встановлює заголовки: Після того, як Cache створить відповідь, AspectCache додає:
Cache-Control: public, max-age=300
Vary: hx-request
Кечування навігатора: У переглядачі зберігається відповідь на 300 секунд (5 хвилин). Підприємницькі запити одного користувача навіть не нападають на сервер.
Кечування CDN (якщо ви використовуєте Glowflare/Fastly): кешування CDN протягом 5 хвилин. Користувачі по всьому світі вмикають CDN, а не мій сервер.
[ResponseCache(Duration = 300)] // 5 minutes client/CDN cache
[OutputCache(Duration = 3600)] // 1 hour server cache
Розмірковуйте:
Еволюція: Спочатку у мене було обом за 5 хвилин, але це означало, що мій сервер знову здавав кожні 5 хвилин, хоча зміст рідко міняється.
VaryByHeader = "hx-request" // ResponseCache
VaryByHeaderNames = new[] { "hx-request" } // OutputCache
Чому це є важливим: Включено запити HTMX hx-request: true заголовок. Я повертаю різні відповіді:
Без 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
[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 → Інший записМітифікація:
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 пропущено для:
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% для дописів блогів (більшість адрес даних неодноразово потрапляє у ті самі популярні дописи).
Вплив:
Іноді я знеохочую і потребую свіжих відповідей кожного разу:
// 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 у виробництві.
Прос:
Збори:
Коли я пропускав катування:
Мій вирок: Для блогу, який має здебільшого статистичний зміст і високий рівень read/write, подвійне кешування - це велика перемога. Я б не використовував його на адміністраціях або панельх приладів з швидкою зміною даних.
Ці три часто плутаються. ось як вони відрізняються і що я насправді використовую для виробництва.
// 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 виключно для передавання даних від контролерів до спільного компонування (аналітики, категорії тощо):
// 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>
}
Чому цей зразок діє:
Помилка, яку я зробив раніше: Я складав. 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>
Це нормально, тому що:
Title і Category Властивості для кожної моделі переглядуЗразок:
// 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 };
Проблеми:
ViewBag.Usr не вдалося під час виконання)Кращі: потужні моделі перегляду:
// 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();
}
Чому я не користуюся тимчасовою базою даних у своєму блозі:
[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!"
});
}
Якщо RupData має сенс:
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]
Мої правила:
Який власник штату?
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
Приклад (клавіша 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");
});
TempData["Flash"] = "Profile saved";
return RedirectToAction("Index");
Перегляд Razor:
@if (TempData["Flash"] is string flash) {
<div class="alert alert-info">@flash</div>
}
flowchart LR U[User] -- cart-id cookie --> S[Server] S --> DB[(Cart Table)] S <--> Cache[Distributed Cache]
Secure, HttpOnly, SameSite, IsEssential (якщо потрібно для згоди/ функціонування).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
}
Швидкий вибір:
Всі три стоси знаходяться на однакових примітивах (HtpContext, модель прив' язки, розпізнавання, захист даних). Наведені вище приклади показують, що API здебільшого відрізняються у ергономіці:
Results.*.Вони всі мають однакові державні механізми, про які тут говориться.
Після того, як я покажу вам усі ці варіанти, ось моя чесна оцінка того, що працює в постановці для моєї платформи в блозі.
♪ Mechanismission Використовуй субстанції Satisf} |-----------|-----------|-----------|--------------| | IMemoryCache * ♪ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | ExputCache Д - р Ґейттерс дзвінок у блогі, далі: | AspectCache Дзвінок: | ViewBag Д. д. д. д. д. д. д. д. д. д. д. д. д. д. | Автентифікаційні оголошення }Ідентифікація користувача, адміністративний прапор' ї Правий інструмент для auth | Маршрут/ Запитання Дівка, фільтрація, без сторонньої лінії та зв'язок | База даних Д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. д. | Куки Д. д. д. д. д. д. д. д. д. д. д. д. д. д. | Сеанс ♪ Never * - ♪ Not security picture ♪ | Тимчасові дані ♪ HTMX стирає потребу ♪ | HttpContext.Items Неужели не было воспользоваться делом | IDistributedCache ♪Never ♪ - ♪ Одинокий сервер (на даний час) ♪
Для чого я використовую його:
Процедура в практиці:
Спонсори, які я збив:
Обмеження масштабування: Якщо я добираюся до декількох серверів, мені знадобиться програма IDistributedCache (Redis) для спільного стану. У поточній версії один сервер + кеш пам' яті є досконалим.
Для чого я використовую його:
Процедура в практиці:
Спонсори, які я збив:
Найкраще для: Втома для читання програма з здебільшого статичним вмістом. Не придатна для персоналізованих або реальних даних.
Для чого я використовую його:
Процедура в практиці:
Спонсори, які я збив:
Правило, яким я дотримуюсь: ViewBag лише для простих скалярів. Складні об' єкти можна знайти у пунктах ViewModels.
Для чого я використовую його:
sub претендувати на налаштування)Процедура в практиці:
User.Claims всюдиСпонсори, які я збив:
Найкраща вправа: Зберігати дані мінімальними і стабільними. Не вкладайте дані, що часто змінюються.
Сеанс (ніколи не використовується):
Тимчасові дані (ніколи не використовується):
HttpContext. Items (ніколи не використовується):
IDistributedCache (ніколи не використовується):
Фаза 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 претендує на особу.
Почати тут:
За потреби додавати: 6. Сеанс (лише якщо вам потрібен стан спілкування на сервері) 7. IDistribedCache (лише у разі зміни масштабу на декілька серверів) 8. Куки (для уподобань клієнтів, згода)
Уникати:
Уроки з ключів:
Стан у веб- програмах не є одним розміром, який пасує всім. Виберіть найсвіжіший параметр, який задовольняє ваші потреби, надаєте перевагу несуттєвим візерункам, якщо ви можете, і будьте конкретними щодо безпеки та життєвого циклу.
Якщо ви хочете пройти глибше про те, як ці частинки проходять через трубопровод, подивіться, як починається моя серія з Частина 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), отже куки можна читати у різних екземплярах.
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();
[ValidateAntiForgeryToken] і використання @Html.AntiForgeryToken() у формах.asp-antiforgery="true" якщо потрібно.IAntiforgery як показано.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.
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
}));
});
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.
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");
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);
});
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");
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);
}
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 є пакетом для sper- request (IDictionary <object, object? >), який живе лише протягом життя лише одного запиту. It is applicationly for termed values from middleware/ filters to your end points, controls, and Razor Pages без торкання глобального стану або довготи.
Оскільки елементи використовують ключі об' єктів, надайте перевагу приватним статичним ключам об' єкта або визначеному типу ключа, щоб уникнути зіткнень назв.
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);
});
Примітки:
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);
}
});
Швидке правило: якщо воно обчислюється під час цього прохання і читається у межах цього прохання вашим власним кодом, елементи є ідеальними.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.