PÄIVITYS (2025-11-10): Lisätty lisää käytännön esimerkkejä todellisesta koodipohjastani, jossa näkyy reaalimaailman malleja, vaihtokauppoja, getcha-malleja ja kehitystä yksinkertaisista ja kehittyneistä valtionhallinnon lähestymistavoista. Sisältää yksityiskohtaisia IMemoryCache-malleja, ViewBagin käyttöä (hyvää ja huonoa), ResponseCache-/OutputCache-strategioita sekä tuotannosta saatuja opetuksia.
HTTP on tunnetusti kansalaisuudeton. Sovelluksesi... ei ole. Käyttäjät kirjautuvat sisään, lisäävät tuotteita koreihin, hyppäävät sivujen väliin, palaavat huomenna ja odottavat, että muistat. ASP.NET Coressa (MVC, Razor Pages, Minimal API) on monia tapoja säilyttää ja siirtää tilaa pyyntöjen välillä. Osa on vain pyyntöjä. Osa on viimeisiä istuntoja varten. Osa elää asiakkaalle. Osa on jaettu ja selviää palvelimen uudelleenkäynnistämisestä. Jokaisessa valinnassa on vaihtokauppoja turvallisuuden, suorituskyvyn, mittakaavan ja kehittäjäergonomian välillä.
Tämä viesti luetteloi vaihtoehdot, näyttää konkreettisia, kopioitavissa olevia esimerkkejä kaikissa kolmessa pinossa ja antaa sinulle päätöksenteko-ohjeistusta, jotta voit valita oikean työkalun tehtävään.
HUOMAUTUS: Tämä on osa kokeilujani tekoälyllä (avustettu piirtäminen) + omalla muokkauksellani. Sama ääni, sama käytännönläheisyys, vain nopeammat sormet.
flowchart LR
subgraph Client
Q[Query String]
R[Route Values]
H[Headers]
F[Form/Hidden Fields]
CK[Cookies]
LS[Local/Session Storage]
end
subgraph Server
I[HttpContext.Items<br/>\nper request only]
TD[TempData<br/>\none redirect]
SS[Session]
MC[IMemoryCache]
DC[IDistributedCache]
AU[Auth Cookie / Claims]
JT[JWT / Bearer]
DB[Database / Durable Store]
BUS[Outbox / Queue]
end
Q --> Model[Model Binding]
R --> Model
F --> Model
H --> Model
CK --> App[Your Code]
Model --> App
App -->|Set| CK
App -->|Set| TD
App -->|Set| SS
App -->|Set| MC
App -->|Set| DC
App -->|Issue| AU
App -->|Issue| JT
App -->|Persist| DB
sequenceDiagram participant M as Middleware participant E as Endpoint/Controller M->>M: Compute TenantId M->>E: HttpContext.Items["TenantId"] = 42 E->>E: Read Items["TenantId"]
Middlewaren esimerkki (kaikki pinot):
app.Use(async (context, next) =>
{
var tenantId = context.Request.Headers["X-TenantId"].FirstOrDefault() ?? "public";
context.Items["TenantId"] = tenantId;
await next(context);
});
app.MapGet("/whoami", (HttpContext ctx) => new { Tenant = ctx.Items["TenantId"] });
public IActionResult WhoAmI() => Json(new { Tenant = HttpContext.Items["TenantId"] });
public IActionResult OnGet() => new JsonResult(new { Tenant = HttpContext.Items["TenantId"] });
Esimerkkejä
app.MapGet("/orders/{id:int}", (int id, int? page) => Results.Ok(new { id, page }));
// GET /orders/5?page=2
[HttpGet("/orders/{id:int}")]
public IActionResult Details(int id, int? page)
=> View(new { id, page });
public IActionResult OnGet(int id, int? page)
=> Page();
Luovat yhteyksiä, jotka säilyttävät tilan:
// Razor Pages
<a asp-page="/Orders/Details" asp-route-id="@Model.Id" asp-route-page="@Model.Page">Next</a>
// MVC
@Html.ActionLink("Next", "Details", "Orders", new { id = Model.Id, page = Model.Page }, null)
app.Use(async (ctx, next) =>
{
var correlationId = ctx.Request.Headers["X-Correlation-Id"].FirstOrDefault()
?? Guid.NewGuid().ToString("n");
ctx.Response.Headers["X-Correlation-Id"] = correlationId;
await next(ctx);
});
Impotenssi (malli):
flowchart TD
C[Client POST /pay\nIdempotency-Key:k] --> S{Seen k?}
S -- No --> E[Execute charge]
E --> P[Persist result by k]
P --> R[Return 200 + result]
S -- Yes --> L[Load result by k]
L --> R
PRG-kuvio MVC/Razor-sivuilla:
sequenceDiagram participant U as User participant P as POST Action participant R as Redirect participant G as GET Action U->>P: POST form P-->>R: 302 Redirect U->>G: GET redirected G-->>U: Final page (no resubmits)
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Save(SettingsModel model)
{
// validate & persist
return RedirectToAction(nameof(Summary), new { tab = model.SelectedTab });
}
public IActionResult OnPost(SettingsModel model)
{
return RedirectToPage("/Settings/Summary", new { tab = model.SelectedTab });
}
Minimaalinen API-esimerkki:
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-sivujen käyttö on samanlaista kautta HttpContext.
Eheyden/luottamuksellisuuden vuoksi käytä ASP.NET Core Data Protection -järjestelmää suojaamaan itse evästeisiin laittamiasi hyötymääriä.
Asetukset (ohjelmanumerot):
builder.Services.AddControllersWithViews().AddSessionStateTempDataProvider(); // optional
builder.Services.AddSession();
var app = builder.Build();
app.UseSession();
MVC-ohjaimessa:
TempData["StatusMessage"] = "Saved!";
return RedirectToAction("Index");
Razor-sivun käsittelijä:
TempData["StatusMessage"] = "Saved!";
return RedirectToPage("/Index");
Näkökulmassa/sivulla:
@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]
Määrittele:
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();
Käyttämällä istuntoa (mitä tahansa pinoa):
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);
});
Istunnon auttajat:
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;
}
Työskentelin kerran valtavassa Britannian hallituksen IT-projektissa, jossa sessiovaltion väärinkäytöstä (muun muassa monista muista arkkitehtonisista synneistä) tuli suoritusta tappava pullonkaula. kaikki Sessioon: käyttäjän mieltymykset, monivaiheiset tiedot, hakutulokset, väliaikaiset laskelmat, jopa välimuistiin sijoitetut lookupsit, joiden olisi pitänyt olla oikeassa välimuistissa tai tietokannassa.
Ongelma: Session tila tallennettiin in-process (ASP.NET-istuntotila webissä.config, tämä oli ennen Core-päivää). Jokaisen pyynnön piti poistaa massiiviset istuntokohteet. Kuorman lisääntyessä istuntotila pallotti kymmeniä megatavuja käyttäjää kohti. Tuhansia rinnakkaisia käyttäjiä varten palvelimet loppuivat muistista.
Epätoivoinen ratkaisu: Lensimme HP:n laitokseen Stuttgartiin tekemään kuormakokeita heidän Superdomelleen. Euroopan tehokkain Windows-koneSe oli peto: kymmenittäin Itanium-prosessoria, satoja gigatavun RAM-muistia. Ajatuksena oli todistaa, että kun laitteita on tarpeeksi, järjestelmä voi täyttää vaatimukset.
Seurauksena oli: Edes Superdomessa emme osuneet vaadittuihin rinnakkaisiin käyttäjäkohteisiin. Istunnon tilaarkkitehtuuri oli perusteellisesti rikki. Vertikaaliskaalaus ei voinut säästää huonoa suunnittelua. Session serialisointi/deserialisointi yläpuolella yhdistettynä massiivisten session-objektien muistipaineeseen tarkoitti, että järjestelmä ei yksinkertaisesti pystynyt skaalautumaan – ei kohtuullisin kustannuksin.
Turvallisuuspainajainen: Pahempi kuin suoritusongelmat, huomasimme koodausvirheen, joka aiheutti istunnon tilan käyttäjien välinen vuoto. Käyttäjä A:n istuntotiedot näkyivät ajoittain Käyttäjä B:n istunnossa. Tämä ei ollut vain kiusallista, vaan se oli katastrofaalista. NHS:n henkilöstö Olimme vahingossa luoneet tietosuojaloukkausmekanismin, joka voisi paljastaa arkaluonteista lääketieteellistä tietoa eri terveydenhuollon ammattilaisten istunnoissa.
Mitä olisi pitänyt tapahtua:
Opetus: Istunnon tila ei skaalaudu vaakatasossa (jopa tahmeissa istunnoissa tai jaetuissa myymälöissä tehdään yhä sarja-/deserialisointia jokaisesta pyynnöstä). Superdome-kokeilu osoitti, että laitteistojen heittäminen arkkitehtonisiin ongelmiin on kallista ja usein turhaa.
Mutta mikä tärkeintä: Session Staten bugeista tulee turvallisuushaavoittuvuuksia. Kiertoturvallisuusongelmat, rotuolosuhteet, virheelliset istuntotunnuksen käsittelyt – nämä eivät vain aiheuta suoritusongelmia, ne voivat vuotaa arkaluontoisia tietoja käyttäjien välillä. Terveydenhuollon yhteydessä (tai pankkialalla tai millä tahansa säännellyllä toimialalla) kyseessä on sääntöjen noudattamista koskeva painajainen ja mahdollinen rikosoikeudellinen vastuu.
Nykyaikaiset neuvot:
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);
});
IDivedCache (esim. 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-pattern-vastaista varoitusta: jos sen on oltava kestävä tai arvovaltainen, tallenna se tietokantaan ja piilota se vapaaehtoisesti.
Tautien kesannointi (yleisin):
Lukeminen (kirjaston/toimittajan kautta): välimuisti käsittelee missien lataamista.
Write-through: Kirjoitukset menevät välimuistiin ja tukevat myymälää synkronisesti.
Write-byed: kirjoita välimuistiin, huuhtele säilyttääksesi epäyhtenäisesti (riski: menetys/epäyhtenäisyys).
Virkistä edessä: Päivitä kuumia avaimia ennen kuin ne päättyvät, jotta vältyt kylmältä hukkaukselta.
Kun sinun täytyy mitätöidä monet asiaan liittyvät merkinnät:
Versioidut etuliitteet (pehmeä invalidaatio): paina globaali versio pienellä avaimella ja sävelnä näppäimillä.
// version key: "v:products"; keys like $"{version}:product:{id}"
var version = await cache.GetStringAsync("v:products") ?? "1";
var key = $"{version}:product:{id}";
Kaikkien tuotteiden mitätöinti: ingress v:products (asiakkailta jää luonnollisesti huomaamatta vanhat ennakkokiinnitetyt avaimet).
Ryhmäkohtainen merkintä (Redis): Pidä avainten sarja tagia kohti; mitätöinnin yhteydessä, hae jäseniä ja poista.
// 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 invalidaatio: julkaise "invaliddate:key"-viesti; jokainen solmu poistaa avaimen paikallisesta IMemoryCachesta.
Skannaa kuvioilla: SCAN/KEYS-järjestelmää tulisi välttää prod-kuumilla poluilla; okei pienten näppäinten hallintatyökaluihin.
T GetOrAdd<T>(IMemoryCache cache, string key, Func<ICacheEntry, T> factory)
=> cache.GetOrCreate(key, e =>
{
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
e.SlidingExpiration = TimeSpan.FromMinutes(2);
e.Priority = CacheItemPriority.Normal;
e.Size = 1;
return factory(e);
});
static async Task<T?> GetOrSetJsonAsync<T>(IDistributedCache cache, string key, Func<Task<T>> factory, TimeSpan ttl)
{
var json = await cache.GetStringAsync(key);
if (json is not null)
return System.Text.Json.JsonSerializer.Deserialize<T>(json);
var value = await factory();
var opts = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = ttl };
await cache.SetStringAsync(key,
System.Text.Json.JsonSerializer.Serialize(value),
opts);
return value;
}
Näin itse asiassa käytän IMemoryCachea blogialustallani, joka on kehittynyt yrityksen ja virheiden kautta. Näytän sinulle kolme todellista mallia yksinkertaisesta hienostuneeseen.
Tämä oli ensimmäinen välimuistitoteutukseni. Blogikategoriat eivät muutu usein, joten jätä ne 30 minuutiksi:
// 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;
}
Miksi tämä toimii:
Sain osuman: Aluksi käytin merkkijononäppäintä "Categories". Toimii hyvin, kunnes sinulla on useita ohjaimia ja yksi käyttää vahingossa uudelleen samaa avainta. Nyt käytän vakioita tai vahvasti kirjoitettuja avaimia (ks. aiempi kohta törmäysten välttämisestä).
Tämä välimuistin käännöstila käyttäjää kohden on monimutkaisempi, koska se tarvitsee rajoja ja sen pitäisi pysyä elossa niin kauan kuin käyttäjä on aktiivinen:
// 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)
});
}
}
Miksi tämä on erilaista:
Virhe, jonka tein: Aluksi en rajoittanut tehtävien määrää. Virrankäyttäjä laukaisi 50+-käännöksen ja minulla oli muistivuoto. Nyt minulla on enintään 5 käyttäjää kohden.
Kaupankäynti: Tämä ei skaalaudu miljooniin käyttäjiin. Jos tästä tulee ongelma, siirryn IDiveddedCacheen (Redis) tai tallentaisin tietokantaan hakemiston käyttäjätunnuksesta + startTime.
Tässä välimuistissa analytiikka mittaa ja seuraa välimuistin tehoa Serilog-jäljityksen avulla:
// 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;
}
}
Mikä tekee tästä tuotantovalmista:
yyyyMMdd Muodosta avain niin eri aikoina samana päivänä jaa välimuistiEvoluutio: Aluksi kätkin 10 minuuttia. Mutta Umamimetrit ovat raskaita noudettavaksi, eivätkä ne juurikaan muutu, joten tunti on hyvä. Huomasin tämän katsomalla SerilogTracingin dataa ja näkemällä, että välimuistissa on liikaa aukkoja.
Toiminnallinen seuranta: Seqissä (log aggregator) voin kysyä:
ActivityName = "GetMetricsWithPrefix" and CacheHit = false
Jos se on korkea, säädän TTL:tä tai avainstrategiaa.
Mallin käyttö Tapauksen vanheneminen Kokokontrolli Havainnointi |---------|----------|------------|--------------|---------------| | Luokat Maailmanlaajuista, harvoin muuttuvaa 30 minuuttia ehdotonta Ei tarvita (pientä) Perushakkuuta | Käännöstehtävät Per-käyttäjä, rajoitettu 6h absoluuttinen + 1h liukuva käsikirja (5 kappaletta max) Ei yhtään (pitäisi lisätä!) | Metriikka Kallis ulkoiset puhelut 1 tunti ehdoton luonnollinen (aikaikkuna) Täysi jäljitys
Tärkeimmät opetukset:
Kun en käytä IMemoryCachea:
Aseta evästeauth:
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();
Kirjautuminen väitteiden kanssa (MVC/minimaali):
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("/");
});
Lue väitteet (mitä tahansa pinoa):
[Authorize]
app.MapGet("/me", (ClaimsPrincipal user)
=> Results.Ok(new { user.Identity!.Name, Roles = user.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value) }));
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", o =>
{
o.Authority = "https://demo.identityserver.io"; // example
o.Audience = "api";
o.RequireHttpsMetadata = true;
});
Käyttö:
[Authorize(AuthenticationSchemes = "Bearer")]
app.MapGet("/secure", () => "ok");
Merenneidon yleiskatsaus:
sequenceDiagram participant C as Client participant STS as Token Service participant API as API C->>STS: Authenticate (username/password) STS-->>C: JWT (signed) C->>API: GET /secure (Authorization: Bearer <jwt>) API->>API: Validate signature, expiry, audience API-->>C: 200
EF Core -luonnos:
builder.Services.AddDbContext<AppDb>(o => o.UseSqlServer(cs));
app.MapPost("/cart/items", async (AppDb db, AddItem cmd) =>
{
var cart = await db.Carts.FindAsync(cmd.CartId) ?? new Cart(cmd.CartId);
cart.Add(cmd.ProductId, cmd.Qty);
await db.SaveChangesAsync();
return Results.Created($"/cart/{cart.Id}", cart);
});
builder.Services.AddResponseCaching();
var app = builder.Build();
app.UseResponseCaching();
app.MapGet("/products", (HttpContext ctx) =>
{
ctx.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue { Public = true, MaxAge = TimeSpan.FromSeconds(30) };
return Results.Ok(new[] { new { Id = 1, Name = "Widget" } });
}).CacheOutput();
ETag-esimerkki:
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");
});
Käytän sekä ResponseCachea että OutputCachea yhdessä blogissani eri tarkoituksiin. Tästä syystä saatat haluta molemmat ja miten ne eroavat toisistaan.
ASP.NET Core on kaksi Samannäköiset välimuistijärjestelmät:
Cache-Control, Vary) kertoo selaimille ja CDN: lle, miten välimuisti välitetään
eNe täydentävät toisiaan. ResponseCache käsittelee asiakkaan/CDN:n välimuistia, OutputCache käsittelee palvelimen sivuvälimuistia.
// 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);
}
Mitä tapahtuu, kun joku pyytää /blog/my-post:
OutputCache-tarkistukset ensin: Onko minulla välimuistin vastaus my-post + en kieltä?
ResponseCache asettaa otsikot: Kun OutputCache tuottaa vastauksen, ResponseCache lisää:
Cache-Control: public, max-age=300
Vary: hx-request
Selain välimuistiin: Browser välimuistit vastaus 300 sekuntia (5 minuuttia). Seuraavat pyynnöt samalta käyttäjältä eivät edes osu palvelimeen.
CDN-välimuisti (jos käytät Cloudflarea/Fastilya): CDN:n välimuistit 5 minuutin ajan. Käyttäjät ympäri maailmaa osuivat CDN:ään, eivät minun palvelimeeni.
[ResponseCache(Duration = 300)] // 5 minutes client/CDN cache
[OutputCache(Duration = 3600)] // 1 hour server cache
Perustelut:
Evoluutio: Aluksi minulla oli molemmat viisi minuuttia, mutta se tarkoitti, että palvelimeni reputti uudelleen viiden minuutin välein, vaikka sisältö harvoin muuttuu. Nyt:
VaryByHeader = "hx-request" // ResponseCache
VaryByHeaderNames = new[] { "hx-request" } // OutputCache
Miksi tällä on merkitystä: HTMX-pyyntöihin kuuluu hx-request: true Vastaan eri kysymyksiin:
Ilman VaryByVälimuisti palauttaisi väärän formaatin. VaryByVälitän kaksi versiota jokaisesta sivusta.
Esimerkki:
User requests /blog/my-post → Cache key: "blog/my-post:en:hx=false" → Full HTML cached
HTMX requests /blog/my-post → Cache key: "blog/my-post:en:hx=true" → Partial HTML cached
[ResponseCache(VaryByQueryKeys = new[] { nameof(slug), nameof(language) })]
[OutputCache(VaryByQueryKeys = new[] { nameof(slug), nameof(language) })]
Ongelma ilman tätä: /blog/my-post?language=fr palvelisi englanninkielistä välimuistiversiota.
VaryByQuery Keysin kanssa: Erilliset välimuistien tietueet:
/blog/my-post?language=en → Cache-avain sisältää "en"/blog/my-post?language=fr → Cache-avain sisältää "fr"Todellinen käyttö blogilistaltani:
[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, /* ... */)
Häkkiräjähdysvaroitus: Jokainen ainutlaatuinen parametriyhdistelmä = erillinen välimuistimerkintä:
page=1&pageSize=20&language=en → Yksi merkintäpage=2&pageSize=20&language=en → Toinen merkintäpage=1&pageSize=10&language=en → Toinen merkintäVähennys:
ResponseCache toimii ulos laatikosta, mutta OutputCache tarvitset setupin:
// Program.cs
builder.Services.AddOutputCache(options =>
{
options.MaximumBodySize = 64 * 1024 * 1024; // 64 MB max response size
options.SizeLimit = 100 * 1024 * 1024; // 100 MB total cache size
});
var app = builder.Build();
app.UseOutputCache(); // Must be in middleware pipeline
OutputCache jätetään väliin:
Set-Cookie OtsikkoCache-Control: no-storeEsimerkki siitä, missä en käytä sitä:
// 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
}
Käytän Prometheus-metriikkaa (valotettu sovelluksen kautta) seuraamaan:
// Pseudo-code for metrics
cache_hits_total{cache="output"} 45230
cache_misses_total{cache="output"} 892
Välimuistini osumaprosentti: ~98% blogikirjoituksista (suurin osa liikenteestä osuu toistuvasti samaan suosittuun postaukseen).
Vaikutus:
Joskus teen vikoja ja tarvitsen aina uusia vastauksia:
// During development, comment out caching
// [ResponseCache(Duration = 300, ...)]
// [OutputCache(Duration = 3600, ...)]
public async Task<IActionResult> Show(string slug, string language = "en")
Tai käytä ympäristökohtaisia asetuksia:
#if DEBUG
// No caching in development
#else
[ResponseCache(Duration = 300, ...)]
[OutputCache(Duration = 3600, ...)]
#endif
Parempaa lähestymistapaa: Käytä asetuksia:
[ResponseCache(Duration = responseCacheDuration, ...)]
jossa responseCacheDuration On 0 kehitysvaiheessa, 300 tuotannossa.
Plussat:
Miinukset:
Kun keskeytin välienselvittelyn:
Tuomioni: Lähinnä staattista sisältöä ja korkeaa luku- ja kirjoitussuhdetta omaavalle blogille kaksoisvälimuisti on valtava voitto. En käyttäisi tätä admin-paneeleissa tai kojetauluissa, joissa on nopeasti muuttuvaa dataa.
Nämä kolme ovat usein hämmentyneitä. Näin ne eroavat toisistaan ja mitä oikeasti käytän tuotannossa.
// 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");
Aihe: ViewData ViewBag TempData
| --------- | ---------- | --------- | ---------- |
|---|---|---|---|
| Elinikä Nykyinen pyyntö Tämänhetkinen pyyntö Yksi uudelleenohjaus | |||
| Avainkäyttö String-avaimet Kiinteistön syntaksi String-avaimet | |||
| Tyyppiturvallisuus Ei yhtään (tarpeellista) Ei yhtään (dynaamista) Ei yhtään (tarpeellista) | |||
| Kokooma-ajan tarkistus Nro n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o | |||
| Selviytyjät suuntaavat Nro n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o n:o |
Käytän blogissani ViewBagia ainoastaan tietojen siirtämiseen ohjaimista jaettuun asetteluun (analyyttiset analyysit, kategoriat jne.):
// 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);
}
Sitten asettelussani (_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>
}
Miksi tämä malli toimii:
Erehdyin aikaisessa vaiheessa: Minä vain asettelin. ViewBag.Categories Jokaisessa toimintamenetelmässä. DRY-rikkomus ja helppo unohtaa. OnActionExecutionAsync tukikohdassa ohjain ratkaisi tämän.
// 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);
}
Kun otetaan huomioon seuraavat seikat:
@{
ViewData["Title"] = ViewBag.Title; // Standard MVC convention for <title>
}
<h1>Category: @ViewBag.Category</h1>
Tämä ei haittaa, koska:
Title sekä Category ominaisuuksia jokaiseen näkymämalliinMallinvastaisuus:
// 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 };
Ongelmia:
ViewBag.Usr epäonnistuu ajon aikana)Parempi: Vahvasti kirjoitetut katselumallit:
// 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);
}
Standardi TempData-käyttölaukku:
[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();
}
Miksi en käytä TempDataa blogissani:
[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!"
});
}
Kun TempDatassa on järkeä:
TempData sai sinut: Oletuksena evästeiden tukemana (ASP.NET Core 2.0:sta lähtien). Jos laitat suuria esineitä TempDataan, paisutat jokaisen pyynnön mukana lähetettyä evästettä. Suureen tilaan, käytä Sessionia tukivaraston tai DB:n kanssa.
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]
Minun sääntöni:
Kumpaa osavaltiota tulee käyttää?
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
Esimerkki (DB + reittiavain):
app.MapPost("/wizard/{id}", async (AppDb db, Guid id, StepInput input) =>
{
var flow = await db.Flows.FindAsync(id) ?? new Flow(id);
flow.Apply(input);
await db.SaveChangesAsync();
return Results.Redirect($"/wizard/{id}/next");
});
TempData["Flash"] = "Profile saved";
return RedirectToAction("Index");
Razor-näkymä:
@if (TempData["Flash"] is string flash) {
<div class="alert alert-info">@flash</div>
}
flowchart LR U[User] -- cart-id cookie --> S[Server] S --> DB[(Cart Table)] S <--> Cache[Distributed Cache]
Secure, HttpOnly, SameSite, IsEssential (jos suostumus/toimintatarve sitä edellyttää).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
}
Pikavalinnat:
Kaikki kolme pinoa istuvat samoilla primitiivisillä (HttpContext, mallisidonta, autaalinen, tietosuoja). Yllä olevat esimerkit osoittavat, että sovellusrajapinnat eroavat toisistaan lähinnä ergonomiassa:
Results.*.Heillä kaikilla on samat valtion mekanismit, joista täällä keskustellaan.
Näytettyäni sinulle kaikki nämä vaihtoehdot, tässä on rehellinen arvioni siitä, mikä toimii bloggausalustani tuotannossa.
Mekanismi Taajuus Käytä Tapauksia Tyytyväisyys |-----------|-----------|-----------|--------------| | ImemoryCache Erittäin korkeat kategoriat, metriikka, käännöstehtävät | OutputCache Renderöidyt blogikirjoitukset, listat Huge perf -voitosta | ResponseCache "High HTTP:n välilyöntejä" "Työskentelyä OutputCachen kanssa" | NäytäBag Medium Analyticsin asetukset, sivujen otsikot OK yksinkertaiselle jutulle | Auth väittää Keskikokoinen käyttäjätunnus, hallintolippu Oikea työkalu | Reitti/kysyntä Keskikokoinen Paginaatio, suodatus, luodit Valtiottomat ja kytköksissä olevat | Tietokanta Keskikokoinen blogikirjoitukset, kommentit, valtion totuuden lähde | Evästeet Alhaiset käyttäjän mieltymykset (tulevaisuus) Ei ole vielä tarvittu | Istunto Ei koskaan - Ei jaettuja kauppoja | TempData HTMX ei koskaan poista tarvetta | HttpContext.Itse asiassa Ei koskaan - Ei ole ollut käyttökelpoista tapausta | JaettuCache Ei koskaan - Yksi ainoa palvelin (tällä hetkellä)
Mihin käytän sitä:
Käytännössä ammattilaiset:
Miinukset, joihin olen törmännyt:
Kallistusraja: Jos pääsen useille palvelimille, tarvitsen jaetun tilan IDivedCache (Redis) -palvelun. Tällä hetkellä yksi palvelin + muistivälimuisti on täydellinen.
Mihin käytän sitä:
Käytännössä ammattilaiset:
Miinukset, joihin olen törmännyt:
Paras: Lukevat sovellukset, joissa on enimmäkseen staattista sisältöä. Ne eivät ole hyväksi henkilökohtaiselle tai reaaliaikaiselle datalle.
Mihin käytän sitä:
Käytännössä ammattilaiset:
Miinukset, joihin olen törmännyt:
Noudatan seuraavaa sääntöä: ViewBag vain yksinkertaisille scalareille. Monimutkaiset esineet menevät ViewModelsiin.
Mihin käytän sitä:
sub Vaatimus konfigurointia vastaan)Käytännössä ammattilaiset:
User.Claims kaikkiallaMiinukset, joihin olen törmännyt:
Parhaita käytäntöjä: Pidä vaateet pieninä ja vakaina. Älä laita usein muuttuvia tietoja väitteisiin.
Istunto (ei koskaan käytetty):
TempData (ei koskaan käytetty):
HttpContext.Itsejä (ei koskaan käytetty):
IdistributedCache (ei koskaan käytetty):
Vaihe 1 (alkuvaihe): Jokainen pyyntö osui tietokantaan ja teki Markdownin.
Vaihe 2 (ensimmäinen optimointi): Lisätty IMemoryCache kategorioihin. Näin välittömän DB-kuorman vähenemisen. Se pysyi yksinkertaisena: 30 minuuttia TTL, ei hienostunutta logiikkaa.
Vaihe 3 (skaalautuminen): Lisätty OutputCache-blogikirjoituksiin, kun liikenne vilkastui. Voimakas suoritusparannus. Alkuperäinen virhe: välimuistissa vain 5 minuuttia. Lisättynä tuntiin seurannan jälkeen näytti sisällön muuttuvan harvoin.
Vaihe 4 (havaittavuus): Lisätty SerilogTraking to metrics välimuistiin. Löydetyt välimuistin virheet olivat suuria, koska päivämäärämuotoiltiin avaimissa. yyyyMMdd Hittiprosentti nousi 60 prosentista 95 prosenttiin.
Vaihe 5 (HTMX-integraatio): Lisätty VaryByHeader korvataan hx-requestAluksi tämä unohtui ja tarjoili kokonaisia sivuja HTMX:n pyynnöille.
Nykytila: Tyytyväinen pinoon. IMemoryCache + OutputCache + ResponseCache hoitaa 98 % valtionhallinnon tarpeistani. Tietokanta kestävän tilan varalle. Auth vaatii identiteettiä.
Aloita tästä:
Lisää tarvittaessa: 6. Istunto (vain, jos sinulla täytyy olla palvelimen puoleinen keskustelutila) 7. IDivedCache (vain kun skaalaudut useille palvelimille) 8. Evästeet (asiakaspuoliset mieltymykset, suostumus)
Vältä:
Tärkeimmät opetukset:
Valtio verkkosovelluksissa ei sovi kaikille. Valitse kevyin vaihtoehto, joka vastaa tarpeitasi, suosi kansalaisuudettomia kuvioita, kun voit, ja kerro selkeästi turvallisuudesta ja elinkaaresta.
Jos haluat syventyä siihen, miten nämä palaset virtaavat putken läpi, katso sarjani alusta Osa 1: Yleiskatsaus ja säätiö ja erityisesti väliohjelmiston ja reititysosien.
Iloinen rakennus.
Nämä esimerkit syventävät aiempia jaksoja tuotantoluokan yksityiskohdilla, jotka voit liittää net9 minimaalisiin malleihin, MVC:hen tai Razor-sivuihin.
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();
});
Vihje: Monisolmuisissa käyttökohteissa pysyttele tietosuoja-avaimissa (esim. jaettuun tiedostojärjestelmään, Redisiin tai Azure Key Vaulliin), jotta evästeet ovat luettavissa kaikissa tapauksissa.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddAntiforgery(o => o.HeaderName = "X-CSRF-TOKEN");
var app = builder.Build();
app.MapGet("/antiforgery/token", (IAntiforgery af, HttpContext ctx) =>
{
var tokens = af.GetAndStoreTokens(ctx);
return Results.Json(new { token = tokens.RequestToken });
});
app.MapPost("/submit", (HttpContext ctx) => Results.Ok("posted"))
.AddEndpointFilter(async (efiContext, next) =>
{
var af = efiContext.HttpContext.RequestServices.GetRequiredService<IAntiforgery>();
await af.ValidateRequestAsync(efiContext.HttpContext);
return await next(efiContext);
});
app.MapControllers();
app.MapRazorPages();
[ValidateAntiForgeryToken] ja käyttö @Html.AntiForgeryToken() muodoissa.asp-antiforgery="true" tarvittaessa.IAntiforgery kuten on näytetty.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();
Säilytä vain pieniä, purkitettavissa olevia tietoja. Pidä kiinni oikeista kärryistä/tilauksista DB:hen.
builder.Services.AddMemoryCache();
app.MapGet("/fx/{pair}", (IMemoryCache cache, string pair) =>
{
var key = $"fx:{pair.ToLowerInvariant()}";
return Results.Ok(cache.GetOrCreate(key, entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
entry.SlidingExpiration = TimeSpan.FromMinutes(2);
entry.Size = 1; // enable size-based eviction if configured
entry.RegisterPostEvictionCallback((k, v, reason, state) =>
{
Console.WriteLine($"Evicted {k} because {reason}");
});
return 0.92m; // fetch from external service in real life
}));
});
builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");
app.MapGet("/feature/{name}", async (IDistributedCache cache, string name) =>
{
var key = $"feat:{name}";
var cached = await cache.GetStringAsync(key);
if (cached is not null) return Results.Text(cached);
// Lock key to prevent thundering herd (very simple approach)
var lockKey = key + ":lock";
var gotLock = await cache.SetStringAsync(lockKey, "1", new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5)
});
try
{
cached = await cache.GetStringAsync(key);
if (cached is null)
{
var computed = "on"; // expensive work
var rnd = Random.Shared.Next(0, 15); // jitter
await cache.SetStringAsync(key, computed, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5).Add(TimeSpan.FromSeconds(rnd))
});
cached = computed;
}
}
finally
{
await cache.RemoveAsync(lockKey);
}
return Results.Text(cached);
});
Vahvassa lukituksessa suositaan Redis alkeellisia (SET NX EX) StackExchange.Redisin kautta.
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("super-secret-key-please-rotate"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
IssuerSigningKey = key,
ValidateIssuerSigningKey = true,
ValidateLifetime = true
};
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapPost("/token", () =>
{
var claims = new[] { new Claim(ClaimTypes.Name, "alice") };
var jwt = new JwtSecurityToken(claims: claims, expires: DateTime.UtcNow.AddMinutes(30), signingCredentials: creds);
var token = new JwtSecurityTokenHandler().WriteToken(jwt);
return Results.Json(new { access_token = token });
});
app.MapGet("/who", [Microsoft.AspNetCore.Authorization.Authorize] () => "ok");
record Todo(int Id, string Title, string Version);
var store = new Dictionary<int, Todo> { [1] = new(1, "Ship", "v1") };
app.MapGet("/todo/{id:int}", (int id, HttpContext ctx) =>
{
if (!store.TryGetValue(id, out var t)) return Results.NotFound();
ctx.Response.Headers.ETag = t.Version;
return Results.Json(t);
});
app.MapPut("/todo/{id:int}", (int id, HttpContext ctx, Todo input) =>
{
if (!store.TryGetValue(id, out var current)) return Results.NotFound();
var ifMatch = ctx.Request.Headers["If-Match"].ToString();
if (string.IsNullOrEmpty(ifMatch) || ifMatch != current.Version)
return Results.StatusCode(StatusCodes.Status412PreconditionFailed);
var next = current with { Title = input.Title, Version = $"v{DateTime.UtcNow.Ticks}" };
store[id] = next;
ctx.Response.Headers.ETag = next.Version;
return Results.Ok(next);
});
public static class TempDataJsonExtensions
{
public static void Put<T>(this ITempDataDictionary tempData, string key, T value)
=> tempData[key] = System.Text.Json.JsonSerializer.Serialize(value);
public static T? Get<T>(this ITempDataDictionary tempData, string key)
=> tempData.TryGetValue(key, out var o) && o is string s
? System.Text.Json.JsonSerializer.Deserialize<T>(s)
: default;
}
// Usage in MVC action
TempData.Put("WizardState", new { Step = 2, Name = "Alice" });
var state = TempData.Get<dynamic>("WizardState");
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
[Timestamp] public byte[] RowVersion { get; set; } = default!;
}
// On update
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
return Results.StatusCode(StatusCodes.Status412PreconditionFailed);
}
app.MapPost("/promote", async (HttpContext ctx) =>
{
var u = ctx.User;
var claims = u.Claims.ToList();
claims.Add(new Claim(ClaimTypes.Role, "Editor"));
var id = new ClaimsIdentity(claims, "Cookies");
await ctx.SignInAsync("Cookies", new ClaimsPrincipal(id));
return Results.Ok();
});
Tämän pitäisi kattaa aukot: vahvemmat turvallisuusoletukset, monisolmuinen valmiustila ja reaalimaailman mallit kätköille, rahakkeille ja ehdollisille pyynnöille.
HttpContext.Items on pyyntöpussi (IDictionary<object, object?>), joka elää vain yhden pyynnön ajan. Se on täydellinen väliohjelmistojen/suodattimien arvojen siirtämiseen päätepisteisiin, ohjaimiin ja Razor Pages -käsittelijöihin koskematta maailmanlaajuiseen tilaan tai pitkäikäisiin kauppoihin.
Koska Kohteet käyttävät objektinäppäimiä, suosivat yksityiset staattiset objektiavaimet tai oma avaintyyppi välttääkseen nimikolarit.
public static class ItemKeys
{
public static readonly object TenantId = new();
public static readonly object UserLocale = new();
public static readonly object PerRequestCache = new();
}
Tai luo koneella kirjoitettu kääre, jossa on laajennukset:
public static class HttpContextItemsExtensions
{
public static void Set<T>(this HttpContext ctx, object key, T value)
=> ctx.Items[key] = value!;
public static T? Get<T>(this HttpContext ctx, object key)
=> ctx.Items.TryGetValue(key, out var v) ? (T?)v : default;
public static T GetOrCreate<T>(this HttpContext ctx, object key, Func<T> factory)
{
if (ctx.Items.TryGetValue(key, out var existing) && existing is T typed)
return typed;
var created = factory();
ctx.Items[key] = created!;
return created;
}
}
// Program.cs
app.Use(async (ctx, next) =>
{
var tenant = ctx.Request.Headers["X-TenantId"].FirstOrDefault() ?? "public";
ctx.Set(ItemKeys.TenantId, tenant); // using extension above
// Per-request cache holder (optional)
ctx.Set(ItemKeys.PerRequestCache, new Dictionary<string, object?>());
await next(ctx);
});
// Minimal API
app.MapGet("/whoami", (HttpContext ctx) => new
{
Tenant = ctx.Get<string>(ItemKeys.TenantId),
});
// MVC Controller
public IActionResult WhoAmI()
=> Json(new { Tenant = HttpContext.Get<string>(ItemKeys.TenantId) });
// Razor Page handler
public IActionResult OnGet()
=> new JsonResult(new { Tenant = HttpContext.Get<string>(ItemKeys.TenantId) });
Käytä kappaleita pienenä välimuistina, joka lukee samassa pyynnössä donât uudelleen osua tietokantoihin/palveluihin.
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);
});
Huomautukset:
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>());
Laske kerran, lue sitten hakuskaalaa tai keskiohjelmistoa.
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);
}
});
Nopea sääntö: Jos se laskee tämän pyynnön aikana ja lukee tämän pyynnön omalla koodilla, kohteet ovat ihanteellisia.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.