Back to "Staat behouden tussen verzoeken in ASP.NET Kern: Een praktische, no-nonsense gids (MVC, rasterpagina's, minimale API's)"

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

AI-Article ASP.NET ASP.NET Core State Web Development

Staat behouden tussen verzoeken in ASP.NET Kern: Een praktische, no-nonsense gids (MVC, rasterpagina's, minimale API's)

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.

Inleiding

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.

Het Landschap bij een Glance

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
  • Client-carryed: query, route, headers, formulieren, cookies, JWT. Schaalt horizontaal maar is zichtbaar voor de client (moet worden gevalideerd/gesigneerd/versleuteld indien van toepassing).
  • Server-carryed: TempData, Session, caches, DB. Vereist affiniteit of distributie strategie.
  • Per verzoek: HttpContext.Items - nuttig voor het intern doorgeven van gegevens tijdens één enkele aanvraag.

Gouden Regels (voordat we in API's duiken)

  1. Veiligheid eerst: Staat lekkage is een echte bedreiging. Server-side staat (Session, in-memory caches) kan lekken tussen gebruikers door het coderen van fouten, racevoorwaarden, of incorrecte lifecycle management. Stateless patronen zijn niet alleen meer schaalbaar en zijn veiliger.
  2. Liever "stateless server" patronen wanneer je nodig hebt om horizontaal te schalen (push status aan client tokens, duurzame winkels, of gedistribueerde caches).
  3. Vertrouw nooit client-gecontroleerde staat. Valideren, ondertekenen en/of versleutelen.
  4. Houd grote gegevens uit cookies en headers; ze opblazen elk verzoek.
  5. Gebruik TempData alleen voor Post-Redirect-Get (PRG) one-shots zoals flitsberichten.
  6. Gebruik Sessie alleen wanneer u server-side conversational state moet handhaven en u distributie gepland hebt en paranoïde moet zijn over beveiliging.
  7. Claims zijn voor identiteit en grofkorrelige toestemming, niet voor algemene app-status.
  8. Cache is geen bron van waarheid. Terug met een duurzame winkel als de gegevens belangrijk zijn.

Per-Request State: HttpContext.Items

  • Toepassingsgebied: huidige aanvraag uitsluitend (afkorting aan het einde van de pijpleiding)
  • Gebruik voor: het passeren van berekende waarden tussen middleware en eindpunten/controllers
  • Schaal: geen impact
  • Beveiliging: alleen server
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);
});
  • Minimale API eindpunt:
app.MapGet("/whoami", (HttpContext ctx) => new { Tenant = ctx.Items["TenantId"] });
  • MVC-controller:
public IActionResult WhoAmI() => Json(new { Tenant = HttpContext.Items["TenantId"] });
  • Razor Page handler:
public IActionResult OnGet() => new JsonResult(new { Tenant = HttpContext.Items["TenantId"] });

Transient Client Carryed State: Route Values and Query String

  • Toepassingsgebied: huidig verzoek; de cliënt draagt het expliciet.
  • Gebruik voor: navigatiecontext, filtering, paginatie, resource-identiteit
  • Beveiliging: moet valideren/machtigen; geheimen niet insluiten

Voorbeelden

  • Minimale API's:
app.MapGet("/orders/{id:int}", (int id, int? page) => Results.Ok(new { id, page }));
// GET /orders/5?page=2
  • MVC:
[HttpGet("/orders/{id:int}")]
public IActionResult Details(int id, int? page)
  => View(new { id, page });
  • Scheermesjes (Bestellingen/Details.cshtml.cs):
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)

Kopteksten: Concordantietabellen, Idempotency Keys, Feature Flags

  • Toepassingsgebied: huidig verzoek, optioneel overgenomen in antwoorden
  • Gebruik voor: tracing, retry-safety, A/B-vlaggen
  • Beveiliging: behandelen als onbetrouwbare invoer, valideren/whitelist
app.Use(async (ctx, next) =>
{
    var correlationId = ctx.Request.Headers["X-Correlation-Id"].FirstOrDefault()
                      ?? Guid.NewGuid().ToString("n");
    ctx.Response.Headers["X-Correlation-Id"] = correlationId;
    await next(ctx);
});

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

Forms and Hidden Fields (PRG)

  • Toepassingsgebied: volgende aanvraag alleen (client posts back the values)
  • Gebruik voor: tovenaar stappen, anti-forgery tokens, het houden van kleine stukjes staat over PRG
  • Veiligheid: altijd valideren; combineren met antiforgery

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)
  • MVC:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Save(SettingsModel model)
{
    // validate & persist
    return RedirectToAction(nameof(Summary), new { tab = model.SelectedTab });
}
  • Scheerpagina's:
