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
UPDATE (2025-11-10): Voegde meer praktische voorbeelden van mijn werkelijke codebase tonen real-world patronen, trade-offs, gotchas, en de evolutie van eenvoudige tot geavanceerde state management benaderingen. Bevat gedetailleerde IMemoryCache patronen, ViewBag gebruik (goed en slecht), ResponseCache/OutputCache strategieën, en lessen geleerd uit de productie.
HTTP is beroemde staatloze. Uw app... is niet. Gebruikers aanmelden, items toevoegen aan manden, hop tussen pagina's, terug te keren morgen, en verwachten dat u zich te herinneren. In ASP.NET Core (MVC, Razor Pages, Minimal API's), zijn er een heleboel manieren om te bewaren en overdracht staat tussen verzoeken. Sommige zijn alleen per aanvraag. Sommige laatste voor een sessie. Sommigen wonen in de client. Sommige zijn gedistribueerd en overleven server herstart. Elke keuze heeft trade-offs over veiligheid, prestaties, schaal, en ergonomie van de ontwikkelaar.
Dit bericht catalogiseert de opties, toont concrete, kopieerbare voorbeelden in alle drie stacks, en geeft u beslissing begeleiding, zodat u kunt kiezen het juiste gereedschap voor de taak.
OPMERKING: Dit maakt deel uit van mijn experimenten met AI (ondersteund opstellen) + mijn eigen bewerking. Zelfde stem, zelfde pragmatisme; gewoon snellere vingers.
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 voorbeeld (alle stapels):
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"] });
Voorbeelden
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();
Het genereren van links die status behouden:
// 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);
});
Idempotentie (patroon):
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-patroon in MVC/Razor-pagina's:
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 });
}
Minimale API-voorbeeld:
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 Pagina's gebruik is identiek via HttpContext.
Gebruik voor integriteit/vertrouwelijkheid het ASP.NET Core Data Protection-systeem om payloads te beschermen die u zelf in cookies plaatst.
Instellen (Program.cs):
builder.Services.AddControllersWithViews().AddSessionStateTempDataProvider(); // optional
builder.Services.AddSession();
var app = builder.Build();
app.UseSession();
In MVC-controller:
TempData["StatusMessage"] = "Saved!";
return RedirectToAction("Index");
In Razor Page handler:
TempData["StatusMessage"] = "Saved!";
return RedirectToPage("/Index");
In beeld/pagina:
@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]
Configureren:
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();
Sessie gebruiken (elke stapel):
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);
});
Sessiehelpers:
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;
}
Ik heb ooit gewerkt aan een enorm Britse regering IT-project waar sessie staat misbruik (onder vele andere architectonische zonden) werd een prestatie-killing bottleneck. Het team had gevuld alles in sessie: gebruikersvoorkeuren, multi-stap vorm gegevens, zoekresultaten, tijdelijke berekeningen, zelfs gecached lookups die in een juiste cache of database had moeten zijn.
Het probleem: Sessie staat werd opgeslagen in-proces (ASP.NET sessie staat in web.config, dit was pre-Core dagen). Elk verzoek moest deserialize massale sessie objecten. Naarmate de belasting steeg, sessie staat ballonnen tot tientallen megabytes per gebruiker. Met duizenden gelijktijdige gebruikers, de servers raakte op geheugen.
De wanhopige oplossing: We vlogen naar HP's faciliteit in Stuttgart om belastingstests uit te voeren op hun Superdome. Europa's krachtigste Windows-machine. Het was een beest: tientallen Itanium processors, honderden gigabytes RAM. Het idee was om te bewijzen dat met genoeg hardware, het systeem kon voldoen aan eisen.
Het resultaat: Zelfs op de Superdome, konden we niet raken de vereiste gelijktijdige gebruikersdoelen. De sessie state architectuur was fundamenteel gebroken. Verticaal schalen kon niet slecht ontwerp. De sessie serialisatie / deserialization overhead, gecombineerd met geheugendruk van massale sessie objecten, betekende dat het systeem gewoon niet kon schalen en niet tegen redelijke kosten.
De nachtmerrie over de beveiliging: Erger dan de performance problemen, ontdekten we een codeerfout die sessiestaat veroorzaakte om lek tussen gebruikers. Gebruiker A's sessie data zou af en toe verschijnen in Gebruiker B's sessie. Dit was niet alleen gênant het was catastrofaal. De gebruikers waren NHS-personeel We hadden per ongeluk een inbreukmechanisme voor gegevensbescherming gecreëerd dat gevoelige medische informatie tijdens de sessies van verschillende beroepsbeoefenaren in de gezondheidszorg aan het licht kon brengen.
Wat had er moeten gebeuren:
De les: Sessie staat schalen niet verticaal en nauwelijks horizontaal (zelfs met plakkerige sessies of gedistribueerde winkels, je bent nog steeds serialiseren / deserialiseren op elk verzoek). Het Superdome experiment bewees dat het gooien van hardware op architectonische problemen is duur en vaak zinloos.
Maar nog belangrijker: session state bugs worden beveiligingskwetsbaarheden. Thread-safety kwesties, racevoorwaarden, onjuiste sessie ID behandeling deze niet alleen leiden tot prestatieproblemen, ze kunnen lek gevoelige gegevens tussen gebruikers. In een gezondheidszorg context (of bankieren, of een gereguleerde industrie), dit is een compliance nachtmerrie en potentiële strafrechtelijke aansprakelijkheid.
Modern advies:
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);
});
IDistributedCache (bv. Redis):
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");
});
Cache-as-state anti-patroon waarschuwing: als het moet duurzaam of gezaghebbend, op te slaan in een database en optioneel cache.
Braaklegging (meest voorkomende):
Doorlezen (via een bibliotheek/provider): cache behandelt het laden van misses.
Write-through: schrijfsels gaan synchroon naar cache en backing store.
Schrijf-achter: schrijf naar cache, flush om asynchroon op te slaan (risico: verlies/onverenigbaarheid).
Refresh-ahead: ververs hete sleutels voordat ze vervallen om koude missies te voorkomen.
Wanneer u veel gerelateerde items moet ongeldig maken:
Versiede voorvoegsels (soft invalidation): bump een globale versie in een kleine toets en componeer er sleutels mee.
// version key: "v:products"; keys like $"{version}:product:{id}"
var version = await cache.GetStringAsync("v:products") ?? "1";
var key = $"{version}:product:{id}";
Om alle producten ongeldig te maken: increase v:products (clients zullen natuurlijk oude prefixed keys missen).
Tag set per groep (Redis): bewaar een Set sleutels per tag; bij ongeldigheid, ophalen van leden en verwijderen.
// 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 ongeldigheid: publiceer een "invalidate:key" bericht; elke knooppunt verwijdert de sleutel van de lokale IMemoryCache.
Scannen met patronen: SCAN/KEYS moet worden vermeden in prod hot paden; oké voor admin tooling op kleine sleutelruimtes.
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;
}
Hier is hoe ik eigenlijk gebruik IMemoryCache in mijn blog platform, geëvolueerd door middel van trial en fout. Ik zal u drie echte patronen van eenvoudig tot verfijnd.
Dit was mijn eerste caching implementatie. Blog categorieën veranderen niet vaak, dus cache ze voor 30 minuten:
// 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;
}
Waarom dit werkt:
Ik heb je geraakt: In eerste instantie heb ik een string key "Categorieën" gebruikt. Werkt prima totdat je meerdere controllers hebt en men per ongeluk dezelfde sleutel hergebruikt. Nu gebruik ik constanten of sterk getypte toetsen (zie de vorige sectie over het vermijden van botsingen).
Deze caches vertaalstatus per gebruiker. Het is complexer omdat het grenzen nodig heeft en in leven moet blijven zolang de gebruiker actief is:
// 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)
});
}
}
Waarom dit anders is:
Foutje dat ik heb gemaakt: Aanvankelijk heb ik het aantal taken niet beperkt. Een power user activeerde 50+ vertalingen en ik had een geheugenlek. Nu houd ik max 5 per gebruiker.
Afhandelen: Dit zal niet schalen naar miljoenen gebruikers. Als dit een probleem wordt, zou ik naar IDistributedCache (Redis) of opslaan in de database met een index op userId + startTime.
Deze caches analytics metrics en tracks cache effectiviteit met behulp van Serilog traceren:
// 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;
}
}
Wat maakt deze productie klaar:
yyyyMMdd formaat in sleutel zo verschillende tijden op dezelfde dag delen van de cacheEvolution: Aanvankelijk heb ik 10 minuten gecached. Maar Umami metrics zijn zwaar te halen en niet veel veranderen, dus 1 uur is prima. Ik ontdekte dit door te kijken naar de SerilogTracing gegevens en het zien van buitensporige cache mist.
Monitoring in actie: In Seq (mijn log aggregator) kan ik vragen:
ActivityName = "GetMetricsWithPrefix" and CacheHit = false
Dit vertelt me mijn cache miss rate. Als het hoog is, pas ik TTL of de belangrijkste strategie aan.
Patronen Gebruik Case Everything Size Control Observability |---------|----------|------------|--------------|---------------| | Categorieën Mondiale, zelden wisselende 30 min absolute Onnodige (kleine) basis houtkap | Vertaaltaken Per gebruiker, begrensd 6h absolute + 1h schuifhandleiding (5 items max.) Geen (moet toevoegen!) | Metrics Dure externe gesprekken 1h absolute natuurlijke (tijdelijk) volledige tracking
Belangrijkste lessen:
Als ik IMemoryCache niet gebruik:
Cookie auth instellen:
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();
Aanmelden bij vorderingen (MVC/minimaal):
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("/");
});
Lees claims (elke stapel):
[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;
});
Gebruik:
[Authorize(AuthenticationSchemes = "Bearer")]
app.MapGet("/secure", () => "ok");
Overzicht van de zeemeermin:
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 sketch:
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 voorbeeld:
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");
});
Ik gebruik zowel ResponseCache als OutputCache samen op mijn blog voor verschillende doeleinden. Hier is waarom je beide wilt en hoe ze verschillen.
ASP.NET Core heeft twee soortgelijke cachingsystemen:
Cache-Control, Vary) vertellen browsers en CDN's hoe te cache
eZe vullen elkaar aan. ResponseCache behandelt client/CDN-caching; OutputCache behandelt server-side-caching.
// 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);
}
Wat gebeurt er als iemand erom vraagt? /blog/my-post:
Uitvoercache controleert eerst: Heb ik een gecached antwoord voor my-post + en taal?
ResponseCache stelt headers in: Na OutputCache genereert de reactie, ResponseCache voegt toe:
Cache-Control: public, max-age=300
Vary: hx-request
Browsercaching: Browser caches het antwoord voor 300 seconden (5 minuten). Latere verzoeken van dezelfde gebruiker niet eens op de server.
CDN-caching (als je Cloudflare/Fastly gebruikt): CDN caches gedurende 5 minuten. Gebruikers wereldwijd drukten op de CDN, niet op mijn server.
[ResponseCache(Duration = 300)] // 5 minutes client/CDN cache
[OutputCache(Duration = 3600)] // 1 hour server cache
Reden:
Evolution: Aanvankelijk had ik beide op 5 minuten. Maar dat betekende dat mijn server elke 5 minuten opnieuw renderde, ook al verandert de inhoud zelden. Nu:
VaryByHeader = "hx-request" // ResponseCache
VaryByHeaderNames = new[] { "hx-request" } // OutputCache
Waarom dit belangrijk is: HTMX-verzoeken omvatten hx-request: true kop. Ik geef verschillende reacties terug:
Zonder VaryBy, de cache zou het verkeerde formaat teruggeven. VaryBy, ik cache twee versies van elke pagina.
Voorbeeld:
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) })]
Probleem zonder dit: /blog/my-post?language=fr zou dienen voor de Engelse gecachede versie.
Met VaryByQueryKeys: Aparte cache-items:
/blog/my-post?language=en → Cache sleutel bevat "en"/blog/my-post?language=fr → Cache sleutel bevat "fr"Echt gebruik uit mijn bloglijst:
[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, /* ... */)
Waarschuwing voor de explosie van de cache: Elke unieke combinatie van parameters = aparte cache-item:
page=1&pageSize=20&language=en → Eén itempage=2&pageSize=20&language=en → Nog een itempage=1&pageSize=10&language=en → Nog een itemMitigatie:
ResponseCache werkt uit de doos, maar voor OutputCache je hebt setup nodig:
// 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
OutputCache is overgeslagen voor:
Set-Cookie kopCache-Control: no-storeVoorbeeld waar ik het niet gebruik:
// 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
}
Ik gebruik Prometheus metrics (exposed via mijn app) om te volgen:
// Pseudo-code for metrics
cache_hits_total{cache="output"} 45230
cache_misses_total{cache="output"} 892
Mijn cache hit rate: ~98% voor blog berichten (de meeste verkeer raakt dezelfde populaire berichten herhaaldelijk).
Impact:
Soms ben ik aan het debuggen en heb ik elke keer nieuwe reacties nodig:
// During development, comment out caching
// [ResponseCache(Duration = 300, ...)]
// [OutputCache(Duration = 3600, ...)]
public async Task<IActionResult> Show(string slug, string language = "en")
Of gebruik omgevingsspecifieke instellingen:
#if DEBUG
// No caching in development
#else
[ResponseCache(Duration = 300, ...)]
[OutputCache(Duration = 3600, ...)]
#endif
Betere aanpak: Configuratie gebruiken:
[ResponseCache(Duration = responseCacheDuration, ...)]
waarbij responseCacheDuration is 0 in ontwikkeling, 300 in productie.
Voordelen:
Nadelen:
Als ik caching oversloeg:
Mijn oordeel: Voor een blog met meestal statische inhoud en een hoge lees-/schrijfverhouding is dubbele caching een enorme winst. Ik zou dit niet gebruiken op admin panelen of dashboards met snel veranderende gegevens.
Deze drie zijn vaak verward. Hier is hoe ze verschillen en wat ik eigenlijk gebruik in de productie.
// 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");
WeergaveData ViewBag TempData
| --------- | ---------- | --------- | ---------- |
|---|---|---|---|
| Levensduur * Huidige aanvraag * Huidige aanvraag * One redirect * | |||
| Toegang tot sleutels Snarentoetsen Syntaxis van de Eigenschappen Snarentoetsen | |||
| Type veiligheid Geen (casts need) Geen (dynamisch) Geen (casts need) | |||
| Controle van de compilatietijd Nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee | |||
| Survives redirect Nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee, nee |
In mijn blog gebruik ik ViewBag uitsluitend voor het doorgeven van gegevens van controllers naar gedeelde lay-out (analyses, categorieën, enz.):
// 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);
}
Dan in mijn lay-out (_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>
}
Waarom dit patroon werkt:
Fouten die ik eerder maakte: Ik was aan het setten. ViewBag.Categories in elke actie methode. DRY overtreding en gemakkelijk te vergeten. OnActionExecutionAsync In de basis controller opgelost dit.
// 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);
}
Met het oog op:
@{
ViewData["Title"] = ViewBag.Title; // Standard MVC convention for <title>
}
<h1>Category: @ViewBag.Category</h1>
Dit is oké omdat:
Title en Category eigenschappen van elk weergavemodelAntipatroon:
// 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 };
Problemen:
ViewBag.Usr faalt op runtime)Beter: Sterk getypte weergavemodellen:
// 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);
}
Standaard TempData use case:
[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();
}
Waarom ik TempData niet gebruik in mijn blog:
[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!"
});
}
Wanneer TempData zinvol is:
TempData gotcha: Standaard ondersteund door cookies (sinds ASP.NET Core 2.0). Als je grote objecten in TempData plaatst, blaas je de cookie op die met elk verzoek wordt verstuurd. Gebruik Sessie voor grote toestanden met een backing store of 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]
Mijn regels:
Welke staatshouder te gebruiken?
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
Voorbeeld (DB + routesleutel):
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");
Scheermesweergave:
@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 (indien vereist door toestemming/functionele behoefte).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
}
Snelle picks:
Alle drie de stapels zitten op dezelfde primitieven (HttpContext, modelbinding, auth, gegevensbescherming). Uit bovenstaande voorbeelden blijkt dat de API's meestal verschillen in ergonomie:
Results.*.Ze delen allemaal dezelfde staatsmechanismen die hier besproken worden.
Na het tonen van al deze opties, hier is mijn eerlijke beoordeling van wat werkt in productie voor mijn blog platform.
Mechanisme Frequency Use Cases Tevredenheid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . |-----------|-----------|-----------|--------------| | IMemoryCache Very High Categories, metrics, vertaaltaken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . | OutputCache Hoog Gerenderde blog posts, lijsten, enorme perf winnen | ResponseCache Hoge HTTP-caching headers werkt met OutputCache | WeergaveBag Meer informatie Analytics settings, paginatitels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . | Auth Claims Gebruikersidentiteit, admin-vlagjuist hulpmiddel voor auth | Route/Query Paginatie, filtering, kogels staatloos en koppelbaar. | Database Vertaald uit het Frans. | Cookies Gebruikersvoorkeuren (toekomst) Nog niet nodig | Directoraat Nooit meer | TempData Nooit--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | HttpContext.Items Nooit | IDistributedCache Nooit meer
Waarvoor ik het gebruik:
Voordelen in de praktijk:
Nadelen die ik heb geraakt:
Schaallimiet: Als ik bij meerdere servers kom, heb ik IDistributedCache (Redis) nodig voor gedeelde status. Voor nu is single server + memory cache perfect.
Waarvoor ik het gebruik:
Voordelen in de praktijk:
Nadelen die ik heb geraakt:
Beste voor: Lezen zware toepassingen met meestal statische inhoud. Niet goed voor gepersonaliseerde of real-time gegevens.
Waarvoor ik het gebruik:
Voordelen in de praktijk:
Nadelen die ik heb geraakt:
Regel Ik volg: ViewBag alleen voor eenvoudige scalars. Complexe objecten gaan in ViewModels.
Waarvoor ik het gebruik:
sub claim tegen config)Voordelen in de praktijk:
User.Claims overalNadelen die ik heb geraakt:
Beste praktijk: Houd claims minimaal en stabiel. Zet niet vaak wisselende gegevens in claims.
Sessie (nooit gebruikt):
TempData (nooit gebruikt):
HttpContext.Items (nooit gebruikt):
IDistributedCache (nooit gebruikt):
Fase 1 (eerste fase): Elk verzoek raakte de database en gaf Markdown.
Fase 2 (eerste optimalisatie): Toegevoegd IMemoryCache voor categorieën. Zag onmiddellijke DB load reductie. Hield het eenvoudig: 30 min TTL, geen chique logica.
Fase 3 (opschaling): Toegevoegd OutputCache voor blog berichten wanneer het verkeer piekte. Massale prestatieverbetering. Eerste fout: alleen gecached voor 5 minuten. Toegenomen tot 1 uur na monitoring toonde inhoud zelden veranderingen.
Fase 4 (waarneembaarheid): Toegevoegd SerilogTracing aan metrics cache. Ontdekte cache misses waren hoog als gevolg van datumopmaak in sleutels. yyyyMMdd In plaats van full time stempels. Hit rate ging van 60% naar 95%.
Fase 5 (HTMX-integratie): Toegevoegd VaryByHeader voor hx-request. Aanvankelijk vergat dit en diende volledige pagina's aan HTMX verzoeken. Debuggen nachtmerrie totdat ik erachter kwam.
Huidige stand: Fijn met de stack. IMemoryCache + OutputCache + ResponseCache handle 98% van mijn state management behoeften. Database voor duurzame staat. Auth claimt voor identiteit. Dat is het.
Start hier:
Toevoegen indien nodig: 6. Sessie (alleen als je server-side conversational status moet hebben) 7. IDistributedCache (alleen wanneer u schaalt naar meerdere servers) 8. Cookies (voor voorkeuren aan klantenzijde, toestemming)
Vermijd:
Belangrijkste lessen:
Staat in webapps is niet een maat past allemaal. Kies de lichtste optie die voldoet aan uw behoeften, de voorkeur geven aan staatloze patronen wanneer je kunt, en zijn expliciet over veiligheid en levenscyclus.
Als je dieper wilt gaan over hoe deze stukken door de pijpleiding stromen, zie mijn serie beginnen met Deel 1: Overzicht en Stichting en vooral de middleware en routing onderdelen.
Fijn gebouw.
Deze voorbeelden verdiepen de eerdere secties met production-grade details die je kunt plakken in net9 minimale sjablonen, MVC, of Razor Pages.
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();
});
Tip: In multi-node implementaties, blijven de sleutels voor gegevensbescherming (bijv. naar een gedeeld bestandssysteem, Redis, of Azure Key Vault) zodat cookies kunnen worden gelezen over instanties.
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] en gebruik @Html.AntiForgeryToken() in formulieren.asp-antiforgery="true" indien nodig.IAntiforgery zoals getoond.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();
Bewaar alleen kleine, comprimeerbare gegevens. Persist echte karren/orders naar een 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);
});
Voor robuuste vergrendeling verkiest u Redis primitieven (SET NX EX) via 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();
});
Dat moet de lacunes dekken: sterkere veiligheidsdefaults, multi-node gereedheid, en real-world patronen voor caches, tokens, en voorwaardelijke verzoeken.
HttpContext.Items is een per-request tas (IDictionary<object, object?>) die alleen leeft voor de levensduur van een enkele aanvraag. Het is perfect voor het doorgeven van berekende waarden van middleware/filters aan uw eindpunten, controllers, en Razor Pages handlers zonder het raken van globale staat of langlevende winkels.
Omdat Items gebruik maakt van objectsleutels, verkiest private statische objectsleutels of een specifiek sleuteltype om naambotsingen te voorkomen.
public static class ItemKeys
{
public static readonly object TenantId = new();
public static readonly object UserLocale = new();
public static readonly object PerRequestCache = new();
}
Of maak een getypte wrapper met extensies:
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) });
Gebruik Items als een kleine cache zo herhaalde leest binnen hetzelfde verzoek niet re-hit databases / 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);
});
Opmerkingen:
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>());
Bereken één keer, lees dan in logscopen of middleware.
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);
}
});
Snelle regel: Als het wordt berekend tijdens dit verzoek en gelezen binnen dit verzoek door uw eigen code, Items is ideaal.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.