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
Zustand zwischen den Anfragen in ASP.NET Core: Ein praktischer Leitfaden ohne Sinn (MVC, Razor Pages, Minimal APIs)
Sunday, 09 November 2025
UPDATE (2025-11-10): Weitere praktische Beispiele aus meiner eigentlichen Codebase, die reale Muster, Trade-Offs, Gotchas und die Evolution von einfachen zu anspruchsvollen staatlichen Management-Ansätzen zeigt. Enthält detaillierte IMemoryCache-Muster, ViewBag-Nutzung (gut und schlecht), ResponseCache/OutputCache-Strategien und Lektionen aus der Produktion gelernt.
Einleitung
HTTP ist berühmt stateless. Ihre App... ist nicht. Benutzer anmelden, fügen Sie Artikel zu Körben, Hop zwischen den Seiten, Rückkehr morgen, und erwarten, dass Sie sich erinnern. In ASP.NET Core (MVC, Razor Pages, Minimal APIs), gibt es eine Menge Möglichkeiten, um zu bewahren und Übertragung Zustand zwischen Anfragen. Einige sind nur pro Anfrage. Einige letzte für eine Sitzung. Einige leben im Client. Einige sind verteilt und überleben Server-Neustarts. Jede Wahl hat Kompromisse über Sicherheit, Leistung, Maßstab und Entwickler Ergonomie.
Dieser Beitrag katalogisiert die Optionen, zeigt konkrete, kopierfähige Beispiele in allen drei Stapeln und gibt Ihnen Entscheidungshilfe, damit Sie das richtige Werkzeug für den Job auswählen können.
HINWEIS: Dies ist Teil meiner Experimente mit KI (unterstütztes Abfassen) + meiner eigenen Bearbeitung. Dieselbe Stimme, gleicher Pragmatismus; nur schnellere Finger.
Die Landschaft auf einen Blick
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-carried: Abfrage, Route, Header, Formulare, Cookies, JWT. Skaliert horizontal, ist aber für den Client sichtbar (muss ggf. validiert/signiert/verschlüsselt werden).
- Server-carried: TempData, Session, Caches, DB. Erfordert Affinität oder Distributionsstrategie.
- Pro-Request: HttpContext.Items - nützlich für die interne Weitergabe von Daten während einer einzigen Anfrage.
Goldene Regeln (bevor wir in APIs tauchen)
- Sicherheit zuerst: Staatliche Leckage ist eine echte Bedrohung. Der serverseitige Zustand (Session, In-Memory-Caches) kann zwischen Benutzern durch Codierungsfehler, Race-Bedingungen oder falsches Lebenszyklusmanagement austreten. Staatenlose Muster sind nicht nur skalierbarer – sie sind sicherer.
- Bevorzugen Sie "stateless server"-Muster, wenn Sie horizontal skalieren müssen (Push-Status auf Client-Token, langlebige Speicher oder verteilte Caches).
- Vertrauen Sie niemals dem client-kontrollierten Zustand. Validieren, unterzeichnen und/oder verschlüsseln.
- Halten Sie große Daten aus Cookies und Headern; sie blasen jede Anfrage auf.
- Verwenden Sie TempData nur für Post-Redirect-Get (PRG)-Einschüsse wie Flash-Nachrichten.
- Verwenden Sie Sitzung nur, wenn Sie den serverseitigen Konversationszustand beibehalten müssen und Sie die Distribution geplant haben – und paranoid bezüglich der Sicherheit sein müssen.
- Ansprüche sind für Identität und grobkörnige Autorisierung, nicht allgemein App-Zustand.
- Cache ist keine Quelle der Wahrheit. Zurück mit einem dauerhaften Speicher, wenn die Daten wichtig sind.
Per-Request State: HttpContext.Items
- Umfang: nur aktuelle Anfrage (Teardown am Ende der Pipeline)
- Verwendung für: Weitergabe berechneter Werte zwischen Middleware und Endpunkten/Controllern
- Skala: keine Auswirkungen
- Sicherheit: Nur 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-Beispiel (alle Stapel):
app.Use(async (context, next) =>
{
var tenantId = context.Request.Headers["X-TenantId"].FirstOrDefault() ?? "public";
context.Items["TenantId"] = tenantId;
await next(context);
});
- Minimaler API-Endpunkt:
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"] });
Transienter client-carried Zustand: Route Werte und Abfrage String
- Umfang: aktuelle Anfrage; der Client trägt sie explizit.
- Verwendung für: Navigationskontext, Filterung, Paginierung, Ressourcenidentität
- Sicherheit: muss validieren/autorisieren; keine Geheimnisse einbetten
Beispiele
- Minimale APIs:
app.MapGet("/orders/{id:int}", (int id, int? page) => Results.Ok(new { id, page }));
// GET /orders/5?page=2
- MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC:
[HttpGet("/orders/{id:int}")]
public IActionResult Details(int id, int? page)
=> View(new { id, page });
- Razor-Seiten (Bestellungen/Details.cshtml.cs):
public IActionResult OnGet(int id, int? page)
=> Page();
Verknüpfungen erzeugen, die den Zustand erhalten:
// 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)
Kopfzeilen: Korrelations-IDs, Idempotenz-Schlüssel, Feature-Flaggen
- Umfang: aktuelle Anfrage, optional in Antworten wiedergegeben
- Verwendung für: Rückverfolgung, Wiederholungssicherheit, A/B-Flaggen
- Sicherheit: als nicht vertrauenswürdige Eingabe behandeln, validieren/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);
});
Idempotenz (Muster):
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
Formulare und versteckte Felder (PRG)
- Umfang: nur nächste Anfrage (Kundenposts zurück zu den Werten)
- Verwenden Sie für: Assistent Schritte, Anti-Fälschungs-Token, halten kleine Teile des Zustands über PRG
- Sicherheit: immer validieren; mit Antifälschung kombinieren
PRG-Muster in MVC/Razor-Seiten:
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: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC: MVC:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Save(SettingsModel model)
{
// validate & persist
return RedirectToAction(nameof(Summary), new { tab = model.SelectedTab });
}
- Razor Seiten:
public IActionResult OnPost(SettingsModel model)
{
return RedirectToPage("/Settings/Summary", new { tab = model.SelectedTab });
}
Cookies: Klein, signiert, manchmal verschlüsselt
- Umfang: jede Anfrage aus dem Browser bis zum Ablauf
- Verwendung für: Präferenzen, unempfindliche Flaggen, Zustimmung; Auth-Cookies (separater Abschnitt)
- Trade-offs: Größenbeschränkungen (~4KB pro Cookie), Performance-Wirkung, muss die Zustimmungsgesetze einhalten
Minimales API-Beispiel:
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 Pages Nutzung ist identisch über HttpContext.
Für Integrität/Vertraulichkeit verwenden Sie das ASP.NET Core Data Protection System, um Nutzlasten zu schützen, die Sie selbst in Cookies setzen.
TempData: One-Redirect Message Bus
- Umfang: überlebt eine Umleitung
- Backing Store: Cookie (Standard) oder Session
- Verwendung für: Flash-Nachrichten, Validierungszusammenfassungen nach PRG
Einrichtung (Programm.cs):
builder.Services.AddControllersWithViews().AddSessionStateTempDataProvider(); // optional
builder.Services.AddSession();
var app = builder.Build();
app.UseSession();
In MVC-Steuerung:
TempData["StatusMessage"] = "Saved!";
return RedirectToAction("Index");
In Razor Page Handler:
TempData["StatusMessage"] = "Saved!";
return RedirectToPage("/Index");
In Ansicht/Seite:
@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]
Sitzung: Server-Side Conversational State
- Umfang: Browser-Sitzung (Cookie-Schlüssel + Server-Speicher)
- Verwendung für: Mehrschritt-Assistenten, kleine Wagendaten, drosselnde Zähler
- Trade-offs: erfordert klebrige Sitzungen oder verteilte Backing Store; kann Skalierung begrenzen
Einrichten:
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();
Verwendung von Sitzung (jeder Stack):
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);
});
Sitzungshelfer:
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;
}
Eine warnende Geschichte: Wenn der Sitzungszustand zum Engpass wird
Ich arbeitete einmal an einem massiven britischen Regierung IT-Projekt, wo Session-State-Missbrauch (unter vielen anderen architektonischen Sünden) wurde ein Performance-killing Engpass. Das Team hatte gefüllt alles in die Sitzung: Benutzereinstellungen, mehrstufige Formulardaten, Suchergebnisse, temporäre Berechnungen, sogar zwischengespeicherte Lookups, die in einem korrekten Cache oder einer Datenbank hätten sein sollen.
Das Problem: Der Sitzungszustand wurde im Prozess gespeichert (ASP.NET-Sitzungszustand in web.config, das waren vor-Core-Tage). Jede Anfrage musste massive Sitzungsobjekte deserialisieren. Als die Last zunahm, balltonierte der Sitzungszustand auf zehn Megabyte pro Benutzer. Mit Tausenden von gleichzeitigen Benutzern liefen die Server aus dem Speicher heraus.
Die verzweifelte Lösung: Wir flogen zur HP-Anlage in Stuttgart, um Lasttests auf ihrem Superdome durchzuführen – damals, Europas leistungsfähigste Windows-Maschine. Es war eine Bestie: Dutzende von Itanium-Prozessoren, Hunderte von Gigabyte RAM. Die Idee war, zu beweisen, dass mit genügend Hardware, konnte das System Anforderungen erfüllen.
Das Ergebnis: Selbst auf dem Superdome konnten wir die erforderlichen gleichzeitigen Benutzerziele nicht treffen. Die Session-State-Architektur war grundsätzlich kaputt. Vertikale Skalierung konnte kein schlechtes Design speichern. Der Sitzungs-Serialisierungs-/Deserialisierungsüberkopf, kombiniert mit Speicherdruck von massiven Sitzungsobjekten, bedeutete, dass das System einfach nicht skalieren konnte – nicht um jeden vernünftigen Preis.
Der Sicherheits-Albtraum: Schlimmer als die Performance-Probleme, entdeckten wir einen Codierungsfehler, der den Sitzungszustand verursachte. Leck zwischen Anwendern. Die Sitzungsdaten von User A würden gelegentlich in der Sitzung von User B erscheinen. Dies war nicht nur peinlich – es war katastrophal. Personal des NHS Zugang zu Patientenakten und klinischen Systemen. Wir hatten versehentlich einen Datenschutzverletzungsmechanismus geschaffen, der sensible medizinische Informationen über verschiedene Gesundheitsberufe hinweg aufdecken könnte.
Was hätte geschehen sollen:
- Staatenlos per Default: Die meisten Sitzungsdaten hätten nie existieren dürfen
- Datenbank für dauerhaften Zustand: Mehrstufiger Formularfortschritt hätte in der Datenbank mit einer Workflow-ID sein sollen
- Cache für Lookups: Gemeinsame Lookups gehörten zu IMemoryCache oder einem verteilten Cache
- Kundenseite für Präferenzen: Benutzereinstellungen könnten in Cookies oder lokale Speicherung gewesen sein
- Verteilte Sitzung bei Bedarf: Wenn die Sitzung wirklich erforderlich war, hätte die Redis-backed-Sitzung die Last geteilt.
Die Lektion: Der Sitzungszustand skaliert nicht vertikal und skaliert kaum horizontal (auch bei klebrigen Sessions oder verteilten Stores, Sie serialisiert/deserialisieren immer noch auf jede Anfrage).Das Superdome-Experiment hat bewiesen, dass das Werfen von Hardware bei architektonischen Problemen teuer und oft zwecklos ist.
Aber was noch wichtiger ist: session state bugs werden zu Sicherheitslücken. Thread-Sicherheit Probleme, Race-Bedingungen, falsche Sitzung ID-Handling – diese nicht nur verursachen Performance-Probleme, sie können sensible Daten zwischen Benutzern undicht. In einem Gesundheits-Kontext (oder Banken, oder jede regulierte Industrie), dies ist eine Compliance Alptraum und potenzielle strafrechtliche Haftung.
Moderne Beratung:
- Wenn Sie mehr als ein paar KB Sitzungsdaten benötigen, haben Sie wahrscheinlich ein Designproblem.
- Wenn Sie sensible Daten in Sitzung speichern, erstellen Sie eine Sicherheits-Angriffsoberfläche
- Staatenlose Architekturen skalieren nicht nur besser – sie sind inhärent sicherer, weil es keinen serverseitigen Zustand gibt, der zu undicht ist
- Überdenken Sie Ihre staatliche Managementstrategie, bevor Sie nach Stuttgart fliegen müssen (oder erklären Sie dem Informationsbeauftragten eine Verletzung der Daten)
Caching: IMemoryCache und IDistributedCache
- Umfang: Server-Prozess (IMemoryCache) oder verteilt (IDistributedCache)
- Verwendung für: abgeleitete/verrechnete Daten, Lookups, kurzlebiger Zustand
- Trade-offs: Cache-Ungültigkeit, Serialisierung für Distributed
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 (z.B. 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-Muster Warnung: Wenn es dauerhaft oder maßgeblich sein muss, speichern Sie es in einer Datenbank und optional zwischenspeichern.
Auswahl zwischen IMemoryCache und IDistributedCache
- ImemoryCache:
- Blähungen schnell, in-Prozess, Objekte bleiben als Objekte (keine Serialisierung).
- Räumung durch Speicherdruck, Größenbegrenzung, absolute/sliding Ablauf und Priorität.
- Nicht über Knoten geteilt; beim App-Recycling/Deploy gelöscht.
- Ideal für Pro-Node Hotsets, berechnete Lookups, kurze TTLs.
- IDistributedCache (Redis/SQL/etc.):
- Über eine Farm geteilt; überlebt App-Neustarts; erfordert Serialisierung (Strings/Byte).
- Etwas höhere Latenz; der Durchsatz hängt von Netzwerk und Backend ab.
- Unterstützt absolute/sliding Ablauf (Provider-abhängig; Redis Anbieter aktualisiert TTL auf Zugriff für Schieben).
- Ideal für Cross-Node-Konsistenz, große Fan-Out-Lesungen, Feature-Flags und Sitzung.
Gemeinsame Cache-Strategien
Cache-Asid (am häufigsten):
- Versuchen Sie cache; 2) wenn miss, laden aus der Quelle; 3) in cache schreiben; 4) zurückgeben.
- Positiv:: einfach; Quelle der Wahrheit bleibt die Datenbank.
- Negativ:: erste Anfrage nach Ablauf ist langsam; mögliche Stempel.
Read-through (über eine Bibliothek/Provider): cache handles loading on misses.
Durchschreiben: Schreibt gehen Sie synchron in den Cache und sichern den Store.
Write-behind: Schreiben Sie in den Cache, bündig, um asynchron zu speichern (Risiko: Verlust/Unstimmigkeit).
Refresh-Ahead: Erfrischen Sie Hot Keys, bevor sie ablaufen, um kalte Misses zu vermeiden.
Expiration, Räumung und Dimensionierung
- Absoluter Ablauf: erlischt immer nach einer festen Dauer (gut für externe Datenfrische).
- Sliding Ablauf: erweitert TTL beim Zugriff (gut für Sessions/userspezifische Daten).
- Größe-basierte Räumung (IMemoryCache): Eintrag festlegen.Größe und konfigurieren Sie SizeLimit auf gebundenen Speicher.
- Priorität (IMemoryCache): CacheItemPriority.High/Normal/Low/NeverRemove wirkt sich auf die Räumung unter Druck aus.
- Jitter: Fügen Sie TTLs kleine zufällige Offsets hinzu, um synchronisiertes Ablaufen zu vermeiden (Stampedes).
Verhindern von Cache-Stampedes (Dunder-Herde)
- Verwenden Sie GetOrCreate/GetOrCreateAsync (IMemoryCache), um die Ein-Thread-Population pro Knoten zu gewährleisten.
- Verteilt: Verwenden Sie einen kurzlebigen Schlossschlüssel (SET NX EX) oder Bibliotheksunterstützung; fügen Sie TTL Jitter hinzu; betrachten Sie Hintergrundaktualisierung.
- Serve stale-wile-revalidate: Halten Sie einen sekundären Schlüssel mit stale-Wert und kurze Verlängerung, während neuer Wert berechnet wird.
Schlüsseldesign und Namenspaking
- Bevorzugen Sie Kleinschreibung, Doppelpunkt-begrenzte Schlüssel: App:entity:123 oder Mieter:us:ususers:42.
- Fügen Sie Versionssegment, um ganze Klassen von Schlüsseln ohne Löschen ungültig: v2:products:123.
- Mieter-aware: Präfixschlüssel mit Mieter oder Organisation id, um Kollisionen zu vermeiden und Reinigungen zu erleichtern.
- Halten Sie Schlüssel klein, aber beschreibend; vermeiden Sie benutzergesteuerte Roheingaben ohne Normalisierung.
Auslaufen von Schlüsselsätzen (Tags/Gruppen)
Wenn Sie viele dieser Einträge für ungültig erklären müssen:
Versionierte Präfixe (soft invalidation): Beugen Sie eine globale Version in einem kleinen Schlüssel und komponieren Sie Schlüssel mit ihm.
// version key: "v:products"; keys like $"{version}:product:{id}" var version = await cache.GetStringAsync("v:products") ?? "1"; var key = $"{version}:product:{id}";Um alle Produkte außer Kraft zu setzen: erhöhen Sie v:products (Kunden verpassen natürlich alte präfixierte Schlüssel).
Tag set pro Gruppe (Redis): Halten Sie einen Satz von Schlüsseln pro Tag; bei Ungültigkeit, holen Sie Mitglieder und löschen.
// 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-Ungültigkeit: Veröffentlichung einer "invalidate:key"-Nachricht; jeder Knoten entfernt den Schlüssel aus seinem lokalen IMemoryCache.
Scannen mit Mustern: SCAN/KEYS sollte in prod heißen Pfaden vermieden werden; okay für Admin-Tooling auf kleinen Keyspaces.
Praktische Helfer
- IMemoryCache get-or-set mit Optionen:
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 mit JSON und Ablauf:
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; }
Überwachung und Sichtbarkeit
- Track Hit/Miss Raten und durchschnittliche Lastzeit; Präsentieren Sie Metriken (Prometheus Zähler) pro Schlüsselgruppe.
- Protokollierung um Cache-Population und Räumungsaufrufe für IMemoryCache hinzufügen.
- Sehen Sie sich für Redis Keyspace-Hits/Missans, Latenz und Speicherfragmentierung an; legen Sie gegebenenfalls Maxmemory-Richtlinien fest.
Real-World IMemoryCache Muster aus der Produktion
Hier ist, wie ich tatsächlich IMemoryCache in meinem Blog-Plattform, entwickelt durch Test-und Fehler. Ich zeige Ihnen drei echte Muster von einfach bis anspruchsvoll.
Muster 1: Einfache Cache-Aside für selten wechselnde Daten (Kategorien)
Dies war meine erste Caching-Implementierung. Blog-Kategorien ändern sich nicht oft, so Cache sie für 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;
}
Warum das funktioniert:
- Kategorien werden auf jeder Seite gelesen (in der Navigation dargestellt)
- Sie ändern sich selten (nur wenn ich neue Blog-Beiträge mit neuen Kategorien hinzufüge)
- 30 Minuten TTL ist in Ordnung; wenn neue Kategorien erscheinen, sehen Benutzer sie innerhalb von 30 Minuten
- Absolute Ablauf nur (kein Schieben), weil es uns egal ist, wie oft es zugegriffen wird
Ich hab dich getroffen: Zunächst habe ich eine String-Taste "Kategorien" verwendet. Funktioniert gut, bis Sie mehrere Controller haben und eine versehentlich den gleichen Schlüssel wieder verwendet. Jetzt verwende ich Konstanten oder stark typisierte Tasten (siehe den vorherigen Abschnitt zur Vermeidung von Kollisionen).
Muster 2: Pro-User-Zustand mit Größenbegrenzungen und gleitendem Ablauf (Übersetzungsaufgaben)
Dieses Caches Übersetzung Status pro Benutzer. Es ist komplexer, weil es Grenzen benötigt und sollte am Leben bleiben, solange der Benutzer aktiv ist:
// 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)
});
}
}
Warum das anders ist:
- Ablauf des Gleitvorgangs: Wenn der Benutzer den Übersetzungsstatus überprüft, halten Sie den Cache bis zu 6 Stunden am Leben
- Größenbegrenzungen: Bewahren Sie nur 5 aktuelle Aufgaben pro Benutzer auf, um zu verhindern, dass der Speicher aufbläht
- Manuelle Verwaltung: Ich beschränke die Größe ausdrücklich, weil MemoryCache keine Eintrags-Level-Element-Count-Limits erzwinge (nur Gesamtcache-Größe)
Fehler, die ich gemacht habe: Zunächst habe ich die Anzahl der Aufgaben nicht begrenzt. Ein Power-User ausgelöst 50+ Übersetzungen und ich hatte ein Speicherleck. Jetzt halte ich max. 5 pro Benutzer.
Abtretung: Dies wird nicht auf Millionen von Benutzern skalieren. Wenn dies ein Problem wird, würde ich zu IDistributedCache (Redis) verschieben oder in der Datenbank mit einem Index auf userId + startTime speichern.
Muster 3: Cache mit Beobachtbarkeit (Metrics caching)
Dieses Caches Analyse Metriken und verfolgt Cache Wirksamkeit mit Serilog Tracing:
// 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;
}
}
Was macht diese Produktion bereit:
- Beobachtungsfähigkeit: SerilogTracing activity tracks cache hit/miss rate and records metrics count
- Zusammengesetzter Schlüssel: Enthält Datumsbereich und Präfix zur Vermeidung von Schlüsselkollisionen
- Datumsformatierung: Verwendung
yyyyMMddFormat in Schlüssel so unterschiedliche Zeiten am selben Tag teilen Sie den Cache - Null-Handhabung: Gibt null zurück, wenn ein externer Dienst fehlschlägt; nicht Cache-Ausfälle
- Gefiltertes Caching: Caches das gefilterte Ergebnis, nicht die rohe Reaktion
Entwicklung: Zuerst habe ich 10 Minuten zwischengespeichert. Aber Umami Metriken sind schwer zu holen und nicht viel ändern, so 1 Stunde ist in Ordnung. Ich entdeckte dies, indem ich die SerilogTracing Daten und sehen übermäßige Cache verpasst.
Überwachung in Aktion: In Seq (mein Log Aggregator) kann ich nachfragen:
ActivityName = "GetMetricsWithPrefix" and CacheHit = false
Dies sagt mir meine Cache Fehlrate. Wenn es hoch ist, justiere ich TTL oder Schlüsselstrategie.
Vergleich der drei Ansätze
Muster-Verwendung Fall-Verlängerung Größen-Kontrolle Beobachtbarkeit |---------|----------|------------|--------------|---------------| | Kategorien Global, selten wechselnd 30 min absolut (klein) nicht benötigt | Übersetzungsaufgaben Pro-User, begrenzt, 6h absolut + 1h gleitend, Handbuch (5 Artikel max) .Keine (sollte hinzufügen!) . | Metrik Teuere externe Anrufe 1h absolut Natürlich (zeitbelichtet) Volle Rückverfolgung
Schlüssellektionen:
- Einfach starten (Muster 1), Komplexität nur bei Bedarf hinzufügen
- Denken Sie immer über Cache-Speicher-Nutzung - Größe Grenzen für pro-User-Caches hinzufügen
- Für teure Operationen, zusätzliche Beobachtbarkeit von Tag eins
- TTL auf Basis der tatsächlichen Datenänderungshäufigkeit anpassen, keine Vermutungen
Wenn ich IMemoryCache nicht benutze:
- Für Benutzer-Authentifizierungszustand (Anrufe im Auth-Cookie statt)
- Für den Warenkorb (würde DB + verteilten Cache in der Produktion verwenden)
- Für Blog-Post-Inhalte (bereits in DB, einmal pro Anfrage geladen)
- Cross-Server-Zustand (benötigt IDistributedCache/Redis)
Authentifizierung Cookies und Ansprüche
- Anwendungsbereich: über Anträge bis zum Ablauf/Abmeldung
- Verwendung für: Identität, grobe Rollen/Zulassungen, eine kleine Anzahl von Profildaten
- Trade-offs: Cookie-Größe; nicht zu viel. Ansprüche sollten stabil sein.
Einrichtungs-Cookie-Auth:
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();
Anmeldung mit Forderungen (MVC/minimal):
app.MapPost("/login", async (HttpContext ctx) =>
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, "123"),
new Claim(ClaimTypes.Name, "Alice"),
new Claim(ClaimTypes.Role, "Admin")
};
var identity = new ClaimsIdentity(claims, "Cookies");
await ctx.SignInAsync("Cookies", new ClaimsPrincipal(identity));
return Results.Redirect("/");
});
Lesen Sie Forderungen (jeder 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 / Trägermarken
- Umfang: Client trägt Token; Stateless Server
- Verwendung für: SPAs/Mobile/APIs, Cross-Domain, Microservices
- Trade-offs: Token-Größe; Rotation/Refresh; Mindestforderungen speichern, Introspektion bei Bedarf verwenden
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", o =>
{
o.Authority = "https://demo.identityserver.io"; // example
o.Audience = "api";
o.RequireHttpsMetadata = true;
});
Verwendung:
[Authorize(AuthenticationSchemes = "Bearer")]
app.MapGet("/secure", () => "ok");
Meerjungfrauen-Übersicht:
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
Langlebiger Zustand: Datenbank und Freunde
- Umfang: für immer (bis Sie es löschen)
- Verwendung für: alles, was nicht verloren gehen darf: Karren, Bestellungen, Profile, langlaufende Workflows
- Muster: Standard CRUD mit EF Core; CQRS; Event Sourcing; Outbox-Muster für Zuverlässigkeit
EF-Kernskizze:
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);
});
Antwort-Caching, ETags und bedingte Anfragen
- Nicht unbedingt -state tragen, , sondern reduziert wiederholte Arbeit, indem der Client / Proxies Wiederverwendung von früheren Antworten. Oft gepaart mit Abfrage / Routenzustand.
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();
Beispiel ETag:
app.MapGet("/resource", (HttpContext ctx) =>
{
var version = "W/\"abc123\""; // compute based on data hash
ctx.Response.Headers.ETag = version;
if (ctx.Request.Headers.IfNoneMatch == version)
return Results.StatusCode(StatusCodes.Status304NotModified);
return Results.Text("payload");
});
ResponseCache vs OutputCache: Real-World-Nutzung in der Produktion
Ich benutze beide ResponseCache und OutputCache zusammen auf meinem Blog für verschiedene Zwecke. Hier ist, warum Sie beide wollen und wie sie unterscheiden.
Die Verwirrung: Zwei Caching-Attribute?
ASP.NET Core hat Zwei ähnlich aussehende Caching-Systeme:
- AnsprechenCache (HTTP-Caching): Setzt HTTP-Header (
Cache-Control,Vary) sagt Browsern und CDNs, wie man zwischenspeichert e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e s e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n e n - AusgabeCache (serverseitig): Schaltet die gerenderte Ausgabe auf dem Server ein, um die Ausführung der Aktion vollständig zu überspringen
Sie ergänzen sich gegenseitig. ResponseCache behandelt Client/CDN-Caching; OutputCache behandelt serverseitiges Caching.
Meine Blog-Post-Aktion (beide Caches angewendet)
// 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);
}
Was passiert, wenn jemand fragt /blog/my-post:
OutputCache prüft zuerst: Habe ich eine zwischengespeicherte Antwort für
my-post+enSprache?- Treffer: Zurückgespeichertes HTML, Aktionsmethode läuft nie (schnell! ~1ms)
- Fräulein: Aktion ausführen, Ansicht rendern, Ergebnis cache für 3600 Sekunden (1 Stunde)
ResponseCache setzt Header: Nach OutputCache erzeugt die Antwort, ResponseCache fügt hinzu:
Cache-Control: public, max-age=300 Vary: hx-requestBrowser-Caching: Browser speichert die Antwort für 300 Sekunden (5 Minuten). Nachfolgende Anfragen desselben Benutzers treffen nicht einmal auf den Server.
CDN-Caching (bei Verwendung von Cloudflare/Fastly): CDN-Caches für 5 Minuten. Benutzer weltweit treffen das CDN, nicht meinen Server.
Warum unterschiedliche Dauern? (300 vs. 3600)
[ResponseCache(Duration = 300)] // 5 minutes client/CDN cache
[OutputCache(Duration = 3600)] // 1 hour server cache
Begründung:
- Server-Cache ist länger (1 Stunde): Ich kontrolliere meinen Server; ich kann Cache reinigen, wenn ich einen Post aktualisiere
- Client-Cache ist kürzer (5 Minuten): Ich kann Benutzerbrowser oder CDNs nicht einfach reinigen; 5 Minuten ist ein vernünftiges Stillstandsfenster
- Handelshemmnisse: Wenn ich einen Beitrag editiere, erscheint ein neuer Inhalt:
- Server-Seite: Sofort (Ich kann den Cache ungültig machen)
- CDN/Browser: Innerhalb von 5 Minuten (oder ich manuell reinigen CDN)
Entwicklung: Anfangs hatte ich beides bei 5 Minuten. Aber das bedeutete, dass mein Server alle 5 Minuten re-renderte, obwohl sich der Inhalt selten ändert.
- Server serviert glücklich gecached HTML für 1 Stunde
- Kunden erhalten alle 5 Minuten frische Inhalte
VaryByHeader für HTMX
VaryByHeader = "hx-request" // ResponseCache
VaryByHeaderNames = new[] { "hx-request" } // OutputCache
Warum das so wichtig ist: HTMX-Anfragen umfassen hx-request: true header. Ich gebe verschiedene Antworten zurück:
- Vollständiger Antrag: Vollständige HTML-Seite mit Layout
- HTMX-Anforderung: Teilansicht ohne Layout
Ohne VaryBy, würde der Cache das falsche Format zurückgeben. VaryBy, Ich cache zwei Versionen jeder Seite.
Beispiel:
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 für Sprache und Pagination
[ResponseCache(VaryByQueryKeys = new[] { nameof(slug), nameof(language) })]
[OutputCache(VaryByQueryKeys = new[] { nameof(slug), nameof(language) })]
Problem ohne diese: /blog/my-post?language=fr würde die englische Cache-Version dienen.
Mit VaryByQueryKeys: Getrennte Cache-Einträge:
/blog/my-post?language=en→ Cache-Taste enthält "en"/blog/my-post?language=fr→ Cache-Taste enthält "fr"
Echte Nutzung aus meiner Blog-Liste:
[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, /* ... */)
Explosionswarnung des Caches: Jede einzigartige Kombination von Parametern = separater Cache-Eintrag:
page=1&pageSize=20&language=en→ Ein Eintragpage=2&pageSize=20&language=en→ Ein weiterer Eintragpage=1&pageSize=10&language=en→ Ein weiterer Eintrag
Milderung:
- Angemessene max-Cache-Größe (OutputCache-Auto-Evicts zuletzt genutzt)
- Häufige Parameter zwischengespeichert (Seite 1, Standard pageSize)
- Gelegentliche Kombinationen können Cache vermissen (annehmbar)
Einrichtung erforderlich
AntwortCache arbeitet aus der Box heraus, aber für AusgabeCache Sie benötigen eine Einrichtung:
// 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
Wenn OutputCache nicht hilft
OutputCache wird übersprungen für:
- Authentifizierte Anfragen (verschiedene Benutzer sehen unterschiedliche Daten)
- POST/PUT/DELETE (nur GET/HEAD zwischengespeichert)
- Antworten mit
Set-CookieKopfzeile - Antworten, die explizit gesetzt werden
Cache-Control: no-store
Beispiel, wo ich es nicht benutze:
// 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
}
Messung der Wirksamkeit
Ich verwende Prometheus-Metriken (ausgestellt über meine App), um zu verfolgen:
// Pseudo-code for metrics
cache_hits_total{cache="output"} 45230
cache_misses_total{cache="output"} 892
Meine Cache Trefferrate: ~98% für Blog-Posts (die meisten Traffic trifft immer wieder die gleichen populären Posts).
Auswirkungen:
- Ohne Caching: ~50ms durchschnittliche Reaktionszeit (DB-Abfrage + Markdown-Rendering)
- Mit OutputCache: ~1-2ms für zwischengespeicherte Antworten
- 25x Beschleunigung
Wenn ich Caching vorübergehend deaktiviere
Manchmal debugge ich und brauche jedes Mal neue Antworten:
// During development, comment out caching
// [ResponseCache(Duration = 300, ...)]
// [OutputCache(Duration = 3600, ...)]
public async Task<IActionResult> Show(string slug, string language = "en")
Oder verwenden Sie umgebungsspezifische Einstellungen:
#if DEBUG
// No caching in development
#else
[ResponseCache(Duration = 300, ...)]
[OutputCache(Duration = 3600, ...)]
#endif
Besserer Ansatz: Konfiguration verwenden:
[ResponseCache(Duration = responseCacheDuration, ...)]
Dabei ist responseCacheDuration ist 0 in der Entwicklung, 300 in der Produktion.
Vor- und Nachteile dieser Dual-Cache-Strategie
Vors.:
- Server-Effizienz: OutputCache reduziert CPU/DB-Last um 98%
- Kunden-/CDN-Vorteile: ResponseCache reduziert meine Bandbreite und verbessert die globale Latenz
- Flexibilität: Verschiedene TTLs für Server vs Client
- Kosteneinsparungen: Weniger DB-Abfragen, weniger Bandbreite
Nachteile:
- Staleness: Inhaltsaktualisierungen dauern bis zu 5 Minuten, um sich an Kunden zu vermehren
- Ungültigkeits-Komplexität des Caches: Das Aktualisieren eines Posts erfordert die Ungültigkeit von Server- und CDN-Caches
- Speichernutzung: OutputCache hält gerenderten HTML im Serverspeicher
- Verwirrung stiften: Manchmal vergessen Cache ist an und fragen sich, warum Änderungen nicht erscheinen
Wenn ich Caching überspringen würde:
- Echtzeit-Daten (Lagerpreise, Live-Sportarten)
- Personalisierte Inhalte (userspezifische Empfehlungen)
- Low-traffic Seiten (Caching Overhead > Nutzen)
- Seiten, die sich sehr häufig ändern
Mein Urteil: Für einen Blog mit meist statischen Inhalten und hohem Lese-/Schreibverhältnis ist Dual Caching ein großer Gewinn. Ich würde dies nicht auf Admin-Panels oder Dashboards mit schnell wechselnden Daten verwenden.
ViewBag, ViewData und TempData: Controller-to-View State (und warum ich meistens zwei von ihnen vermeide)
Diese drei sind oft verwirrt. Hier ist, wie sie sich unterscheiden und was ich tatsächlich in der Produktion benutze.
Die drei Amigos verglichen
// 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");
Eigenschaften AnsichtDaten AnsichtTasche TempData
|---------|----------|---------|----------|
| Art | ViewDataDictionary Dynamisch ITempDataDictionary |
| Lebenslänglich Aktuelle Anfrage Aktuelle Anfrage Eine Weiterleitung
| Schlüsselzugriff String-Tasten Eigenschaften Syntax String-Tasten
| Typsicherheit Keine (Dynamik) Keine (Dynamik) Keine (Dynamik) Keine (Dynamik)
| Kompile-Zeit-Kontrolle Nein Nein
| Umleitung von Survives Nein Nein Nein
Was ich tatsächlich benutze: ViewBag für globale Layoutdaten
In meinem Blog verwende ich ViewBag ausschließlich für die Weitergabe von Daten von Controllern an Shared Layout (Analytik, Kategorien, etc.):
// 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);
}
Dann in meinem Layout (_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>
}
Warum dieses Muster funktioniert:
- Global: Jede Seite benötigt Kategorien nav und analytics
- einmal berechnet: BaseController läuft vor jeder Aktion
- HTMX-Optimierung: Analytics-Skript auf Teilanfragen überspringen (HTMX braucht es nicht erneut injiziert)
- Gespeichert: Kategorien werden zwischengespeichert (siehe mein IMemoryCache-Muster oben), also nicht auf DB jede Anfrage
Fehler, die ich früher gemacht habe: Ich habe mich versenkt. ViewBag.Categories in jeder einzelnen Aktionsmethode. DRY Verletzung und leicht zu vergessen. OnActionExecutionAsync in der Basissteuerung löste dies.
ViewBag für seitenspezifische Daten (annehmbares Muster)
// 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);
}
Im Blickpunkt:
@{
ViewData["Title"] = ViewBag.Title; // Standard MVC convention for <title>
}
<h1>Category: @ViewBag.Category</h1>
Das ist okay, weil:
- Einfache Skalarwerte (String, Int)
- Nur in der Ansicht verwendet, nicht herumgereicht
- Eine Alternative wäre die Hinzufügung von
TitleundCategoryEigenschaften zu jedem Ansichtsmodell
Was ich vermeide: Komplexe Objekte in ViewBag
Anti-Muster:
// 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 };
Probleme:
- Keine Kompilierungszeitsicherheit (Typo)
ViewBag.UsrFehler bei der Laufzeit) - Schwer nachzuvollziehen, welche Daten in der Ansicht verfügbar sind
- Macht das Testen schwieriger (müssen ViewBag-Wörterbuch inspizieren)
- Kein Intellisense
Besser: Stark typisierte Ansichtsmodelle:
// 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: Ich verwende es nicht (und hier ist warum)
Standard-TempData-Nutzungsfall:
[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();
}
Warum ich TempData nicht in meinem Blog benutze:
- Ich verwende HTMX anstelle von Umleitungen: Meine Formulare senden über HTMX und geben Teilansichten mit Inline-Erfolg/Fehlermeldungen zurück. Keine Umleitung = keine Notwendigkeit für 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!"
});
}
- Für traditionelle PRG (Post-Redirect-Get), Ich würde TempData verwenden . Aber ich bevorzuge die Vermeidung von Umleitungen, wenn möglich für bessere UX .
Wenn TempData Sinn macht:
- Traditionelle MVC-Apps mit Seitenumleitungen nach POST
- Mehrschritt-Assistenten, bei denen Sie zwischen den Schritten umleiten
- Flash-Nachrichten nach Authentifizierung leitet um
TempData gotcha: Standardmäßig unterstützt durch Cookies (seit ASP.NET Core 2.0). Wenn Sie große Objekte in TempData setzen, blasen Sie das mit jeder Anfrage gesendete Cookie auf. Für großen Zustand verwenden Sie Sitzung mit einem Backing Store oder DB.
Schnellentscheidungsbaum
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]
Meine Regeln:
- ViewBag nur für globale Layout-Sachen (analytisch, nav, breadcrumbs)
- ViewBag für einfache Seitentitel/Positionen (optional; kann ViewModel verwenden)
- Nie ViewBag für komplexe Objekte (Verwenden Sie ViewModels)
- Daten niemals anzeigen (ViewBag hat eine schönere Syntax)
- TempData nur, wenn Sie wirklich PRG brauchen (Ich weiß nicht, dank HTMX)
Muster: Wizards und Multi-Step Flows
Welcher Staatsinhaber soll verwendet werden?
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
- Small, Single-Session: Session oder TempData zwischen den Schritten.
- Cross-Device/long-running: persist to DB, tragen Sie einen Schlüssel in der URL.
Beispiel (DB + Routenschlüssel):
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");
});
Muster: Flash-Nachrichten mit TempData
- In POST setzen; einmal nach Umleitung lesen.
TempData["Flash"] = "Profile saved";
return RedirectToAction("Index");
Razor Ansicht:
@if (TempData["Flash"] is string flash) {
<div class="alert alert-info">@flash</div>
}
Muster: Warenkorb
- Kleine Wagen: Sitzung (wenn Ihre Skala bescheiden ist und Sie klebrige / verteilte Sitzung haben).
- Größere Carts/Multi-Device: DB + Cart-id in Cookie oder URL. Cache für Geschwindigkeit.
flowchart LR U[User] -- cart-id cookie --> S[Server] S --> DB[(Cart Table)] S <--> Cache[Distributed Cache]
Sicherheits-, Datenschutz- und Compliance-Checkliste
- Validieren Sie alle client-provided Zustand: Abfrage, Header, Formulare, Cookies, JWT Ansprüche.
- Schützen Sie sensiblen client-stored Zustand: verwenden Sie Datenschutz für Cookies, die Sie ausgeben; nie speichern Geheimnisse in Query Strings.
- Cookie-Flags setzen:
Secure,HttpOnly,SameSite,IsEssential(falls dies durch Zustimmung/funktionelle Notwendigkeit erforderlich ist) - Authentifizierungs-Cookies bei Privileg-Änderungen generieren; Ansprüche minimal halten.
- Verschlüsseln Sie im Ruhezustand für serverseitige Stores nach Bedarf; sorgen Sie für die Schlüsseldrehung (Datenschutzschlüssel, JWT-Signaturschlüssel).
- GDPR/CCPA: Bereitstellung von Export-/Deletionspfaden für Benutzerdaten; Minimierung der Aufbewahrung.
Entscheidungsmatrix (Heat 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
}
Schnellauswahl:
- Brauchen Sie einen redirect Blitz? TempData.
- Brauchen Sie Assistent über mehrere Anfragen in einer Sitzung? Sitzung (oder DB + Taste, wenn langlaufende / Multi-Gerät).
- Brauchen Sie Skalierbarkeit und staatenlose APIs? JWT für Identität, DB/DistributedCache für Zustand.
- Sie müssen nur Daten innerhalb der Pipeline übergeben? HttpContext.Items.
- Brauchen Sie Cache für berechnete Lookups? IMemoryCache lokal; IDistributedCache über eine Farm.
MVC vs Razor Pages vs Minimal APIs: Gleiche Grundlagen, unterschiedliche Formen
Alle drei Stapel sitzen auf den gleichen Primitiven (HttpContext, Modellbindung, Auth, Datenschutz). Die obigen Beispiele zeigen, dass sich die APIs vor allem in der Ergonomie unterscheiden:
- Minimale APIs: Parameterbindung aus Route/Query/body/Claims; Rückgabe
Results.*. - MVC: Attribute, Filter, Modellbindung in Aktionsparameter/View-Modelle.
- Razor Pages: Seitenhandler mit gebundenen Eigenschaften und Tag-Helper zur Generierung von Links/Formularen.
Sie alle teilen die gleichen staatlichen Mechanismen, die hier diskutiert werden.
Pitfalls und Anti-Patterns
- Speichern großer oder sensibler Daten in Cookies oder TempData.
- Abhängig von In-Memory-Cache für die Richtigkeit (es ist ein Cache, nicht Wahrheit).
- Baugenehmigung basierend auf client-sent route/query Flags ohne Server-Checks.
- Überfütterung von Cookies oder JWTs mit volatilen Claims.
- Sitzung ohne Distributionsstrategie (funktioniert lokal, Brüche im Maßstab).
Real-World State Management: Was ich eigentlich benutze (und nicht verwenden)
Nachdem ich Ihnen all diese Optionen gezeigt habe, hier ist meine ehrliche Einschätzung, was in der Produktion für meine Blog-Plattform funktioniert.
Mein State Management Stack (in der Reihenfolge der Häufigkeit)
Mechanismen Häufigkeit Anwendungsfälle Zufriedenheit |-----------|-----------|-----------|--------------| | ImemoryCache Kategorien, Metriken, Übersetzungsaufgaben | AusgabeCache Hochgewordene Blogposts, Listen, große perf win | AntwortCache Hochaktive HTTP-Caching-Header Funktioniert mit OutputCache | AnsichtBag Medium-Analytik-Einstellungen, Seitentitel OK für einfache Sachen | Auth Claims Mittlere Benutzeridentität, Admin-Flagge Rechtes Werkzeug für auth | Route/Fragen Mittelmäßig Paginieren, Filtern, Schnecken Staatenlos und verlinkbar | Datenbank Medien Blog Beiträge, Kommentare, Zustand Quelle der Wahrheit | Cookies Niedrige Benutzerpräferenzen (zukünftig) Noch nicht benötigt | Sitzung Keine verteilten Store-Einrichtung | TempData HTMX eliminiert den Bedarf | HttpContext.Posten Nie hatte ich nicht Gebrauch Fall | IDistributedCache Ein einziger Server (für jetzt)
Detaillierte Pros/Cons aus Produktionserfahrung
ImmatoriumCache
Wofür ich es benutze:
- Kategorienliste (global, 30 min TTL)
- Per-User-Übersetzung Aufgabenverfolgung (6h absolut + 1h gleiten)
- Externe API-Antworten (Umami-Metriken, 1h TTL)
Profis in der Praxis:
- Schnell blank (im Prozess)
- Keine Serialisierung über Kopf
- Reduzierte DB/API-Last um ~95%
- Einfach zu implementieren und zu verstehen
- Beobachtung über SerilogTracing
Negativ, dass ich getroffen habe:
- Speicherlecks, wenn Sie pro Benutzer Caches nicht einschränken
- Gelöscht beim App-Neustart (annehmbar für meinen Anwendungsfall)
- Nicht über Server verteilt (fein für einzelne Instanz)
- Cache-Ungültigkeit ist manuell (notwendig für Remove() explizit)
Skalierungsgrenzwert: Wenn ich zu mehreren Servern komme, benötige ich IDistributedCache (Redis) für Shared State. Derzeit ist Single Server + Memory Cache perfekt.
AusgabeCache + AntwortCache
Wofür ich es benutze:
- Blog-Post-Rendering (1 Stunden Server, 5 Minuten Client/CDN)
- Blogliste/Kategorienseiten
- Kalenderdaten
Profis in der Praxis:
- 25x Beschleunigung (50ms → 2ms)
- Skalen zu hohem Verkehr, ohne einen Schweiß zu brechen
- Separate TTLs für Server vs Client
- Funktioniert nahtlos mit HTMX (VaryByHeader)
- Messbare Wirkung über Prometheus-Metriken
Negativ, dass ich getroffen habe:
- Debugging-Verwechslung (vergessener Cache ist eingeschaltet)
- Cache-Explosion mit vielen Abfrageparameter-Kombinationen
- Stileness-Fenster (5 min für Kunden)
- Notwendigkeit, bei Inhaltsaktualisierungen manuell ungültig zu werden
Am besten für: Lesen-schwere Anwendungen mit meist statischen Inhalten. Nicht gut für personalisierte oder Echtzeit-Daten.
AnsichtBag
Wofür ich es benutze:
- Globale Layoutdaten (Analysen, Kategorien)
- Titel der Seite
Profis in der Praxis:
- Einfach und schnell für Layout-Level-Daten
- Einmal im BaseController, überall verfügbar
- Funktioniert gut mit der Liste der zwischengespeicherten Kategorien
Negativ, dass ich getroffen habe:
- Keine Typsicherheit (Typen versagen zur Laufzeit)
- Verlockend, für komplexe Daten zu viel zu verwenden
- Schwer zu testen
Ich folge der Regel: ViewBag nur für einfache Skalar. Komplexe Objekte gehen in ViewModels.
Auth-Ansprüche
Wofür ich es benutze:
- Benutzer-ID, Name, E-Mail, Avatar-URL
- Admin-Flagge (Überprüfung)
subAnspruch gegen config)
Profis in der Praxis:
- Sicher (unterschriebenes Cookie, Datenschutz)
- Automatisch mit ASP.NET Core Identity/OAuth
- Verfügbar über
User.Claimsüberall - Sliding Ablauf hält Benutzer angemeldet
Negativ, dass ich getroffen habe:
- Cookie-Größenbegrenzung (nicht überschüssig)
- Ansprüche sind bis zum erneuten Login statisch
- Wenn ich einen Claim (wie "IsEditor") füge, muss Auth-Cookie neu ausgegeben werden
Bewährte Verfahren: Halten Sie Ansprüche minimal und stabil. Legen Sie nicht häufig wechselnde Daten in Ansprüche.
Dinge, die ich nicht benutze (und warum)
Sitzung (nie verwendet):
- Benötigt einen Distributed Store (Redis)
- Erhöht Komplexität für minimalen Nutzen
- Meine Anwendungsfälle werden besser bedient durch:
- Auth Ansprüche (Identität)
- IMemoryCache (kurzlebiger Zustand)
- Datenbank (dauerhafter Zustand)
TempData (nie verwendet):
- HTMX eliminiert Post-Redirect-Get-Muster
- Formulare liefern Teilansichten mit Inline-Nachrichten
- Keine Notwendigkeit, Umleitungen zu überleben
HttpContext.Items (nie verwendet):
- Ich hatte noch keinen Anwendungsfall für den Pro-Request-Zustand.
- Meine Middleware berechnet keine Werte für Controller
- Wenn ich eine Mieter-Erkennung brauche, würde ich sie benutzen.
IDistributedCache (nie verwendet):
- Bereitstellung eines einzigen Servers
- IMemoryCache erfüllt alle Bedürfnisse
- Würde Redis verwenden, wenn ich auf mehrere Server skaliere
Entwicklung meines Ansatzes
Phase 1 (erste Phase): Kein Caching überhaupt. Jede Anfrage traf die Datenbank und machte Markdown. Funktioniert gut für niedrigen Verkehr.
Phase 2 (erste Optimierung): Hinzugefügt IMemoryCache für Kategorien. Saw sofortige DB Lastreduktion. Halten Sie es einfach: 30 min TTL, keine ausgefallene Logik.
Phase 3 (Skalierung): Added OutputCache for blog posts when traffic spiked. Massive Leistung Verbesserung. Erster Fehler: nur 5 Minuten zwischengespeichert. Erhöht auf 1 Stunde nach der Überwachung zeigte Inhalte selten Änderungen.
Phase 4 (Beobachtbarkeit): Hinzugefügt SerilogTracing zum Metrik-Cache. Entdeckte Cache-Misses waren aufgrund der Datumsformatierung in Schlüsseln hoch. yyyyMMdd Anstelle von Vollzeitstempeln. Hitrate stieg von 60% auf 95%.
Phase 5 (HTMX-Integration): Hinzugefügt VaryByHeader für hx-request. Zunächst vergessen dies und serviert volle Seiten für HTMX-Anfragen. Debugging Alptraum, bis ich es herausgefunden habe.
Aktueller Zustand: Glücklich mit dem Stack. IMemoryCache + OutputCache + ResponseCache behandeln 98% meiner staatlichen Management-Anforderungen. Datenbank für dauerhaften Zustand. Auth Ansprüche nach Identität. Das ist es.
Beratung für Ihre App
Starten Sie hier:
- Routen-/Querparameter für die Navigation/Filterung (immer erst staatenlos)
- Auth Ansprüche auf Identität
- Datenbank für alles, was fortbestehen muss
- IMemoryCache für leselastige berechnete Daten
- AusgabeCache für teure Seiten
Bei Bedarf hinzufügen: 6. Sitzung (nur wenn Sie einen serverseitigen Konversationszustand haben müssen) 7. IDistributedCache (nur wenn Sie auf mehrere Server skalieren) 8. Cookies (für clientseitige Präferenzen, Zustimmung)
Vermeiden Sie:
- Komplexe Objekte in ViewBag/TempData
- Sitzung ohne einen verteilten Backing Store
- Caching benutzerspezifische oder häufig wechselnde Daten
- Vorzeitige Optimierung (Maß zuerst!)
Schlüssellektionen:
- Einfach starten, Komplexität nur hinzufügen, wenn Messungen zeigen, dass Sie es brauchen
- Beobachtbarkeit ist entscheidend (kennen Sie Ihre Cache Hit-Raten!)
- Denken Sie über Skalierung Grenzen früh (pro-Benutzer-Caches benötigen Grenzen)
- Ungültigkeit des Caches gründlich testen (Stalenität ist ein echtes Problem)
- Dokumentieren Sie Ihre TTL-Entscheidungen (zukünftig fragen Sie "warum 30 Minuten?")
Wrap-up
Status in Web-Apps ist nicht eine Größe passt alle. Wählen Sie die leichteste Option, die Ihren Bedürfnissen entspricht, bevorzugen staatenlose Muster, wenn Sie können, und explizit über Sicherheit und Lebenszyklus.
Wenn Sie tiefer gehen wollen, wie diese Stücke durch die Pipeline fließen, sehen Sie meine Serie beginnt mit Teil 1: Überblick und Stiftung und vor allem die Middleware- und Routing-Teile.
Glückliches Gebäude.
Anhang: Mehr kopierbare Beispiele (Refinements)
Diese Beispiele vertiefen die früheren Abschnitte mit produktionsfähigen Details, die Sie in net9 Minimalvorlagen, MVC oder Razor Pages einfügen können.
Cookies: Werte mit Datenschutz schützen
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();
});
Tipp: Bei Multi-Node-Bereitstellungen bleiben Datenschutzschlüssel bestehen (z.B. auf ein freigegebenes Dateisystem, Redis oder Azure Key Vault), so dass Cookies über Instanzen hinweg gelesen werden können.
Antifälschung in MVC-, Razor-Seiten und minimalen APIs
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: Aktionen mit dekorieren
[ValidateAntiForgeryToken]und Verwendung@Html.AntiForgeryToken()in Form von Formularen. - Razor Pages: Standardmäßig aktiviert auf Formular-Posts; verwenden
asp-antiforgery="true"falls erforderlich. - Minimal: validieren über
IAntiforgeryWie gezeigt.
Sitzung mit Redis und Sliding vs absolute Ablauf
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();
Speichern Sie nur kleine, kompressible Daten. Persist echte Karren / Bestellungen auf eine DB.
IMemoryCache mit Eingabeoptionen und Räumungsaufruf
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 mit Jitter, um Stempel zu vermeiden
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);
});
Zur robusten Verriegelung bevorzugen Sie Redis Primitives (SET NX EX) über StackExchange.Redis.
Ausgabe und Validierung von JWTs vor Ort (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");
Bedingte Aktualisierungen mit 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: komplexe Objekte über 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-Kohärenz-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);
}
Erfrischende Ansprüche durch Wiederausgabe des Auth-Cookies
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();
});
Dies sollte die Lücken abdecken: stärkere Sicherheitsdefaults, Multi-Node-Präsenz und reale Muster für Caches, Tokens und bedingte Anfragen.
Deep Dive: HttpContext.Items (Praktische Muster und Helfer)
HttpContext.Items ist eine pro-Request-Tasche (IDictionary < Object, Object?>), die nur für die Lebensdauer einer einzigen Anfrage lebt. Es ist perfekt, um berechnete Werte von Middleware/Filtern an Ihre Endpunkte, Controller und Razor Pages-Handler weiterzugeben, ohne den globalen Zustand oder langlebige Stores zu berühren.
- Lebenszyklus: Auf Anfrage erstellter Anfang; verworfen, wenn die Antwort abgeschlossen ist.
- Umfang: Nur aktuelle Anfrage — Kreuzt niemals Umleitungen oder Hintergrundarbeiten.
- Leistung: O(1) Lookups; ideal für das Pro-Request-Caching.
- Sicherheit: nur serverseitig; für den Client nicht sichtbar.
Warum Artikel statt
- Session/TempData: Diese Queranfragen und Einführung von Vertriebsbedenken. Elemente sind ephemer und skalenfreundlich.
- DI Scoped Dienste: Verwenden Sie diese für Verhalten und gemeinsame Abhängigkeiten. Elemente sind besser für Ad-hoc, berechnete Werte (Tenant, Benutzer-Lokalität, Feature-Flags) und pro-Request-Caches.
- HttpContext.Features: Für Framework/Transport-Level-Features (IEndpointFeature, IHttpUpgradeFeature).
Vermeiden Sie Schlüsselkollisionen: stark typisierte Tasten
Da Items Objektschlüssel verwenden, bevorzugen Sie private statische Objektschlüssel oder einen dedizierten Schlüsseltyp, um Namenskollisionen zu vermeiden.
public static class ItemKeys
{
public static readonly object TenantId = new();
public static readonly object UserLocale = new();
public static readonly object PerRequestCache = new();
}
Oder erstellen Sie einen getippten Wrapper mit Erweiterungen:
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;
}
}
Muster: In Middleware berechnen, in Endpoints/Controller/Seiten verbrauchen
// 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) });
Muster: Pro-Request-Cache, um wiederholte Arbeit zu vermeiden
Verwenden Sie Elemente als kleinen Cache, so dass wiederholt liest innerhalb der gleichen Anfrage nicht re-hit Datenbanken / Dienste.
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);
});
Anmerkungen:
- Threading: Eine einzelne Anfrage führt typischerweise auf einem logischen Pfad aus; Elemente sind nicht thread-safe für paralleles Schreiben. Wenn Sie parallele Aufgaben starten, die Artikel teilen, fügen Sie Ihre eigene Synchronisation hinzu.
- Größe: Halten Sie die Werte klein und billig zu berechnen / Serialize. Es ist in-Memory pro Anfrage.
Muster: Filter, die Objekte bevölkern (MVC/Razor-Seiten)
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>());
Muster: Enrich loggt überall ohne Zuweisungen
Berechnen Sie einmal, dann lesen Sie in Logging-Scopes oder 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);
}
});
Wenn Elemente nicht verwendet werden sollen
- Daten, die nach Umleitung oder über Anfragen benötigt werden (verwenden Sie stattdessen TempData/Session/DB).
- App-weite Singletons oder Cross-Request-Caches (verwenden Sie IMemoryCache/IDistributedCache).
- Werte, die zur Identität/Zulassung gehören (Verwendung von Ansprüchen/Politiken).
Schnelle Regel: Wenn es während dieser Anfrage berechnet und innerhalb dieser Anfrage durch Ihren eigenen Code gelesen wird, ist Artikel ideal.