public IActionResult OnPost(SettingsModel model)
{
    return RedirectToPage("/Settings/Summary", new { tab = model.SelectedTab });
}

Cookies: Klein, ondertekend, soms versleuteld

  • Toepassingsgebied: elk verzoek van de browser tot het einde
  • Gebruik voor: voorkeuren, niet-gevoelige vlaggen, toestemming; auth cookies (afzonderlijke sectie)
  • Afspraken: groottelimieten (~4KB per cookie), prestatie-impact, moet voldoen aan de toestemmingswetgeving

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.


TempData: One-Redirect Bericht Bus

  • Toepassingsgebied: overleeft één redirect
  • Backing store: Cookie (standaard) of Sessie
  • Gebruik voor: flitsberichten, validatiesamenvattingen na PRG

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]

Sessie: Server-Side Conversational State

  • Toepassingsgebied: browsersessie (cookiesleutel + serveropslag)
  • Gebruik voor: multi-stap tovenaars, kleine kar data, throttling tellers
  • trade-offs: vereist plakkerige sessies of gedistribueerde backing store; kan de schaal te beperken

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;
}

Een waarschuwend verhaal: Wanneer sessie staat wordt de bottleneck

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:

  1. Standaard staatloos: De meeste van die sessiegegevens hadden nooit mogen bestaan
  2. Database voor duurzame toestand: Multi-step forward had in de database moeten zitten met een workflow ID
  3. Cache voor opzoeken: Gedeelde opzoekingen hoorden in IMemoryCache of een gedistribueerde cache
  4. Client-side voor voorkeuren: Gebruikersvoorkeuren kunnen in cookies of lokale opslag zijn geweest
  5. Verdeelde sessie indien nodig: Als sessie echt nodig was, zou Redis-backed sessie de lading hebben gedeeld

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:

  • Als je meer dan een paar KB sessiegegevens nodig hebt, heb je waarschijnlijk een ontwerpprobleem.
  • Als je gevoelige gegevens in sessies opslaat, creëer je een beveiligingsaanvalsoppervlak.
  • Stateless architecturen niet alleen schaal beter ze zijn inherent veiliger omdat er geen server-side staat om te lekken
  • Heroverweeg uw strategie voor het beheer van de staat voordat u naar Stuttgart moet vliegen (of leg een inbreuk op de gegevens aan de commissaris voor informatie uit)

Caching: IMemoryCache en IDistributedCache

  • Scope: serverproces (IMemoryCache) of gedistribueerd (IDistributedCache)
  • Gebruik voor: afgeleide/berekende gegevens, opzoeken, kortlevende toestand
  • trade-offs: cache ongeldigheid, serialisatie voor gedistribueerd

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.

Kiezen tussen IMemoryCache en IDistributedCache

  • IMemoryCache:
    • Vloeiend snel, in proces, blijven objecten als objecten (geen serialisatie).
    • Uitschakeling door geheugendruk, groottelimiet, absolute/glijdende afloop en prioriteit.
    • Niet gedeeld over knooppunten; goedgekeurd op app recycle/deploy.
    • Geweldig voor per-node hotsets, berekende opzoekingen, korte TTL's.
  • IDistributedCache (Redis/SQL/etc.):
    • Gedeeld over een boerderij; overleeft app herstart; vereist serialisatie (strings/bytes).
    • Iets hogere latentie; doorvoer is afhankelijk van netwerk en backend.
    • Ondersteunt absolute/glijdende vervaldatum (provider-dependent; Redis provider updates TTL op toegang voor glijden).
    • Ideaal voor cross-node consistentie, grote fan-out leest, feature vlaggen, en sessie.

Gemeenschappelijke cachestrategieën

  • Braaklegging (meest voorkomende):

    1. Probeer cache; 2) als missen, laden van bron; 3) schrijven naar cache; 4) retour.
    • Voordelen: eenvoudig; bron van waarheid blijft de database.
    • Nadelen:: first request after expiration is slow; possible stampeds.
  • 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.

