Hålla staten mellan förfrågningar i ASP.NET Core: En praktisk, No-Nonsense Guide (MVC, Razor Sidor, Minimal API:er)
UPPDATERING (2025-11-10): Tillagd mer praktiska exempel från min faktiska kodbas visar verkliga mönster, kompromisser, gotchas, och utvecklingen från enkel till sofistikerad statlig förvaltning metoder. Inkluderar detaljerade ImoryCache mönster, ViewBag användning (bra och dåliga), ResponseCache / OutputCache strategier, och lärdomar från produktionen.
Inledning
HTTP är berömt statslöst. Din app... är inte det. Användare loggar in, lägger till objekt i korgar, hoppar mellan sidor, återvänder i morgon och förväntar sig att du kommer ihåg. I ASP.NET Core (MVC, Razor Pages, Minimal APIs), finns det många sätt att bevara och överföra tillstånd mellan förfrågningar. Vissa är endast per begäran. Några sista för en session. Vissa lever i klienten. Vissa distribueras och överlever serverstarter. Varje val har kompromisser mellan säkerhet, prestanda, skala och utvecklarergonomi.
Det här inlägget katalogiserar alternativen, visar konkreta, kopiera-pasterbara exempel i alla tre staplar, och ger dig beslut vägledning så att du kan välja rätt verktyg för jobbet.
OBS: Detta är en del av mina experiment med AI (assisterad skrivande) + min egen redigering. Samma röst, samma pragmatism; bara snabbare fingrar.
Landskapet vid en glänta
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: fråga, rutt, rubriker, formulär, cookies, JWT. Skalor horisontellt men är synlig för kunden (måste valideras/signeras/krypteras där så är lämpligt).
- Server-skickad: TempData, Session, caches, DB. Kräver affinitet eller distributionsstrategi.
- Per-begäran: HttpContext.Items - användbara för att skicka data internt under en enda begäran.
Gyllene Regler (innan vi dyker in i API:er)
- Säkerheten först: Statens läckage är ett verkligt hot. Server-side-tillstånd (Session, in-minnescache) kan läcka mellan användare genom kodfel, rasförhållanden, eller felaktig livscykelhantering. Stateless mönster är inte bara mer skalbara – de är säkrare.
- Föredrar "statless server" mönster när du behöver skala horisontellt (push state to client pollettes, hållbara butiker, eller distribuerade caches).
- Lita aldrig på klientkontrollerat tillstånd. Validera, signera och/eller kryptera.
- Håll stor data från cookies och headers; de uppblåser varje begäran.
- Använd TempData endast för Post-Redirect-Get (PRG) enbilder som flash-meddelanden.
- Använd Session endast när du måste upprätthålla konversationstillståndet på serversidan och du har planerat distribution – och vara paranoid när det gäller säkerhet.
- Påståenden gäller identitet och grovkornig behörighet, inte allmänt app-tillstånd.
- Cache är inte en källa till sanning. Tillbaka med en hållbar butik om datan spelar någon roll.
Per-request State: HttpContext.Items
- Tillämpningsområde: Endast aktuell begäran (förfall vid slutet av rörledningen)
- Användning för: passera beräknade värden mellan mellanprogram och slutpunkter/kontroller
- Skala: ingen påverkan
- Säkerhet: endast 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"]
Exempel på Middleware (alla staplar):
app.Use(async (context, next) =>
{
var tenantId = context.Request.Headers["X-TenantId"].FirstOrDefault() ?? "public";
context.Items["TenantId"] = tenantId;
await next(context);
});
- Minimalt effektmått för API:
app.MapGet("/whoami", (HttpContext ctx) => new { Tenant = ctx.Items["TenantId"] });
- MVC- styrenhet:
public IActionResult WhoAmI() => Json(new { Tenant = HttpContext.Items["TenantId"] });
- Handläggare för Razor Page:
public IActionResult OnGet() => new JsonResult(new { Tenant = HttpContext.Items["TenantId"] });
Transient Client-Carried State: Ruttvärden och Query String
- Räckvidd: aktuell begäran; kunden bär den explicit.
- Användning för: navigeringskontext, filtrering, paginering, resursidentitet
- Säkerhet: måste validera/auktorisera; bädda inte in hemligheter
Exempel
- Minimala API:
app.MapGet("/orders/{id:int}", (int id, int? page) => Results.Ok(new { id, page }));
// GET /orders/5?page=2
- MVC:
[HttpGet("/orders/{id:int}")]
public IActionResult Details(int id, int? page)
=> View(new { id, page });
- Radarsidor (Beställare/Detaljer.cshtml.cs):
public IActionResult OnGet(int id, int? page)
=> Page();
Skapa länkar som bevarar tillstånd:
// 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)
Rubriker: Korrelations-ID, Idempotency Keys, Feature-flaggor
- Räckvidd: aktuell begäran, valfritt ekade i svaren
- Användning för: spårning, återförsökssäkerhet, A/B-flaggor
- Säkerhet: behandla som opålitlig inmatning, validera/vitlista
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);
});
Oegentlighet (mönster):
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
Former och dolda fält (PRG)
- Räckvidd: endast nästa begäran (klientinlägg tillbaka värdena)
- Användning för: guide steg, anti-förfalska tokens, hålla små bitar av staten över PRG
- Säkerhet: validera alltid; kombinera med förfalskningsskydd
PRG-mönster i MVC/Razor-sidor:
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 });
}
- Radarsidor:
public IActionResult OnPost(SettingsModel model)
{
return RedirectToPage("/Settings/Summary", new { tab = model.SelectedTab });
}
Cookies: Små, signerade, ibland krypterade
- Räckvidd: varje begäran från webbläsaren tills utgången
- Användning för: preferenser, icke-känsliga flaggor, samtycke; auth cookies (separat avsnitt)
- Avsättningar: storleksgränser (~4KB per cookie), prestandapåverkan, måste följa samtyckeslagar
Minimalt API- exempel:
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"));
Användning av MVC/Razor Pages är identisk via HttpContext.
För integritet/konfidentiellhet, använd ASP.NET Core Data Protection-systemet för att skydda nyttolaster som du själv lägger i cookies.
TempData: Buss med ett omkopplat meddelande
- Räckvidd: överlever en omdirigering
- Säkerhetskopiera: Cookie (standard) eller Session
- Användning för: flash-meddelanden, valideringssammanfattningar efter PRG
Ställ in (Program.cs):
builder.Services.AddControllersWithViews().AddSessionStateTempDataProvider(); // optional
builder.Services.AddSession();
var app = builder.Build();
app.UseSession();
I MVC-styrenhet:
TempData["StatusMessage"] = "Saved!";
return RedirectToAction("Index");
I Razor Page Handler:
TempData["StatusMessage"] = "Saved!";
return RedirectToPage("/Index");
I vyn/sidan:
@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]
Session: Konversationstillstånd för serversidor
- Omfattning: webbläsarsession (cookies-nyckel + serverbutik)
- Användning för: flerstegsguider, små kundvagnsdata, strypräknare
- Avyttring: kräver klibbiga sessioner eller distribuerad bakgrundslagring; kan begränsa skalan
Anpassa:
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();
Använda Session (alla staplar):
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);
});
Sessionshjälpare:
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;
}
En varnande berättelse: När sessionstillståndet blir flaskhalsen
Jag arbetade en gång med ett massivt brittiskt IT-projekt där sessionsstatsmissbruk (bland många andra arkitektoniska synder) blev en performance-dödande flaskhals. allt till session: användarinställningar, formulärdata i flera steg, sökresultat, tillfälliga beräkningar, till och med cache-lookups som borde ha varit i en riktig cache eller databas.
Problemet: Sessionstillståndet var lagrat i processen (ASP.NET sessionstillstånd i web.config, detta var pre-Core dagar). Varje begäran var tvungen att deserialisera massiva session objekt. Som belastning ökade, sessionstillstånd ballongerade till tiotals megabyte per användare. Med tusentals samtidiga användare, servrarna fick slut på minne.
Den desperata lösningen: Vi flög till HP: s anläggning i Stuttgart för att köra belastningstester på deras Superdome – på den tiden, Europas mest kraftfulla Windows-maskin. Det var ett odjur: dussintals Itanium processorer, hundratals gigabyte RAM. Tanken var att bevisa att med tillräckligt med hårdvara, systemet kunde uppfylla kraven.
Resultatet: Även på Superdome, kunde vi inte träffa de nödvändiga samtidiga användarmål. sessionstillstånd arkitekturen var i grunden bruten. Vertikal skalning kunde inte spara dålig design. sessionen serialisering / deserialization overhead, kombinerat med minnestryck från massiva session objekt, innebar att systemet helt enkelt inte kunde skala – inte till någon rimlig kostnad.
Säkerhetsmardrömmen: Värre än prestandaproblemen, upptäckte vi ett kodfel som orsakade sessionstillstånd till läckage mellan användare. Användare A: s sessionsdata skulle ibland visas i User B session. Detta var inte bara pinsamt – det var katastrofal. Användarna var NHS-personal Tillgång till patientjournaler och kliniska system. Vi hade av misstag skapat en mekanism för dataintrång som kunde avslöja känslig medicinsk information under olika hälso- och sjukvårdspersonals sessioner.
Vad skulle ha hänt:
- Statslösa som standard: De flesta av sessionsdata skulle aldrig ha funnits
- Databas för hållbart tillstånd: Flerstegs formulär framsteg borde ha varit i databasen med ett arbetsflöde ID
- Cache för uppslagning: Delade uppslag hörde hemma i ImoryCache eller en distribuerad cache
- Kundsida för preferenser: Användarinställningar kunde ha varit i cookies eller lokal lagring
- Distribuerad session vid behov: Om sessionen verkligen var nödvändig, skulle Redis-backed sessionen ha delat belastningen
Lärdomen: Sessionstillstånd skalas inte vertikalt och skalar knappt horisontellt (även med klibbiga sessioner eller distribuerade butiker, du fortfarande serialiserar/deserialiserar på varje begäran). Superdome experimentet visade att kasta hårdvara på arkitektoniska problem är dyrt och ofta meningslöst.
Men ännu viktigare: sessionstillståndsbuggar blir säkerhetssårbarheter. Tråd-säkerhet frågor, ras villkor, felaktig session ID-hantering – dessa inte bara orsakar prestanda problem, de kan läcka känslig data mellan användare. I ett hälso-och sjukvårdssammanhang (eller bank, eller någon reglerad industri), är detta en efterlevnad mardröm och potentiell brottsligt ansvar.
Moderna råd:
- Om du behöver mer än några KB av sessionsdata, har du förmodligen ett designproblem
- Om du lagrar känsliga data i session, skapar du en säkerhetsattack yta
- Statlösa arkitekturer skalas inte bara bättre – de är till sin natur säkrare eftersom det inte finns något server-side-tillstånd att läcka
- Tänk om din strategi för statlig förvaltning innan du behöver flyga till Stuttgart (eller förklara ett dataintrång för informationskommissionären)
Caching: IMemoryCache och I DistributedCache
Tillämpningsområde: serverprocess (IMEMoryCache) eller distribuerad (IDistributedCache)
Användning för: härledd/komputerad data, uppslagning, kortlivat tillstånd
Avyttring: cacheinvalidering, seriellisering för distribuerad
Vad är det med dig?
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);
});
I DistributedCache (t.ex., 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-mönster varning: Om det måste vara hållbart eller auktoritativt, lagra den i en databas och valfritt cache den.
Välja mellan IMemoryCache och I DistributedCache
-
- Vad är det med dig?
- Blasing snabbt, under process, objekt förblir som objekt (ingen serialisering).
- Vräkning av minnestryck, storleksgräns, absolut/glidande utgång och prioritet.
- Ej delat mellan noder; rensat på app återvinning/deployment.
- Perfekt för per-node hotset, beräknade lookups, korta TTLs.
- IDistributedCache (Redis/SQL/etc.):
- Delas över en gård; överlever app startar om; kräver seriellisering (strängar/byte).
- Något högre latens; dataflöde beror på nätverk och gränssnitt.
- Stöder absolut/glidande utgång (leverantörsberoende; Redis leverantör uppdaterar TTL om åtkomst för glidning).
- Idealisk för cross-node konsistens, stora fläkt-out avläsningar, funktionsflaggor, och session.
Gemensamma cachestrategier
Cache-uttag (vanligast):
- Försök cache; 2) om du missar, ladda från källa; 3) skriv till cache; 4) returnera.
- Fördelar: enkel; källan till sanningen förblir databasen.
- Minus: första begäran efter utgången är långsam; möjliga stämplar.
Genomläsning (via ett bibliotek/leverantör): cache hanterar lastning på missar.
Write-through: skriver gå till cache och säkerhetskopiera lagra synkroniserat.
Skriv-bakgrund: skriv till cache, spola för att lagra asynkront (risk: förlust/inkonsistens).
Refresh-ahead: uppdatera heta nycklar innan de löper ut för att undvika kalla missar.
Utgång, vräkning och dimensionering
- Absolut utgång: löper alltid ut efter en fast varaktighet (bra för extern data färskhet).
- Glidande utgång: utökar TTL vid åtkomst (bra för sessioner/användarspecifika data).
- Storleksbaserad vräkning (IMEMoryCache): ange post.Storlek och anpassa storlekLimit till bundet minne.
- Prioritet (MemoryCache): CacheItemPrioritering.Hög/Normal/Low/NeverRemove påverkar vräkning under tryck.
- Jitter: Lägg till små slumpmässiga förskjutningar i TTL för att undvika synkroniserad utgång (stampar).
Förhindra cachekräftor (tröghetsbesättning)
- Använd GetOrCreate/GetOrCreateAsync (MemoryCache) för att säkerställa enkelgängad population per nod.
- Distribuerad: använd en kortlivad låsnyckel (SET NX EX) eller biblioteksstöd; lägg till TTL jitter; överväga att uppdatera bakgrunden.
- Servera död-samtidigt-revalidat: behåll en sekundär nyckel med gammalt värde och kort förlängning medan nytt värde beräknas.
Nyckeldesign och namnsättning
- Föredrar små bokstäver, kolon avgränsade nycklar: app:entity:123 eller hyresgäst:us:us:users:42.
- Inkludera versionssegment för att ogiltigförklara hela klasser av nycklar utan att ta bort: v2:products:123.
- Hyresgäst-medveten: prefix nycklar med hyresgäst eller organisation ID för att undvika kollisioner och underlätta utrensningar.
- Håll tangenterna små men beskrivande; undvik användarkontrollerad rå input utan normalisering.
Utgående uppsättningar nycklar (taggar/grupper)
När du behöver ogiltigförklara många relaterade poster:
Versionerade prefix (soft invalidation): slå en global version i en liten nyckel och komponera nycklar med den.
// version key: "v:products"; keys like $"{version}:product:{id}" var version = await cache.GetStringAsync("v:products") ?? "1"; var key = $"{version}:product:{id}";För att ogiltigförklara alla produkter: ökning v:produkter (klienter kommer naturligtvis att missa gamla prefixade nycklar).
Tagguppsättning per grupp (Redis): behåll en uppsättning nycklar per tagg; vid ogiltigförklaring, hämta medlemmar och ta bort.
// 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-invalidering: Publicera ett "invalidate:key"-meddelande; varje nod tar bort nyckeln från sin lokala IMEMoryCache.
Skanna med mönster: SCAN/KEYS bör undvikas i prod heta vägar; okej för administratörsverktyg på små nyckelytor.
Praktiska hjälpare
- ImoryCache få-eller-set med alternativ:
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); }); - I DistributedCache med JSON och utgångsdatum:
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; }
Övervakning och synlighet
- Spårets träff/felfrekvens och genomsnittlig belastningstid; exponera mätvärden (Prometheus räknare) per nyckelgrupp.
- Lägg till loggning runt cache befolkning och vräkning callbacks för ImoryCache.
- För Redis, titta på tangentrymd hits/misses, latency, och minnesfragmentering; ange maxminnespolicyer som lämpligt.
Real-World MemoryCache Mönster från produktionen
Här är hur jag faktiskt använder IMemoryCache i min blogg plattform, utvecklats genom försök och fel. Jag ska visa dig tre verkliga mönster från enkel till sofistikerad.
Mönster 1: Enkelt cacheuttag för sällan växlande data (Kategorier)
Detta var min första caching genomförande. Blogg kategorier ändras inte ofta, så cache dem i 30 minuter:
// 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;
}
Varför detta fungerar:
- Kategorier läses på varje sida (visas i navigering)
- De ändras sällan (endast när jag lägger till nya blogginlägg med nya kategorier)
- 30-minuters TTL är bra; om nya kategorier visas, användare se dem inom 30 minuter
- Absolut utgång endast (ingen glidning) eftersom vi inte bryr oss om hur ofta det är nås
Fick dig jag träffade: Inledningsvis använde jag en strängnyckel "Kategorier". Fungerar bra tills du har flera styrenheter och en av misstag återanvänder samma nyckel. Nu använder jag konstanter eller starkt typade tangenter (se föregående avsnitt om att undvika kollisioner).
Mönster 2: Per-användare tillstånd med storleksgränser och glidande utgång (Översättningsuppgifter)
Detta caches översättningsstatus per användare. Det är mer komplicerat eftersom det behöver gränser och bör hålla sig vid liv så länge användaren är aktiv:
// 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)
});
}
}
Därför är detta annorlunda:
- Glidande utgång: Om användaren fortsätter att kontrollera översättningsstatus, hålla cachen vid liv upp till 6 timmar max
- Storleksgränser: Behåll bara 5 senaste uppgifter per användare för att förhindra minnesblåsning
- Manuell hantering: Jag begränsar uttryckligen storlek eftersom MemoryCache inte genomdriva entry-level objekt räkna gränser (endast övergripande cachestorlek)
Misstag jag gjorde: Inledningsvis begränsade jag inte antalet uppgifter. En strömanvändare utlöste 50+ översättningar och jag hade en minnesläcka. Nu håller jag max 5 per användare.
Avräkning: Detta kommer inte att skala till miljontals användare. Om detta blir ett problem, Jag skulle flytta till IDistributedCache (Redis) eller lagra i databasen med ett index på userId + startTime.
Mönster 3: Cache med observerbarhet (Metrics caching)
Detta cache mätvärden och spår cache effektivitet med hjälp av Serilog spårning:
// 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;
}
}
Vad gör denna produktion-ready:
- Observationsförmåga: SerilogTracing aktivitet spår cache hit / miss hastighet och poster mätvärden räkna
- Sammansatt nyckel: Inkluderar datumintervall och prefix för att undvika viktiga kollisioner
- Datum för formatering: Användningsområden
yyyyMMddformat i nyckel så olika tider samma dag dela cache - Hantering av null: Returnerar noll om extern tjänst misslyckas; inte cachefel
- Filtrerad cachelagring: Caches det filtrerade resultatet, inte rå respons
Utveckling: Inledningsvis cached jag för 10 minuter. Men Umami mätvärden är tunga att hämta och inte ändra mycket, så 1 timme är bra. Jag upptäckte detta genom att titta på SerilogTracing data och se överdriven cache missar.
Övervakning i verksamhet: I Seq (min loggsamlare) kan jag fråga:
ActivityName = "GetMetricsWithPrefix" and CacheHit = false
Om den är hög, justerar jag TTL eller strategi.
Jämförelse av de tre tillvägagångssätten
på mönster på användningsfall på grund av att storlekskontrollen upphör att gälla på grund av att det inte längre är möjligt att kontrollera storleken |---------|----------|------------|--------------|---------------| | Kategorier globala, sällan föränderliga 30 min absolute, inte behövs (små) på grund av loggning | Översättningsuppgifter till Per-användare, avgränsad till 6h absolut + 1h glidande på Manual (5 punkter max) och ingen (bör läggas till!) | Tröskelvärden till Dyr externa samtal 1h absolut på naturlig (tid-fönsterad) på full spårning
Viktiga lärdomar:
- Starta enkelt (mönster 1), lägg till komplexitet endast när det behövs
- Tänk alltid på användning av cacheminne - lägg till storleksgränser för cachefiler per användare
- För dyra operationer, lägg till observerbarhet från dag ett
- Justera TTL baserat på faktiska data förändringsfrekvens, inte gissningar
När jag inte använder IMemoryCache:
- För användarautentiseringstillstånd (anspråk i auth-cookie istället)
- För kundvagn (skulle använda DB + distribuerad cache i produktionen)
- För blogginlägg innehåll (redan i DB, laddad en gång per begäran)
- Gränsöverskridande servertillstånd (skulle behöva I DistributedCache/Redis)
Behörighetscookies och anspråk
- Tillämpningsområde: över ansökningar tills de upphör att gälla/undertecknas
- Användning för: identitet, grova roller/tillstånd, en liten mängd profildata
- Avsättningar: cookie storlek; gör inte för mycket. Påståenden bör vara stabila.
Ställ in cookie- behörighetskontroll:
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();
Logga in med fordringar (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("/");
});
Läs påståenden (alla staplar):
[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
- Omfattning: klienten bär token; statslös server
- Användning för: SPA/mobil/API, cross-domän, microservices
- Avyttring: tokenstorlek; rotation/uppfräschning; lagra minimala anspråk, använd introspektion vid behov
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", o =>
{
o.Authority = "https://demo.identityserver.io"; // example
o.Audience = "api";
o.RequireHttpsMetadata = true;
});
Användning:
[Authorize(AuthenticationSchemes = "Bearer")]
app.MapGet("/secure", () => "ok");
Översikt över sjöjungfrur:
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
Hållbar stat: Databas och vänner
- Räckvidd: för alltid (tills du tar bort den)
- Användning för: allt som inte får gå förlorat: vagnar, beställningar, profiler, långa arbetsflöden
- Mönster: standard CRUD med EF Core; CQRS; händelsesourcing; utkorgsmönster för tillförlitlighet
Miljöavtryckets kärnritning:
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);
});
Svar Caching, ETags, och villkorliga förfrågningar
- Inte strikt tillstånd bär, men minskar upprepade arbete genom att låta klienten / ombud återanvända tidigare svar. Ofta paras med fråga / route tillstånd.
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();
Exempel på 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 Användning i produktion
Jag använder både ResponseCache och OutputCache tillsammans på min blogg för olika ändamål. Här är varför du kanske vill både och hur de skiljer sig.
Förvirringen: Två cacheegenskaper?
ASP.NET Core har två Likvärdiga cachesystem:
- ResponseCache (HTTP caching): Ställer in HTTP- rubriker (
Cache-Control,Vary) berätta webbläsare och CDNs hur man cache e ör - UtmatningCache (serversidan): Caches den utgivna utmatningen på servern för att hoppa över att utföra åtgärden helt
De kompletterar varandra. ResponseCache hanterar klient/CDN-cache; OutputCache hanterar serversidecache.
Min blogg inlägg åtgärd (båda cache tillämpas)
// 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);
}
Vad händer när någon begär /blog/my-post:
UtmatningCache-kontroller först: Har jag en cachad svar för
my-post+enSpråk?- Träff: Return cachad HTML, åtgärdsmetod aldrig körs (snabb! ~1ms)
- Fröken: Kör åtgärd, visa, cacheresultat i 3600 sekunder (1 timme)
ResponseCache sätter rubriker: Efter utgångCache genererar svaret, ResponseCache lägger till:
Cache-Control: public, max-age=300 Vary: hx-requestBrowser caching: Webbläsare cache svaret i 300 sekunder (5 minuter). Efterföljande förfrågningar från samma användare inte ens träffa servern.
CDN-cachelagring (om du använder Cloudflare/Fastly): CDN-cache i 5 minuter. Användare över hela världen slog CDN, inte min server.
Varför olika varaktigheter? (300 vs 3600)
[ResponseCache(Duration = 300)] // 5 minutes client/CDN cache
[OutputCache(Duration = 3600)] // 1 hour server cache
Motivering:
- Servercachen är längre (1 timme): Jag kontrollerar min server; Jag kan rensa cache om jag uppdaterar ett inlägg
- Kundcachen är kortare (5 minuter): Jag kan inte rensa användare webbläsare eller CDNs lätt; 5 minuter är ett rimligt dödläge fönster
- Avräkning: Om jag redigerar ett inlägg visas nytt innehåll:
- Server-side: Omedelbart (jag kan ogiltigförklara cache)
- CDN/browsers: Inom 5 minuter (eller jag manuellt rensa CDN)
Utveckling: Inledningsvis hade jag båda på 5 minuter. Men det innebar att min server var femte minut även om innehållet sällan ändras. Nu:
- Servern servar gladely cached HTML för 1 timme
- Kunderna får färskt innehåll var femte minut
VaryByHeader för HTMX
VaryByHeader = "hx-request" // ResponseCache
VaryByHeaderNames = new[] { "hx-request" } // OutputCache
Därför är detta viktigt: HTMX-förfrågningar inkluderar hx-request: true Rubrik. Jag returnerar olika svar:
- Fullständig begäran: Komplett HTML-sida med layout
- HTMX- begäran: Partiell vy utan layout
Utan VaryBy, cache skulle returnera fel format. VaryBy, Jag cache två versioner av varje sida.
Exempel:
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 språk och pagination
[ResponseCache(VaryByQueryKeys = new[] { nameof(slug), nameof(language) })]
[OutputCache(VaryByQueryKeys = new[] { nameof(slug), nameof(language) })]
Problem utan detta: /blog/my-post?language=fr skulle tjäna den engelska cachad-versionen.
Med VaryByQueryKeys: Separata cacheposter:
/blog/my-post?language=en→ Cache-nyckeln innehåller "en"/blog/my-post?language=fr→ Cache nyckel inkluderar "fr"
Verklig användning från min blogglista:
[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, /* ... */)
Varning för cacheexplosion: Varje unik kombination av parametrar = separat cachepost:
page=1&pageSize=20&language=en→ Ett inläggpage=2&pageSize=20&language=en→ En annan postpage=1&pageSize=10&language=en→ En annan post
Begränsning:
- Rimlig max cachestorlek (OutputCache auto-evicts som används minst nyligen)
- Vanliga parametrar cachad (sida 1, förvald sidaStorlek)
- Mindre vanliga kombinationer kan missa cache (acceptabel)
Inställning krävs
SvarKassa fungerar ut ur lådan, men för UtmatningCache du behöver ställa in:
// 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
När OutputCache inte hjälper
UtmatningCache hoppar över för:
- Autentiserade förfrågningar (olika användare ser olika data)
- POST/PUT/DELETE (endast GET/HEAD-cached)
- Svar med
Set-CookieHuvud - Svar som uttryckligen anger
Cache-Control: no-store
Exempel där jag inte använder den:
// 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
}
Mätning av effektivitet
Jag använder Prometheus mätvärden (som visas via min app) för att spåra:
// Pseudo-code for metrics
cache_hits_total{cache="output"} 45230
cache_misses_total{cache="output"} 892
Min träffhastighet i cache: ~98% för blogginlägg (de flesta trafik träffar samma populära inlägg upprepade gånger).
Inverkan:
- Utan caching: ~50ms genomsnittlig svarstid (DB fråga + Markdown rendering)
- Med OutputCache: ~1-2ms för cacheade svar
- 25x upphastighet
När jag tillfälligt inaktiverar caching
Ibland felsöker jag och behöver nya svar varje gång:
// During development, comment out caching
// [ResponseCache(Duration = 300, ...)]
// [OutputCache(Duration = 3600, ...)]
public async Task<IActionResult> Show(string slug, string language = "en")
Eller använd miljöspecifika inställningar:
#if DEBUG
// No caching in development
#else
[ResponseCache(Duration = 300, ...)]
[OutputCache(Duration = 3600, ...)]
#endif
Bättre tillvägagångssätt: Använd inställning:
[ResponseCache(Duration = responseCacheDuration, ...)]
där responseCacheDuration är 0 i utveckling, 300 i produktion.
För- och nackdelar med denna strategi med dubbla kedjor
Förmåner:
- Servereffektivitet: UtgångCache minskar CPU / DB belastningen med 98%
- Förmåner för klient/CDN: ResponseCache minskar min bandbredd och förbättrar den globala latensen
- Flexibilitet: Olika TTL för server mot klient
- Kostnadsbesparingar: Färre DB-frågor, mindre bandbredd
Minus:
- Tålamod: Innehållsuppdateringar tar upp till 5 minuter att sprida till kunder
- Invaliditet av cache: Uppdatering av ett inlägg kräver ogiltigförklaring av både server- och CDN-cache
- Minnesanvändning: OutputCache håller renderad HTML i serverminnet
- Felsökning av förvirring: Ibland glömmer cache är på och undrar varför förändringar inte visas
När jag hoppade över caching:
- Realtidsdata (stockpriser, livesport)
- Personligt innehåll (användarspecifika rekommendationer)
- Lågtrafiksidor (caching overhead > förmån)
- Sidor som ändras mycket ofta
Min dom: För en blogg med mestadels statiskt innehåll och högt läs-/skrivförhållande är dubbel caching en enorm vinst. Jag skulle inte använda detta på administratörspaneler eller instrumentpaneler med snabbt föränderliga data.
ViewBag, ViewData och TempData: Controller-to-View State (och varför jag mestadels undviker två av dem)
Dessa tre är ofta förvirrade. Så här skiljer de sig åt och vad jag faktiskt använder i produktionen.
De tre amigos jämfört
// 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");
på funktionen på ViewData på ViewBag på TempData
|---------|----------|---------|----------|
| Typ | ViewDataDictionary på dynamisk nivå ITempDataDictionary |
| Livstid på nuvarande begäran på nuvarande begäran på en omdirigering
| Nyckelåtkomst Stringtangenterna med egenskapssyntax på Stringtangenterna
| Typsäkerhet Inga (sändningar behövs) på Inga (dynamiska) på Inga (sändningar behövs)
| Kontroll av kompileringstid Det finns inga bevis för att det finns någon anledning att tro att det inte finns någon anledning att tro att det inte finns någon anledning till det.
| Omdirigering av överlevande Nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej, nej,
Vad jag faktiskt använder: ViewBag för globala layoutdata
I min blogg använder jag ViewBag uteslutande för att överföra data från styrenheter till delad layout (analytik, kategorier, 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);
}
Sedan i min 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>
}
Varför detta mönster fungerar:
- Allmänt: Varje sida behöver kategorier navigering och analys
- Beräknad en gång: BaseController körs före varje åtgärd
- HTMX-optimering: Skip analytics skript på partiella förfrågningar (HTMX behöver inte det åter injiceras)
- Nötkreatur: Kategorier är cachad (se min IMemoryCache mönster ovan), så inte slå DB varje begäran
Misstag jag gjorde tidigt: Jag ställde in. ViewBag.Categories i varje åtgärd metod. TRY överträdelse och lätt att glömma. Flytta den till OnActionExecutionAsync i baskontrollen löste detta.
ViewBag för sidspecifika data (acceptabelt mönster)
// 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);
}
Mot bakgrund av följande:
@{
ViewData["Title"] = ViewBag.Title; // Standard MVC convention for <title>
}
<h1>Category: @ViewBag.Category</h1>
Detta är okej eftersom:
- Enkla skalorvärden (sträng, int)
- Används endast i vyn, inte passerat runt
- Alternativt skulle läggas till
TitleochCategoryegenskaper för varje visningsmodell
Vad jag undviker: Komplexa objekt i ViewBag
Patientskydd:
// 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 };
Problem:
- Ingen kompilerings-tidssäkerhet (typo)
ViewBag.Usrmisslyckas vid körning) - Svårt att spåra vilka data som finns tillgängliga i vyn
- Gör testning svårare (måste inspektera ViewBag ordbok)
- Ingen underförståddhet
Bättre: Starkt utformade vymodeller:
// 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: Jag använder den inte (och här är varför)
Standard TempData användningsfall:
[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();
}
Varför jag inte använder TempData i min blogg:
- Jag använder HTMX istället för omdirigeringar: Mina formulär skicka in via HTMX och returnera partiella vyer med inline success/terror meddelanden. Ingen omdirigering = inget behov av 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 traditionell PRG (Post-Redirect-Get), Jag skulle använda TempData. Men jag föredrar att undvika omdirigeringar när det är möjligt för bättre UX.
När TempData är vettigt:
- Traditionella MVC-appar med helsida omdirigeringar efter POST
- Flerstegsguider där du omdirigerar mellan stegen
- Flash-meddelanden efter autentisering omdirigerar
TempData fick dig: Som standard backas upp av cookies (sedan ASP.NET Core 2.0). Om du lägger stora objekt i TempData, uppblåser du cookien som skickas med varje begäran. För stora tillstånd, använd Session med en bakgrundsbutik eller DB.
Snabbt beslutsträd
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]
Mina regler:
- ViewBag endast för globala layout-grejer (analytiker, navigator, brödsmulor)
- ViewBag för enkla sidtitlar/rubriker (frivilligt; kan använda ViewModel)
- Visa aldrig Bag för komplexa objekt (använd visningsmallar)
- Visa aldrig data (ViewBag har trevligare syntax)
- TempData endast om du verkligen behöver PRG (Det gör jag inte, tack vare HTMX)
Mönster: Guider och flerstegsflöden
Vilken tillståndshavare ska användas?
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
- Liten, enkelsession: Session eller TempData mellan stegen.
- Cross-device/long-running: Fortsätt till DB, bär en nyckel i webbadressen.
Exempel (DB + ruttnyckel):
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");
});
Mönster: Flashmeddelanden med TempData
- Ställ in POST; läs en gång efter omdirigering.
TempData["Flash"] = "Profile saved";
return RedirectToAction("Index");
Razorvy:
@if (TempData["Flash"] is string flash) {
<div class="alert alert-info">@flash</div>
}
Mönster: Varukorg
- Små vagnar: Session (om din skala är blygsam och du har klibbig/distribuerad session).
- Större vagnar/multi-enhet: DB + cart-id i cookie eller URL. Cache för hastighet.
flowchart LR U[User] -- cart-id cookie --> S[Server] S --> DB[(Cart Table)] S <--> Cache[Distributed Cache]
Checklista för säkerhet, integritet och efterlevnad
- Validera alla kundtillhandahållna tillstånd: fråga, rubriker, formulär, cookies, JWT-anspråk.
- Skydda känsliga klientlagrade tillstånd: använd dataskydd för cookies du utfärdar; lagra aldrig hemligheter i frågesträngar.
- Ställ in cookieflaggor:
Secure,HttpOnly,SameSite,IsEssential(om så krävs av samtycke/funktionellt behov). - Regenerera autentiseringscookies om privilegieändringar; behåll anspråk minimalt.
- Kryptera i vila för server-side butiker efter behov; säkerställa nyckel rotation (Data Protection nycklar, JWT signering nycklar).
- GDPR/CCPA: ge användaren data export/deletion vägar; minimera lagring.
Beslutsmatris (värmeblad)
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
}
Snabbval:
- Behöver du en redirect-blixt?
- Behöver du guide över flera förfrågningar i en session? Session (eller DB + nyckel om långkörning/multi-enhet).
- Behöver skalbarhet och statslösa API:er? JWT för identitet, DB/DistributedCache för stat.
- Behöver du bara skicka data inne i rörledningen? HttpContext.Items.
- Behöver cache för beräknade uppslag? IMemoryCache lokalt; I DistributedCache över en gård.
MVC vs Razor Pages vs Minimal API: Samma grunder, olika former
Alla tre staplar sitter på samma primitiva (HttpContext, modellbindning, auth, dataskydd). Exemplen ovan visar att API:erna skiljer sig mest åt i ergonomi:
- Minimala API: Parameter som binder från rutt/query/body/claims.
Results.*. - MVC: attribut, filter, modellbindning till åtgärdsparametrar/vymodeller.
- Razor Sidor: sidhanterare med bundna egenskaper och tagghjälpare för att skapa länkar/former.
De delar alla samma statliga mekanismer som diskuteras här.
Fallgropar och antipatner
- Lagring av stora eller känsliga data i cookies eller TempData.
- Beroende på in-minne cache för korrekthet (det är en cache, inte sanning).
- Bygga behörighet baserat på klient-skickad rutt/query flaggor utan serverkontroller.
- Överfyllning av auth cookies eller JWTs med flyktiga påståenden.
- Session utan distributionsstrategi (fungerar lokalt, bryter i skala).
Real-World State Management: Vad jag faktiskt använder (och inte använder)
Efter att ha visat dig alla dessa alternativ, här är min ärliga bedömning av vad som fungerar i produktionen för min blogg plattform.
Min statsförvaltnings stack (i ordning efter frekvens)
Mekanism på frekvens på användningsfall på tillfredsställelse på grund av tillfredsställelse
|-----------|-----------|-----------|--------------|
| ImemoryCache Ordförande en mycket hög med kategorier, mätvärden, översättningsuppgifter
| UtmatningCache / Höga på Rendered blogginlägg, listar på Stora perf vinna
| SvarKassa Höga HTTP-cachehuvuden och fungerar med OutputCache
| VisaBag Inställningar för medelstora analyser, sidtitlar och OK för enkla saker
| Sanningspåståenden Medium på användarens identitet, adminflagga på rätt verktyg för auth
| Färdväg/Löjtnant på medelstort, filtrerande, sniglar, statlösa och länkbara
| Databas "Medium på blogginlägg, kommentarer, stat" på "Sanningens källa"
| Kakor Preferenser för användare (framtida) har ännu inte behövts
| Sammanträde på Aldrig på - på ❌ Ingen distribuerad butiksinstallation
| Tempdata Aldrig - ❌ HTMX eliminerar behovet
| HttpContext. Föremål Har aldrig haft användningsfall
| I DistributedCache på aldrig - på en server (för tillfället)
Detaljerade för- och nackdelar från produktionserfarenhet
ImemoryCache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Vad jag använder den till:
- Kategorier lista (global, 30 min TTL)
- Spårning av översättningsuppgifter per användare (6h absolut + 1h glidning)
- Externa API-svar (Umami metrics, 1h TTL)
Förmåner i praktiken:
- Blåsning snabbt (under bearbetning)
- Ingen seriellisering över huvud taget
- Minskad DB/API belastning med ~95%
- Lätt att implementera och förstå
- Observationsförmåga via SerilogTracing
Jag har slagit till:
- Minnesläckage om du inte begränsar cacheminnen per användare
- Cleared vid omstart av appen (acceptabelt för mitt användningsfall)
- Ej delat på servrar (fint för enstaka instans)
- Cache ogiltigförklaring är manuell (behöver ta bort () explicit)
Skalningsgräns: Om jag kommer till flera servrar, skulle jag behöva IDistributedCache (Redis) för delat tillstånd. För tillfället är en enda server + minne cache perfekt.
UtgångCache + ResponseCache till
Vad jag använder den till:
- Blogg inlägg rendering (1 timme server, 5 min klient / CDN)
- Blogglista/kategorisidor
- Kalenderuppgifter
Förmåner i praktiken:
- 25x upphastighet (50ms → 2ms)
- Skalar till hög trafik utan att svettas
- Separata TTL för server mot klient
- Fungerar sömlöst med HTMX (VaryByHeader)
- Mätbar påverkan via Prometheus-mått
Jag har slagit till:
- Felsökning av förvirring (glöm att cache är på)
- Cache explosion med många frågeparameter kombinationer
- Stabilitetsfönster (5 min för klienter)
- Behov av att manuellt ogiltigförklara innehållsuppdateringar
Bäst för: Lästunga program med mestadels statiskt innehåll. Inte bra för personliga eller realtidsdata.
VisaBag
Vad jag använder den till:
- Globala layoutdata (analys, kategorier)
- Sidtitlar
Förmåner i praktiken:
- Enkelt och snabbt för layout-nivå data
- Ställ in en gång i BaseController, finns överallt
- Fungerar bra med lista över cachade kategorier
Jag har slagit till:
- Ingen typ av säkerhet (typos misslyckas vid körning)
- Frestande att överutnyttja för komplexa data
- Svårt att testa
Regel I följer: ViewBag för enkla skalar. Komplexa objekt går i ViewModels.
Auth Claims påstått
Vad jag använder den till:
- Användar-ID, namn, e-post, avatar URL
- Administratörsflagga (kontroll
subanspråk mot inställning)
Förmåner i praktiken:
- Säker (signerad cookie, dataskydd)
- Automatisk med ASP.NET Core Identitet / OAuth
- Tillgänglig via
User.Claimsöverallt - Slidande utgångsdatum håller användare inloggade
Jag har slagit till:
- Gräns för cookiestorlek (överskrid inte påståenden)
- Påståenden är statiska tills de loggas in igen
- Om jag lägger till ett påstående (som "IsEditor"), måste återutge auth cookie
Bästa praxis: Håll anspråk minimala och stabila. Lägg inte ofta växlande data i påståenden.
Saker jag inte använder (och varför)
Session (används aldrig):
- Skulle kräva distribuerad lager (Redis)
- Ökar komplexiteten för minimal nytta
- Mina användningsfall betjänas bättre av:
- Authority-anspråk (identitet)
- ImoryCache (kortlivat tillstånd)
- Databas (varaktigt tillstånd)
TempData (används aldrig):
- HTMX eliminerat mönster efter omdirigering
- Formulär returnerar partiella vyer med inline-meddelanden
- Inget behov av att överleva omdirigeringar
HttpContext.Items (aldrig använda):
- Har inte haft ett användningsfall för per-request state
- Mitt mellanprogram beräknar inte värden för regulatorer
- Om jag behövde detektering av hyresgästen, skulle jag använda den.
I DistributedCache (används aldrig):
- Installation av en enda server
- ImoryCache uppfyller alla behov
- Skulle använda Redis om jag skalade till flera servrar
Utveckling av mitt sätt att närma mig
Fas 1 (ursprunglig): Ingen caching alls. Varje begäran träffade databasen och gjorde Markdown. Arbetade bra för låg trafik.
Fas 2 (första optimering): Tillagd IMemoryCache för kategorier. Såg omedelbar DB-belastning minskning. Sparade det enkelt: 30 min TTL, ingen tjusig logik.
Fas 3 (uppskalning): Lägger till OutputCache för blogginlägg när trafiken spikade. Massiv prestandaförbättring. Initial misstag: cachad för 5 minuter bara. Ökat till 1 timme efter övervakning visade innehåll sällan förändringar.
Fas 4 (observerbarhet): Lägger till SerilogTracing till mätvärden cache. Upptäckt cache missar var hög på grund av datumformatering i nycklar. Fast nyckelformat till yyyyMMdd Slagfrekvensen gick från 60% till 95%.
Fas 5 (integration av HTMX): Tillagd VaryByHeader för hx-request. Inledningsvis glömde detta och serverade hela sidor till HTMX förfrågningar. Debugging mardröm tills jag kom på det.
Nuvarande tillstånd: Lycklig med stacken. IMemoryCache + OutputCache + ResponseCache hantera 98% av mitt statliga behov. Databas för hållbart tillstånd. Auth anspråk på identitet. Det är allt.
Råd till din app
Börja här:
- Rutt/query-paramer för navigering/filtrering (alltid statslösa först)
- Auth-anspråk för identitet
- Databas för allt som måste finnas kvar
- IMemoryCache för lästunga beräknade data
- UtmatningCache för dyra-att-rendera sidor
Lägg till vid behov: 6. Session (endast om du måste ha konversationstillstånd på serversidan) 7. I DistributedCache (endast när du skala till flera servrar) 8. Cookies (för preferenser på klientsidan, samtycke)
Undvik:
- Komplexa objekt i ViewBag/TempData
- Session utan distribuerad bakgrundslagring
- Caching användarspecifika eller ofta föränderliga data
- För tidig optimering (mät först!)
Viktiga lärdomar:
- Starta enkelt, lägg till komplexitet endast när mätningar visar att du behöver det
- Observerbarheten är kritisk (känn din cache hit hastigheter!)
- Tänk på skala gränser tidigt (per-användare caches behöver gränser)
- Provningscacheinvalidering grundligt (sanning är ett verkligt problem)
- Dokumentera dina TTL val (framtid kommer du att fråga "varför 30 minuter?")
Uppslagning
Ange i webbappar är inte en storlek passar alla. Välj det lättaste alternativet som uppfyller dina behov, föredrar statslösa mönster när du kan, och vara tydlig om säkerhet och livscykel.
Om du vill gå djupare på hur dessa bitar flyter genom rörledningen, se min serie börjar med Del 1: Översikt och Stiftelse och särskilt de mellanprodukter och routing delar.
Trevlig byggnad.
Tillägg: Fler exemplar-Pasteable exempel (Refinements)
Dessa exempel fördjupar de tidigare avsnitten med detaljer av produktionskvalitet som du kan klistra in i minimala mallar för net9, MVC eller Razor Pages.
Cookies: Skydda värden med dataskydd
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();
});
Tips: Vid multi-node distributioner, framhärda dataskydd nycklar (t.ex. till ett delat filsystem, Redis, eller Azure Key Vault) så att cookies kan läsas över fall.
Antiförfalskning i MVC, Razor sidor och minimala API:er
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: dekorera åtgärder med
[ValidateAntiForgeryToken]och användning@Html.AntiForgeryToken()i former. - Razor-sidor: aktiverat som standard på formulärposter; använd
asp-antiforgery="true"vid behov. - Minimalt: validera via
IAntiforgeryenligt vad som visas.
Session med Redis och glidande vs absolut utgångsdatum
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();
Spara endast små, komprimerade data. Persist real carts/orders till en DB.
IMemoryCache med inträdesalternativ och vräkning 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
}));
});
I DistributedCache få-eller-set med jitter för att undvika stämplade
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);
});
För robust låsning föredrar Redis primitiva (SET NX EX) via StackExchange.Redis.
Utfärda och validera JWT lokalt (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");
Villkorliga uppdateringar med 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: komplexa objekt 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 Convergency 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);
}
Uppfriskande anspråk genom att återutfärda auth-cookien
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();
});
Detta bör täcka luckorna: starkare säkerhetsbrister, flernodes beredskap och verkliga mönster för cachelagringar, polletter och villkorliga förfrågningar.
Djupdykning: HttpContext.Items (Praktiska mönster och hjälpare)
Items is a per-request bag (IDictionary < object, object?>) that lives only for the lifes of a one request. Det är perfekt för att passera beräknade värden från mellanprodukter/filter till dina slutpunkter, styrenheter och Razor Pages-hanterare utan att röra globala tillstånd eller långlivade butiker.
- Livscykel: skapas på begäran start; kastas när svaret avslutas.
- Räckvidd: Endast aktuell begäran – korsar aldrig omdirigeringar eller bakgrundsarbete.
- Prestanda: O(1) lookups; perfekt för per-request caching.
- Säkerhet: endast serversidan, inte synlig för kunden.
Varför saker istället för
- Session/TempData: Dessa korsförfrågningar och problem med distributionen. Objekten är efemära och skalvänliga.
- DI Scoped tjänster: Använd dessa för beteende och delade beroenden. Objekt är bättre för ad-hoc, beräknade värden (tenant, user locale, funktionsflaggor) och per-request caches.
- HttpContext.Funktioner: För ram/transportnivåfunktioner (IEndpointFeature, IHtpUpgradeFeature). Objekt är för app-nivådata.
Undvik nyckelkollisioner: starkt typade tangenter
Eftersom objekt använder objekttangenter, föredrar privata statiska objekttangenter eller en dedikerad nyckeltyp för att undvika namn kollisioner.
public static class ItemKeys
{
public static readonly object TenantId = new();
public static readonly object UserLocale = new();
public static readonly object PerRequestCache = new();
}
Eller skapa ett typat omslag med tillägg:
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;
}
}
Mönster: Beräkna i mellanprogram, konsumera i slutpunkter / kontroller / sidor
// 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) });
Mönster: Per-request cache för att undvika upprepat arbete
Använd objekt som en liten cache så upprepade avläsningar inom samma begäran inte återhitta databaser / tjänster.
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);
});
Anmärkningar:
- Trådning: En enda begäran utförs vanligtvis på en logisk sökväg; Objekt är inte trådsäkra för parallella skriver. Om du startar parallella uppgifter som delar objekt, lägg till din egen synkronisering.
- Storlek: Hålla värden små och billiga att beräkna / serialize. It på minnet per begäran.
Mönster: Filter som befolkar objekt (MVC/Razor Pages)
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>());
Mönster: Rika loggar utan tilldelningar överallt
Beräkna en gång, sedan läsa i loggning omfattningar eller mellanprogram.
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);
}
});
När ska objekt inte användas?
- Data som behövs efter omdirigering eller över förfrågningar (använd TempData/Session/DB istället).
- App-wide singletons eller kors-request caches (använd IMemoryCache/IDistributedCache).
- Värden som hör hemma i identitet/auktorisation (använd anspråk/politik).
Snabb regel: Om det är beräknat under denna begäran och läsa inom denna begäran av din egen kod, Objekt är perfekt.