Geldigheidsduur, uitzetting en grootte

  • Absolute houdbaarheid: verloopt altijd na een vaste duur (goed voor externe gegevensversheid).
  • Schuiftijd: breidt TTL uit op toegang (goed voor sessies/gebruikersspecifieke gegevens).
  • Grootte-gebaseerde uitzetting (IMemoryCache): instellen entry.Size en configureren SizeLimit om gebonden geheugen.
  • Prioriteit (IMemoryCache): CacheItemPriority.High/Normal/Low/NeverRemove beïnvloedt uitzetting onder druk.
  • Jitter: voeg kleine willekeurige offsets toe aan TTL's om gesynchroniseerd verlopen te voorkomen (stempels).

Voorkomen van cachebuien (donderende kudde)

  • Gebruik GetOrCreate/GetOrCreateAsync (IMemoryCache) om een single-thread populatie per node te garanderen.
  • Verdeeld: gebruik een kortlevende vergrendelingssleutel (SET NX EX) of bibliotheekondersteuning; voeg TTL jitter toe; overweeg achtergrondverversing.
  • Serveer oude-while-revalidate: houd een secundaire sleutel met oude waarde en korte extensie terwijl nieuwe waarde wordt berekend.

Sleutelontwerp en naamspatiëring

  • Prefereer kleine letters, colon-gedelimiteerde sleutels: app:entiteit:123 of huurder:us:gebruikers:42.
  • Voeg versiesegment toe om hele klassen van sleutels ongeldig te maken zonder deletes: v2:products:123.
  • Huurder-bewust: voorvoegsel sleutels met huurder of organisatie id om botsingen te voorkomen en te verlichten zuiveringen.
  • Houd sleutels klein maar beschrijvend; vermijd door de gebruiker gecontroleerde ruwe invoer zonder normalisatie.

Verlopen sets sleutels (tags/groepen)

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.

Praktische helpers

  • IMemoryCache get-or-set met opties:
    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);
      });
    
  • IDistributedCache met JSON en vervaldatum:
    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;
    }
    

Toezicht en zichtbaarheid

  • Track hit/miss rates en gemiddelde laadtijd; ontmasker metrics (Prometheus counters) per key group.
  • Voeg logging rond cache populatie en uitzetting callbacks voor IMemoryCache toe.
  • Voor Redis, bekijk keyspace hits/misses, latency, en geheugenfragmentatie; stel maxmemory beleid waar nodig.

Real-World IMemoryCache patronen van productie

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.

Patroon 1: Eenvoudige cache-uitbraak voor zelden wisselende gegevens (Categorieën)

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:

  • Categorieën worden op elke pagina gelezen (getoond in navigatie)
  • Ze veranderen zelden (alleen als ik nieuwe blog berichten met nieuwe categorieën toe te voegen)
  • 30 minuten TTL is prima; als er nieuwe categorieën verschijnen, zien gebruikers ze binnen 30 minuten
  • Absolute vervaldatum alleen (geen glijden) omdat het ons niet kan schelen hoe vaak het wordt benaderd

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).

Patroon 2: Per-gebruiker staat met maatlimieten en glijdend verlopen (vertalingstaken)

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:

  • Schuiftijd: Als de gebruiker blijft controleren vertaling status, houd de cache in leven tot maximaal 6 uur
  • Groottelimieten: Houd 5 meest recente taken per gebruiker om geheugen opgeblazenheid te voorkomen
  • Handmatig beheer: Ik limiteer expliciet de grootte omdat MemoryCache geen ingangs-niveau aantal limieten oplegt (alleen de totale cachegrootte)

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.

Patroon 3: Cache met opmerkzaamheid (Metrics caching)

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:

  • Waarneembaarheid: SerilogTracing activiteit tracks cache hit / miss rate en records metrics tellen
  • Samengestelde sleutel: Bevat datumbereik en voorvoegsel om belangrijke botsingen te voorkomen
  • Datumopmaak: Gebruikt yyyyMMdd formaat in sleutel zo verschillende tijden op dezelfde dag delen van de cache
  • Null handling: Returns null als externe service mislukt; niet cache fouten
  • Gefilterde caching: Caches het gefilterde resultaat, niet de ruwe respons

Evolution: 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.

Vergelijking van de drie benaderingen

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:

  1. Start eenvoudig (patroon 1), voeg alleen complexiteit toe als dat nodig is
  2. Altijd denken over cache geheugengebruik - voeg grootte limieten voor per-user caches
  3. Voor dure operaties, toe te voegen observeerbaarheid vanaf dag één
  4. Aanpassen TTL op basis van de werkelijke gegevensveranderingsfrequentie, niet raden

Als ik IMemoryCache niet gebruik:

  • Voor authenticatiestatus van de gebruiker (claims in plaats daarvan in auth cookie)
  • Voor winkelwagen (zou gebruik maken van DB + gedistribueerde cache in productie)
  • Voor blog post inhoud (reeds in DB, eenmaal geladen per aanvraag)
  • Cross-server status (moet IDistributedCache/Redis)

Authenticatie cookies en claims

  • Toepassingsgebied: voor alle aanvragen tot het verstrijken/uitloggen
  • Gebruik voor: identiteit, grove rollen/machtigingen, een kleine hoeveelheid profielgegevens
  • Ruil-offs: cookiegrootte; niet overstuffen. Claims moeten stabiel zijn.

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) }));

JWT / Bearer Tokens

  • Toepassingsgebied: client draagt token; staatloze server
  • Gebruik voor: SPA's/mobiel/API's, cross-domein, microservices
  • Afspraken: tokengrootte; rotatie/versplinterd; minimale claims opslaan, gebruik introspectie indien nodig
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

Duurzame staat: Database en vrienden

  • Toepassingsgebied: voor altijd (totdat u het verwijdert)
  • Gebruik voor: alles wat niet verloren mag gaan: karren, bestellingen, profielen, langlopende workflows
  • Patronen: standaard CRUD met EF Core; CQRS; event sourcing; outbox patroon voor betrouwbaarheid

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);
});

Response Caching, ETags en Voorwaardelijke Verzoeken

  • Niet strikt de status dragen, maar vermindert herhaalde werkzaamheden door de client/proxies eerdere reacties te laten hergebruiken. Vaak gekoppeld met query/route status.
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");
});

ResponseCache vs OutputCache: Real-World Usage in Production

Ik gebruik zowel ResponseCache als OutputCache samen op mijn blog voor verschillende doeleinden. Hier is waarom je beide wilt en hoe ze verschillen.

De verwarring: Twee caching attributen?

ASP.NET Core heeft twee soortgelijke cachingsystemen:

  1. ResponseCache (HTTP-caching): HTTP-headers instellen (Cache-Control, Vary) vertellen browsers en CDN's hoe te cache e
  2. OutputCache (serverzijde): Laat de weergegeven uitvoer op de server overslaan om de actie volledig uit te voeren

Ze vullen elkaar aan. ResponseCache behandelt client/CDN-caching; OutputCache behandelt server-side-caching.

Mijn blog post actie (beide caches toegepast)

// 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:

  1. Uitvoercache controleert eerst: Heb ik een gecached antwoord voor my-post + en taal?

    • Hit: Return cached HTML, actie methode draait nooit (snel! ~1ms)
    • Juffrouw: Actie uitvoeren, weergave weergeven, cache resultaat gedurende 3600 seconden (1 uur)
  2. ResponseCache stelt headers in: Na OutputCache genereert de reactie, ResponseCache voegt toe:

    Cache-Control: public, max-age=300
    Vary: hx-request
    
  3. Browsercaching: Browser caches het antwoord voor 300 seconden (5 minuten). Latere verzoeken van dezelfde gebruiker niet eens op de server.

  4. CDN-caching (als je Cloudflare/Fastly gebruikt): CDN caches gedurende 5 minuten. Gebruikers wereldwijd drukten op de CDN, niet op mijn server.

Waarom verschillende duur? (300 vs 3600)

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

Reden:

  • Server cache is langer (1 uur): Ik bestuur mijn server; Ik kan cache verwijderen als ik een bericht update
  • Client cache is korter (5 minuten): Ik kan gebruikersbrowsers of CDN's niet gemakkelijk zuiveren; 5 minuten is een redelijk oudheid venster
  • Afhandelen: Als ik een bericht bewerk, verschijnt er nieuwe inhoud:
    • Serverzijde: Onmiddellijk (ik kan cache ongeldig maken)
    • CDN/browsers: Binnen 5 minuten (of ik verwijder handmatig CDN)

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:

  • Server serveert gelukkig gecached HTML voor 1 uur
  • Klanten krijgen elke 5 minuten vers genoeg inhoud

VaryByHeader voor HTMX

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:

  • Volledige aanvraag: Complete HTML pagina met layout
  • HTMX-verzoek: Gedeeltelijke weergave zonder layout

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

VaryByQueryKeys voor taal en paginatie

[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 item
  • page=2&pageSize=20&language=en → Nog een item
  • page=1&pageSize=10&language=en → Nog een item

Mitigatie:

  • Redelijke max cache grootte (OutputCache auto-evicts minst recent gebruikt)
  • Gemeenschappelijke parameters gecached (pagina 1, standaard paginagrootte)
  • Soms voorkomende combinaties kunnen cache missen (aanvaardbaar)

Instellen vereist

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

Wanneer OutputCache niet helpt

OutputCache is overgeslagen voor:

  • Aangemelde verzoeken (verschillende gebruikers zien verschillende gegevens)
  • POST/PUT/DELETE (alleen Get/HEAD cached)
  • Reacties met Set-Cookie kop
  • Antwoorden die expliciet worden ingesteld Cache-Control: no-store

Voorbeeld 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
}

Doeltreffendheid meten

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:

  • Zonder caching: ~50ms gemiddelde responstijd (DB query + Markdown rendering)
  • Met OutputCache: ~1-2ms voor gecachede antwoorden
  • 25x snelheid

Wanneer ik tijdelijk caching uitschakel

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.

Voor- en nadelen van deze dual-cache strategie

Voordelen:

  • Efficiënt gebruik van server: OutputCache reduceert CPU/DB belasting met 98%
  • Cliënt/CDN-voordelen: ResponseCache vermindert mijn bandbreedte en verbetert de wereldwijde latentie
  • Flexibiliteit: Verschillende TTLs voor server vs client
  • Kostenbesparing: Minder DB queries, minder bandbreedte

Nadelen:

  • Stabiliteit: Inhoud updates duren maximaal 5 minuten om te propageren aan klanten
  • Complexe cache-invalidatie: Het bijwerken van een bericht vereist ongeldig maken van zowel server- als CDN-caches
  • Geheugengebruik: OutputCache bevat weergegeven HTML in servergeheugen
  • Verwarring debuggen: Soms vergeet cache is aan en vraag je af waarom veranderingen niet verschijnen

Als ik caching oversloeg:

  • Real-time gegevens (voorraadprijzen, live sport)
  • Gepersonaliseerde inhoud (gebruikersspecifieke aanbevelingen)
  • Pagina's met weinig verkeer (caching overhead > uitkering)
  • Pagina's die zeer vaak veranderen

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.


ViewBag, ViewData en TempData: Controller-to-View State (en waarom ik meestal vermijden twee van hen)

Deze drie zijn vaak verward. Hier is hoe ze verschillen en wat ik eigenlijk gebruik in de productie.

De drie amigo's vergeleken

// 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

Wat ik eigenlijk gebruik: ViewBag voor globale layout data

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:

  • Algemeen: Elke pagina heeft categorieën nav en analytics nodig
  • Eenmaal berekend: BaseController loopt voor elke actie
  • HTMX-optimalisatie: Skip analytics script op gedeeltelijke verzoeken (HTMX hoeft het niet opnieuw te injecteren)
  • Gekacheld: Categorieën zijn gecached (zie mijn IMemoryCache patroon hierboven), dus niet raken DB elke aanvraag

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.

ViewBag voor paginaspecifieke gegevens (aanvaardbaar patroon)

// 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:

  • Eenvoudige scalaire waarden (tekenreeks, int)
  • Gebruikt alleen in het uitzicht, niet doorgegeven
  • Alternatief zou zijn toe te voegen Title en Category eigenschappen van elk weergavemodel

Wat ik vermijd: Complexe objecten in ViewBag

Antipatroon:

// 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:

  • Geen compilatie-tijd veiligheid (typo ViewBag.Usr faalt op runtime)
  • Moeilijk te volgen welke gegevens beschikbaar zijn
  • Maakt het testen moeilijker (nodig om ViewBag woordenboek te inspecteren)
  • Geen IntelliSense

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);
}

TempData: Ik gebruik het niet (en hier is waarom)

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:

  1. Ik gebruik HTMX in plaats van redirects: Mijn formulieren verzenden via HTMX en retourneren gedeeltelijke weergaven met inline succes / foutmeldingen. Geen redirect = geen noodzaak voor TempData.
[HttpPost]
public async Task<IActionResult> Submit(ContactViewModel model)
{
    if (!ModelState.IsValid)
        return PartialView("_ContactForm", model); // Show errors inline

    await sender.SendEmailAsync(contactModel);

    // Return success view directly (no redirect)
    return PartialView("_Response", new ContactViewModel
    {
        Email = model.Email,
        Name = model.Name,
        Comment = "Message sent!"
    });
}
  1. Voor traditionele PRG (Post-Redirect-Get), Ik zou TempData gebruiken. Maar ik verkies het vermijden van omleidingen wanneer mogelijk voor betere UX.

Wanneer TempData zinvol is:

  • Traditionele MVC-apps met volledige pagina redirects na POST
  • Multi-step tovenaars waar u doorverwijzen tussen stappen
  • Flash-berichten na authenticatie-omleidingen

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.

Snelle beslissingsboom

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:

  1. ViewBag alleen voor globale layout spullen (analyse, navigatie, broodkruimels)
  2. ViewBag voor eenvoudige paginatitels/rubrieken (facultatief; kan ViewModel gebruiken)
  3. Nooit ViewBag voor complexe objecten (gebruik ViewModels)
  4. Nooit bekijkenData (ViewBag heeft mooiere syntax)
  5. TempData alleen als je echt PRG nodig hebt (Ik niet, dankzij HTMX)

Patroon: Wizards en Multi-Step stroomt

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
  • Kleine, enkele sessie: Sessie of TempData tussen stappen.
  • Cross-device/long-running: blijf staan bij DB, draag een sleutel in de URL.

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");
});

Patroon: Flash berichten met TempData

  • Stel in POST; lees één keer na redirect.
TempData["Flash"] = "Profile saved";
return RedirectToAction("Index");

Scheermesweergave:

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

Patroon: Winkelwagen

  • Kleine karren: Sessie (als uw schaal is bescheiden en je hebt plakkerige / gedistribueerde sessie).
  • Grotere karren/multi-apparaat: DB + cart-id in cookie of URL. Cache voor snelheid.
flowchart LR
  U[User] -- cart-id cookie --> S[Server]
  S --> DB[(Cart Table)]
  S <--> Cache[Distributed Cache]

Beveiliging, privacy en naleving Checklist

  • Valideer alle door de klant verstrekte status: query, headers, formulieren, cookies, JWT claims.
  • Bescherm gevoelige door de klant opgeslagen staat: gebruik gegevensbescherming voor cookies die u afgeeft; bewaar nooit geheimen in query strings.
  • Cookievlaggen instellen: Secure, HttpOnly, SameSite, IsEssential (indien vereist door toestemming/functionele behoefte).
  • Authenticatiecookies regenereren bij wijzigingen in privileges; claims minimaal houden.
  • Versleutelen in rust voor server-side stores indien nodig; zorgen voor sleutelrotatie (Gegevensbeschermingssleutels, JWT ondertekeningssleutels).
  • GDPR/CCPA: bieden user data export/deletion paden; minimaliseren bewaren.

Decision Matrix (Cheat Sheet)

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:

  • Heb je een redirect flash nodig?
  • Wizard nodig voor meerdere verzoeken in één sessie? Sessie (of DB + sleutel als langlopend/multi-apparaat).
  • Behoefte aan schaalbaarheid en staatloze API's? JWT voor identiteit, DB/DistributedCache voor staat.
  • Moeten gegevens alleen binnen de pijpleiding? HttpContext.Items.
  • Heeft u cache nodig voor het opzoeken? IMemoryCache lokaal; IDistributedCache over een boerderij.

MVC vs Razor Pages vs Minimale API's: Zelfde Stichtingen, Verschillende Vormen

Alle drie de stapels zitten op dezelfde primitieven (HttpContext, modelbinding, auth, gegevensbescherming). Uit bovenstaande voorbeelden blijkt dat de API's meestal verschillen in ergonomie:

  • Minimale API's: parameterbinding van route/query/body/claims; terugkeer Results.*.
  • MVC: attributen, filters, modelbinding in actieparameters/viewmodellen.
  • Scheermes: paginaverwerkers met gebonden eigenschappen en tag helpers voor het genereren van links/vormen.

Ze delen allemaal dezelfde staatsmechanismen die hier besproken worden.


Pitfalls en anti-patterns

  • Het opslaan van grote of gevoelige gegevens in cookies of TempData.
  • Afhankelijk van in-geheugen cache voor juistheid (het is een cache, niet de waarheid).
  • Bouwvergunning op basis van client-sent route/query vlaggen zonder server controles.
  • Overvulling van auth cookies of JWT's met vluchtige claims.
  • Sessie zonder distributiestrategie (werken lokaal, pauzes op schaal).

Real-World State Management: Wat ik eigenlijk gebruik (en niet gebruiken)

Na het tonen van al deze opties, hier is mijn eerlijke beoordeling van wat werkt in productie voor mijn blog platform.

Mijn staat management stack (in volgorde van frequentie)

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

Gedetailleerde pro's/cons uit productie-ervaring

IMemoryCache

Waarvoor ik het gebruik:

  • Categorieënlijst (globaal, 30 min TTL)
  • Per-gebruiker vertaaltaak tracking (6h absolute + 1h glijden)
  • Externe API-responsen (Umami-metrics, 1h TTL)

Voordelen in de praktijk:

  • Vloeiend snel (in proces)
  • Geen serialisatie overhead
  • Verminderde DB/API belasting met ~95%
  • Makkelijk te implementeren en te begrijpen
  • Waarneming via SerilogTracing

Nadelen die ik heb geraakt:

  • Geheugen lekken als u niet beperken per-user caches
  • Opgeruimd bij herstart van de app (aanvaardbaar voor mijn use case)
  • Niet gedeeld over servers (fijn voor één voorbeeld)
  • Cache ongeldigheid is handmatig (moet verwijderen() expliciet)

Schaallimiet: Als ik bij meerdere servers kom, heb ik IDistributedCache (Redis) nodig voor gedeelde status. Voor nu is single server + memory cache perfect.

OutputCache + ResponseCache

Waarvoor ik het gebruik:

  • Blog post rendering (1 uur server, 5 min client/CDN)
  • Bloglijst/categorie pagina's
  • Agendagegevens

Voordelen in de praktijk:

  • 25x snelheid (50ms → 2ms)
  • Schaalt tot hoog verkeer zonder te zweten
  • Aparte TTLs voor server vs client
  • Werkt naadloos met HTMX (VaryByHeader)
  • Measureable impact via Prometheus metrics

Nadelen die ik heb geraakt:

  • Verwarring debuggen (vergeten cache staat aan)
  • Cache explosie met vele query parameter combinaties
  • Stabiliteitsvenster (5 min voor cliënten)
  • Behoefte om handmatig ongeldig te maken op inhoud updates

Beste voor: Lezen zware toepassingen met meestal statische inhoud. Niet goed voor gepersonaliseerde of real-time gegevens.

WeergaveBag

Waarvoor ik het gebruik:

  • Globale lay-outgegevens (analyses, categorieën)
  • Paginatitels

Voordelen in de praktijk:

  • Eenvoudig en snel voor layout-niveaugegevens
  • Eenmaal geplaatst in BaseController, overal beschikbaar
  • Werkt prima met gecachede categorieën lijst

Nadelen die ik heb geraakt:

  • Geen typeveiligheid (typos fail at runtime)
  • Verleiden tot overmatig gebruik van complexe gegevens
  • Moeilijk te testen

Regel Ik volg: ViewBag alleen voor eenvoudige scalars. Complexe objecten gaan in ViewModels.

Auth Claims . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Waarvoor ik het gebruik:

  • Gebruikers-ID, naam, e-mail, avatar-URL
  • Beheerdervlag (controleren) sub claim tegen config)

Voordelen in de praktijk:

  • Veilig (gesigneerde cookie, gegevensbescherming)
  • Automatisch met ASP.NET Core Identity/OAuth
  • Beschikbaar via User.Claims overal
  • Schuiftijd houdt gebruikers ingelogd

Nadelen die ik heb geraakt:

  • Maximale cookiegrootte (niet te veel claims)
  • Claims zijn statisch tot herinloggen
  • Als ik een claim toevoeg (zoals "IsEditor"), moet ik auth cookie opnieuw uitgeven

Beste praktijk: Houd claims minimaal en stabiel. Zet niet vaak wisselende gegevens in claims.

Dingen die ik niet gebruik (en waarom)

Sessie (nooit gebruikt):

  • Vereiste gedistribueerde opslag (Redis)
  • Voegt complexiteit toe voor een minimaal voordeel
  • Mijn use cases worden beter bediend door:
    • Authvorderingen (identiteit)
    • IMemoryCache (kortlevende toestand)
    • Database (duurzame toestand)

TempData (nooit gebruikt):

  • HTMX geëlimineerd Post-Redirect-Get patroon
  • Formulieren retourneren gedeeltelijke weergaven met inline berichten
  • Geen noodzaak om omleidingen te overleven

HttpContext.Items (nooit gebruikt):

  • Heb nog geen use case gehad voor per-request status
  • Mijn middleware berekent geen waarden voor controllers
  • Als ik huurderdetectie nodig had, zou ik het gebruiken.

IDistributedCache (nooit gebruikt):

  • Eén server-implementatie
  • IMemoryCache voldoet aan alle behoeften
  • Redis zou gebruiken als ik naar meerdere servers zou schalen

Ontwikkeling van mijn aanpak

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.

Advies voor uw app

Start hier:

  1. Route-/queryparams voor navigatie/filtering (altijd eerst stateloos)
  2. Auth-eisen voor identiteit
  3. Database voor alles wat moet blijven bestaan
  4. IMemoryCache voor read-heavy berekende gegevens
  5. OutputCache voor dure-tot-render pagina's

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:

  • Complexe objecten in ViewBag/TempData
  • Sessie zonder gedistribueerde backing store
  • Caching van gebruikersspecifieke of vaak veranderende gegevens
  • Voortijdige optimalisatie (maat eerst!)

Belangrijkste lessen:

  • Start eenvoudig, voeg complexiteit alleen toe wanneer metingen laten zien dat je het nodig hebt
  • Observabiliteit is cruciaal (ken je cache hit rates!)
  • Denk aan schaallimieten vroeg (per-gebruiker caches hebben grenzen nodig)
  • Test cache ongeldigheid grondig (verhaal is een echt probleem)
  • Documenteer uw TTL-keuzes (toekomstige vraag "waarom 30 minuten?")

Wrap-Up

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.


Bijlage: Meer kopieerbare voorbeelden (definities)

Deze voorbeelden verdiepen de eerdere secties met production-grade details die je kunt plakken in net9 minimale sjablonen, MVC, of Razor Pages.

Cookies: Bescherm waarden met gegevensbescherming

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.

Antiforese in MVC, Razor Pages, en Minimal API's

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

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

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

app.MapControllers();
app.MapRazorPages();
  • MVC: decoreer acties met [ValidateAntiForgeryToken] en gebruik @Html.AntiForgeryToken() in formulieren.
  • Scheermes: standaard ingeschakeld op formulierberichten; gebruik asp-antiforgery="true" indien nodig.
  • Minimaal: valideren via IAntiforgery zoals getoond.

Sessie met Redis en glijden vs absolute vervaldatum

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.

IMemoryCache met invoeropties en eviction callback

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
    }));
});

IDistributedCache get-or-set met jitter om stampen te voorkomen

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.

Lokaal afgeven en valideren van JWT's (demo)

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");

Voorwaardelijke updates met ETags (If-Match)

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

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

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

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

TempData: complexe objecten via JSON

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

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

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

EF Core concurrency token

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.


Deep Dive: HttpContext.Items (Praktische patronen en helpers)

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.

  • Levenscyclus: gemaakt op verzoek start; weggegooid wanneer de reactie voltooid is.
  • Reikwijdte: huidige aanvraag alleen .. nooit kruist omleidingen of achtergrondwerk.
  • Performance: O(1) lookups; ideaal voor per-request caching.
  • Veiligheid: alleen aan de serverkant; niet zichtbaar voor de client.

Waarom items in plaats van

  • Sessie/TempData: Deze vragen kruisen en distributieproblemen introduceren. Items zijn kortstondig en schaalvriendelijk.
  • DI Scoped services: Gebruik deze voor gedrag en gedeelde afhankelijkheden. Items is beter voor ad-hoc, berekende waarden (huurder, gebruiker locale, feature flags) en per-verzoek caches.
  • Kenmerken: Voor framework/transport-niveau functies (IEndpointFeature, IHttpUpgradeFeature). Items is voor app-niveau gegevens.

Vermijd belangrijke botsingen: sterk getypte toetsen

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;
    }
}

Patroon: Bereken in middleware, verbruik in eindpunten/controllers/pagina's

// 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) });

Patroon: Per aanvraag cache om herhaald werk te voorkomen

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:

  • Threading: Een enkel verzoek wordt meestal uitgevoerd op één logisch pad; Items is niet draadveilig voor parallelle schrijfsels. Als u parallelle taken start die items delen, voeg dan uw eigen synchronisatie toe.
  • Grootte: Houd waarden klein en goedkoop om te berekenen / serialize. Het is in-geheugen per aanvraag.

Patroon: Filters die items bevolken (MVC/Razor Pagina's)

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>());

Patroon: Verrijk logs zonder toewijzingen overal

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);
    }
});

Wanneer geen items gebruiken

  • Gegevens nodig na redirect of over verzoeken (gebruik TempData/Session/DB in plaats daarvan).
  • App-breed singletons of cross-request caches (gebruik IMemoryCache/IDistributedCache).
  • Waarden die thuishoren in identiteit/autorisatie (gebruik claims/beleiden).

Snelle regel: Als het wordt berekend tijdens dit verzoek en gelezen binnen dit verzoek door uw eigen code, Items is ideaal.

logo

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