ASP.NET Core: Käytännöllinen, ei-nonsense-opas (MVC, Razor-sivut, minimaaliset rajapinnat) (Suomi (Finnish))

ASP.NET Core: Käytännöllinen, ei-nonsense-opas (MVC, Razor-sivut, minimaaliset rajapinnat)

Sunday, 09 November 2025

//

48 minute read

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.

Johdanto

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.

Maisema glögissä

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
  • Asiakkaat: kysely, reitti, otsikot, lomakkeet, evästeet, JWT. Skaalaa vaakatasossa, mutta näkyy asiakkaalle (täytyy tarvittaessa vahvistaa/allekirjoittaa/salata).
  • Palvelin: TempData, istunto, välimuistit, DB. Vaatii affiniteetti- tai jakelustrategiaa.
  • Esityspyyntö: HttpContext.Itsestään on hyötyä tietojen välittämisessä sisäisesti yhden pyynnön aikana.

Golden Rules (ennen kuin sukellamme API-rajapintoihin)

  1. Turvallisuus ensin: Valtion vuoto on todellinen uhka. Palvelimen sivutila (Session, in-muistettavat välimuistit) voi vuotaa käyttäjien välillä koodausvirheiden, kilpailuolosuhteiden tai väärän elinkaaren hallinnan kautta. Valtiottomat kuviot eivät ole vain skaalautuvia, vaan ne ovat varmempia.
  2. Suosi "stateless server" -malleja, kun sinun täytyy skaalata vaakatasossa (push state to clients, sustainable stores tai jaetut välimuistit).
  3. Älä koskaan luota asiakasohjattuun tilaan. Vahvista, allekirjoita ja/tai salaa.
  4. Pidä suuret tiedot poissa evästeistä ja otsikoista; ne paisuttavat jokaista pyyntöä.
  5. Käytä TempDataa vain jälki-redirect-Get (PRG) -lähetyksiin, kuten flash-viesteihin.
  6. Käytä istuntoa vain silloin, kun sinun täytyy ylläpitää palvelinpuolen keskustelutilaa ja olet suunnitellut jakelua - ja olet vainoharhainen turvallisuuden suhteen.
  7. Vaatimukset koskevat identiteettiä ja karkeaa hyväksyntää, eivät yleistä sovellustilaa.
  8. Cache ei ole totuuden lähde, vaan tukee sitä kestävällä kaupalla, jos tiedoilla on merkitystä.

Per-Request State: HttpContext.Itse asiassa

  • Soveltamisala: vain tämänhetkinen pyyntö (putkijohdon loppupäässä alaskytkentä)
  • Käyttö: väliohjelmistojen ja päätepisteiden/ohjaimien välittämien arvojen välittäminen
  • Skaala: ei vaikutusta
  • Turvallisuus: vain palvelin
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);
});
  • Minimaalinen API- päätetapahtuma:
app.MapGet("/whoami", (HttpContext ctx) => new { Tenant = ctx.Items["TenantId"] });
  • MVC-ohjain:
public IActionResult WhoAmI() => Json(new { Tenant = HttpContext.Items["TenantId"] });
  • Razor Page -käsittelijä:
public IActionResult OnGet() => new JsonResult(new { Tenant = HttpContext.Items["TenantId"] });

Ohimenevä asiakasliittovaltio: Reittiarvot ja Query String

  • Soveltamisala: nykyinen pyyntö; asiakas kantaa sen selkeästi.
  • Käyttö: navigointikonteksti, suodatus, paginointi, resurssi-identiteetti
  • Turvallisuus: on validoitava/valtuutettava, ei upoteta salaisuuksia

Esimerkkejä

  • Minimirajapinnat:
app.MapGet("/orders/{id:int}", (int id, int? page) => Results.Ok(new { id, page }));
// GET /orders/5?page=2
    • Ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei, ei.
[HttpGet("/orders/{id:int}")]
public IActionResult Details(int id, int? page)
  => View(new { id, page });
  • Razor-sivut (Tilaukset/Yksityiskohdat.cshtml.cs):
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)

Otsikko: Vastaavuustunnukset, impotenssiavaimet, ominaisuusliput

  • Soveltamisala: nykyinen pyyntö, vastausten valinnainen toistaminen
  • Käyttö: jäljitys, uudelleenkoeturvallisuus, A/B-liput
  • Turvallisuus: käsitellään epäluotettavana syötteenä, validoituna/valkoisena
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

Lomakkeet ja piilokentät (PRG)

  • Soveltamisala: vain seuraava pyyntö (tiedonantaja palauttaa arvot)
  • Käyttö: velhon askeleita, väärennösten vastaisia kuponkeja, pieniä valtionosia PRG:n alueella
  • Turvallisuus: aina voimassa; yhdistä väärennösten vastaiseen toimintaan

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

Evästeet: Pienet, signeeratut, Joskus salatut

  • Soveltamisala: jokainen pyyntö selaimelta loppuun asti
  • Käyttö: Asetukset, muut kuin arkaluonteiset liput, suostumus, auth cookies (erillinen osio)
  • Kompromissit: Kokorajojen (~4KB evästettä kohti), tulosvaikutusten, on oltava suostumuslakien mukaisia

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


TempData: Yhden suunnan viestibussi

  • Soveltamisala: selviää yhdestä uudelleenohjauksesta
  • Takaisinmyynti: Eväste (oletus) tai istunto
  • Käyttö: flash-viestit, validointitiivistelmät PRG:n jälkeen

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]

Istunto: Server-side keskustelutila

  • Soveltamisala: Selainsessio (evästeavain + palvelinvarasto)
  • Käyttö: monivaiheiset velhot, pienet kärrytiedot, trottauslaskurit
  • Kompromissit: vaatii tahmeita istuntoja tai jaettuja tukikauppoja, voi rajoittaa skaalaa

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

Varoittava tarina: Kun istunnosta tulee pullonkaula

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:

  1. Perusteeton oletuksena: Suurinta osaa siitä istuntodatasta ei olisi koskaan pitänyt olla olemassa
  2. Tietokanta kestävää tilaa varten: Monivaiheisen etenemisen olisi pitänyt olla tietokannassa työnkulkutunnuksilla
  3. Välipala etsimistä varten: Jaetut lookupit kuuluivat IMemoryCacheen tai jaettuun välimuistiin
  4. Asiakaspuoli mieltymyksille: Käyttäjäasetukset olisivat voineet olla evästeissä tai paikallisessa tallennuksessa
  5. Jaettu istunto tarvittaessa: Jos istunto olisi todella tarpeen, Redisin tukema istunto olisi jakanut kuorman

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:

  • Jos huomaat tarvitsevasi enemmän kuin muutaman KB-istunnon dataa, sinulla on todennäköisesti suunnitteluongelma
  • Jos tallennat arkaluontoisia tietoja istunnossa, luot turvahyökkäyspinnan
  • Valtiottomat arkkitehtuurit eivät vain skaalaudu paremmin, vaan ovat luonnostaan varmempia, koska palvelimen puolella ei ole vuototilaa
  • Harkitse uudelleen valtion johtamisstrategiaasi ennen kuin lennät Stuttgartiin (tai selitä tietomurto tiedotuskomissaarille)

Välimuisti: IMemoryCache ja IDivedCache

  • Soveltamisala: palvelinprosessi (IMemoryCache) tai jaettu (tunnistettuCache)
  • Käyttö: johdettu/laskettu data, lookups, lyhytaikainen tila
  • Vaihtokaupat: välimuistin mitätöinti, sarjanumero jaettavaksi

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.

IMemoryCachen ja DistributedCachen valinta

  • IMemoryCache:
    • Nopeat, prosessoivat esineet pysyvät esineinä (ei sarjanumerointia).
    • Eviointi muistipaineen, kokorajoituksen, absoluuttisen/liukuvan vanhenemisen ja prioriteettien avulla.
    • Ei jaeta solmujen välillä, selviää sovelluksen kierrätyksestä/käytöstä.
    • Hienot kyhmyihin, tietokoneisiin, lyhyisiin TTL-lähetyksiin.
  • IDivedCache (Redis/SQL/etc.):
    • Jaettuna maatilalla; selviää sovelluksesta uudelleen; vaatii sarjanumerointia (säikeet/tavut).
    • Hieman korkeampi latenssi; läpilyönti riippuu verkosta ja taustasta.
    • Tukee ehdotonta/liukumäkeä (tarjoajariippuvainen; Redis-toimittaja päivittää TTL:n liukumäkeä varten).
    • Ihanteellinen ristisolmukohdille, suurille fan-out-lukemille, lipuille ja istunnolle.

Yhteiset välimuististrategiat

  • Tautien kesannointi (yleisin):

    1. Kokeile välimuistia; 2) jos se ei toimi, lataa lähteestä; 3) kirjoita välimuistiin; 4) palaa.
    • Plussat: yksinkertainen; totuuden lähde on edelleen tietokanta.
    • Miinukset: ensimmäinen pyyntö vanhenemisen jälkeen on hidas; mahdolliset leimat.
  • 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.

Vanhentuminen, häätö ja kokoaminen

  • Absoluuttinen vanheneminen: vanhenee aina tietyn ajan kuluttua (hyvää ulkoisille tiedoille tuoreuden kannalta).
  • Liukkauden päättyminen: pidentää TTL:ää pääsystä (hyvää istuntoja ja käyttäjäkohtaisia tietoja varten).
  • Kokopohjainen häätö (IMemoryCache): aseta merkintä.Kokoa ja määritä SizeLimit sidottuun muistiin.
  • Ensisijainen tavoite (ImemoryCache): CacheItemPriority.High/Normal/Low/NeverRemove vaikuttaa häätöön paineen alla.
  • Jitter: Lisää pieniä satunnaisia offseteja TTL:iin, jotta vältytään synkronoidulta vanhenemiselta (leimat).

Välimuistin leimaamisen estäminen (lauman peittäminen)

  • Käytä GetOrCreate/GetOrCreateAsync (IMemoryCache) varmistaaksesi yhden luukun populaation solmua kohden.
  • Hajautettu: käytä lyhytkestoista lukitusnäppäintä (SET NX EX) tai kirjastotukea, lisää TTL-viesti; harkitse taustapäivitystä.
  • Palvele vanhentunutta ajankohtaa: säilytä tahmealla arvolla varustettu toissijainen avain ja lyhyt laajennus samalla, kun uusi arvo lasketaan.

Avainsuunnittelu ja nimittely

  • Mieluiten pienikokoinen, paksusuolen rajatut avaimet: app:entity:123 tai vuokralainen:us:users:42.
  • Sisällytä versiosegmentti mitätöimään kokonaisia avaimia ilman poistoja: v2:Products:123.
  • Vuokralaistieto: etuliitä avaimet vuokralaisen tai organisaation tunnisteeseen välttääksesi törmäykset ja helpottaaksesi puhdistusta.
  • Pidä avaimet pieninä, mutta kuvailevina; vältä käyttäjän kontrolloimaa raakapanosta ilman normalisointia.

Päättyvät avainsarjat (merkit/ryhmät)

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.

Käytännöllisiä auttajia

  • IMemoryCache saa tai asettaa vaihtoehtoja:
    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 with JSON ja sen viimeinen voimassaolopäivä:
    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;
    }
    

Seuranta ja näkyvyys

  • Radan osuma-/virhenopeudet ja keskimääräinen kuormitusaika; paljasta mittarit (Prometheus-laskurit) avainryhmää kohti.
  • Lisää kirjautuminen kätköväestön ympärille ja häätökutsut IMemoryCachelle.
  • Rediksen kannalta katso näppäinten osumia/puutteita, latenssia ja muistin pirstoutumista; aseta mahdollisimman suuret toimintatavat.

Real-World I MemoryCache -mallit tuotannosta

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.

Kuvio 1: Yksinkertainen välimuistien kesannointi harvoin muuttuville tiedoille (kategoriat)

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:

  • Luokat luetaan joka sivulla (näytetään navigoinnissa)
  • Ne harvoin muuttuvat (vain kun lisään uusia blogikirjoituksia ja uusia kategorioita)
  • 30 minuutin TTL sopii; jos uusia kategorioita ilmestyy, käyttäjät näkevät ne 30 minuutin kuluessa
  • Absoluuttinen vanheneminen vain (ei liukumista), koska emme välitä kuinka usein siihen pääsee käsiksi

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

Kuvio 2: Käyttäjäkohtainen tila, jossa kokorajat ja liukuva vanheneminen (käännöstehtävät)

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:

  • Liukastumisen päättyminen: Jos käyttäjä tarkistaa käännöstilan, pidä välimuisti elossa enintään 6 tuntia
  • Kokorajat: Pidä vain 5 viimeisintä tehtävää käyttäjää kohti muistin turvotuksen estämiseksi
  • Manuaalinen hallinta: Rajoitan nimenomaan kokoa, koska MemoryCache ei määrää entry-tason kohdelukujen raja-arvoja (vain välimuistin kokonaiskoko)

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.

Kaava 3: Havaintokykyinen välimuisti (Metrics-välimuisti)

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:

  • Havainnointikelpoisuus: SerilogTracing aktivity tracks cache hit/miss rate and records metrics require
  • Yhdistelmäavain: Sisältää päivämäärän ja etuliitteen välttääkseen keskeiset yhteentörmäykset
  • Päiväysmuotoilu: Käytökset yyyyMMdd Muodosta avain niin eri aikoina samana päivänä jaa välimuisti
  • Nullin käsittely: Palauttaa nollan, jos ulkoinen palvelu epäonnistuu; ei välimuistiviat
  • Suodatettu välimuisti: Jää suodatettu tulos väliin, ei raakavastetta

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

Kolmen lähestymistavan vertailu

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:

  1. Aloita yksinkertainen (malli 1), lisää monimutkaisuutta vain tarvittaessa
  2. Ajattele aina välimuistin käyttöä - lisää käyttäjäkohtaisten välimuistien kokorajat
  3. Kalliiden toimintojen osalta lisää havainnoitavuus ensimmäisestä päivästä alkaen
  4. Säädä TTL:ää todellisen tiedonmuutostaajuuden perusteella, ei arvauksia

Kun en käytä IMemoryCachea:

  • Käyttäjän tunnistautumistila (väitteitä auth-evästeessä)
  • Ostoskärryyn (käyttäisi DB + jaettua välimuistia tuotannossa)
  • Blogikirjoituksen sisältöä varten (jo DB:ssä, ladattuna kerran per pyyntö)
  • Ristinpalvelijoiden tila (tarvitsee IDiveddedCache/Redis)

Todentaminen Evästeet ja väitteet

  • Soveltamisala: pyynnöissä päättymiseen/loppuun asti
  • Käyttö: henkilöllisyys, karkeat roolit/oikeudet, pieni määrä profiilitietoja
  • Vahingonkorvausten tulisi olla vakaita.

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

JWT / Bearer Tokens

  • Soveltamisala: asiakas kantaa kuponkia, kansalaisuudeton palvelin
  • Käyttö: SPA:t/mobile/API:t, rajanylitysalue, mikropalvelut
  • Vahingonkorvaus: rahakkeen koko, pyöriminen/uudistaminen, vähimmäisväitteiden tallentaminen, tarvittaessa itsetutkiskelu
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

Kestävä tila: Tietokanta ja ystävät

  • Soveltamisala: ikuisesti (kunnes poistat sen)
  • Käyttö: mitä tahansa, mitä ei saa hukata: kärryjä, tilauksia, profiileja, pitkäaikaisia työnkulkuja
  • Mallit: vakio CRUD, jossa EF Core; CQRS; tapahtumahankinta; outbox-malli luotettavuudelle

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

Reaktiovälikohtaukset, ETagit ja ehdolliset pyynnöt

  • Ei pelkästään valtion kuljettamista, mutta vähentää toistuvaa työtä antamalla asiakkaan/proksien käyttää aiempia vastauksia uudelleen. Usein yhdistettynä kyselyn/reitin tilaan.
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");
});

ResponseCache vs OutputCache: Real-World Use in Production

Käytän sekä ResponseCachea että OutputCachea yhdessä blogissani eri tarkoituksiin. Tästä syystä saatat haluta molemmat ja miten ne eroavat toisistaan.

Hämmennys: Kaksi välilyöntiä?

ASP.NET Core on kaksi Samannäköiset välimuistijärjestelmät:

  1. ResponseCache (HTTP-välimuisti): Sarjat HTTP-otsikot (Cache-Control, Vary) kertoo selaimille ja CDN: lle, miten välimuisti välitetään e
  2. OutputCache (palvelinpuoli): Renderöi renderoidun ulostulon palvelimelle jättääksesi toimen kokonaan väliin

Ne täydentävät toisiaan. ResponseCache käsittelee asiakkaan/CDN:n välimuistia, OutputCache käsittelee palvelimen sivuvälimuistia.

Bloggaustoimintoni (molemmat välimuistit käytössä)

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

  1. OutputCache-tarkistukset ensin: Onko minulla välimuistin vastaus my-post + en kieltä?

    • Osuma: Palautus välimuistissa HTML, toimintatapa ei koskaan toimi (nopea! ~1ms)
    • Neiti: Suorita toiminta, tee näkymä, välimuistin tulos 3600 sekuntia (1 tunti)
  2. ResponseCache asettaa otsikot: Kun OutputCache tuottaa vastauksen, ResponseCache lisää:

    Cache-Control: public, max-age=300
    Vary: hx-request
    
  3. Selain välimuistiin: Browser välimuistit vastaus 300 sekuntia (5 minuuttia). Seuraavat pyynnöt samalta käyttäjältä eivät edes osu palvelimeen.

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

Miksi eri kestot? (300 vs. 360)

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

Perustelut:

  • Palvelinvälimuisti on pidempi (1 tunti): Hallitsen palvelintani; voin poistaa välimuistin päivittämällä viestin
  • Asiakasvälimuisti on lyhyempi (5 minuuttia): En pysty puhdistamaan käyttäjän selaimia tai CDN-levyjä helposti; 5 minuuttia on kohtuullinen tunkkaisuusikkuna
  • Kaupankäynti: Jos muokkaan viestiä, uusi sisältö ilmestyy:
    • Palvelinpuoli: Välittömästi (voin mitätöidä välimuistin)
    • CDN/selaimet: 5 minuutin kuluessa (tai poistan CDN:n manuaalisesti)

Evoluutio: Aluksi minulla oli molemmat viisi minuuttia, mutta se tarkoitti, että palvelimeni reputti uudelleen viiden minuutin välein, vaikka sisältö harvoin muuttuu. Nyt:

  • Palvelin palvelee iloisesti välimuistissa olevaa HTML:ää tunnin ajan
  • Asiakkaat saavat tuoretta sisältöä 5 minuutin välein

VaryByHeader HTMX:lle

VaryByHeader = "hx-request"  // ResponseCache
VaryByHeaderNames = new[] { "hx-request" }  // OutputCache

Miksi tällä on merkitystä: HTMX-pyyntöihin kuuluu hx-request: true Vastaan eri kysymyksiin:

  • Täysi pyyntö: Täydennä HTML-sivu asettelulla
  • HTMX-pyyntö: Osittainen näkymä ilman asettelua

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

VaryByQueryKielen ja paginoinnin avaimet

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

  • Kohtuullinen max cache -koko (OutputCache-autoevicts, vähiten hiljattain käytetty)
  • Yhteiset parametrit välimuistissa (sivu 1, oletussivukoko)
  • Melko harvinaiset yhdistelmät saattavat jäädä väliin (hyväksyttävä)

Asetukset vaaditaan

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

Kun OutputCache ei auta

OutputCache jätetään väliin:

  • Viralliset pyynnöt (eri käyttäjät näkevät erilaisia tietoja)
  • POST/PUT/DELETE (vain GET/HEAD-välimuisti)
  • Reaktiot potilailla, joilla on Set-Cookie Otsikko
  • Vastaukset, jotka on nimenomaisesti asetettu Cache-Control: no-store

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

Tehokkuuden mittaaminen

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:

  • Ilman välilyöntejä: ~50 ms keskimääräinen vasteaika (DB-kysely + Markdown renderointi)
  • OutputCache: ~1-2ms välimuistiin jätettyihin vastauksiin
  • 25x Nopeutus

Kun tilapäisesti sammutan välimuistin

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.

Kaksiportaisen strategian hyvät ja huonot puolet

Plussat:

  • Palvelimen tehokkuus: OutputCache vähentää CPU/DB-kuormaa 98 %
  • Asiakkaan/CDN:n edut: ResponseCache vähentää kaistanleveyttäni ja parantaa maailmanlaajuista latenssia
  • Joustavuus: Eri TTL:t palvelimelle ja asiakkaalle
  • Kustannussäästöt: Vähemmän DB-kyselyitä, vähemmän kaistanleveyttä

Miinukset:

  • Viihtyvyys: Sisällön päivittäminen vie asiakkaille jopa 5 minuuttia
  • Välimuistin mitätöinnin monimutkaisuus: Viestin päivittäminen edellyttää sekä palvelimen että CDN- välimuistien mitätöintiä
  • Muistinkäyttö: OutputCache pitää palvelinmuistissa renderoitua HTML:ää
  • Vianetsintähämmennystä: Joskus unohdat välimuistin olevan päällä ja ihmettelet, miksi muutokset eivät näy

Kun keskeytin välienselvittelyn:

  • Reaaliaikaiset tiedot (osakehinnat, live-urheilu)
  • Henkilökohtainen sisältö (käyttäjäkohtaiset suositukset)
  • Matalan liikenteen sivut (välimuistin ylitys > etu)
  • Sivuja, jotka muuttuvat hyvin usein

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.


ViewBag, ViewData ja TempData: Controller-to-View State (ja miksi useimmiten vältän kaksi niistä)

Nämä kolme ovat usein hämmentyneitä. Näin ne eroavat toisistaan ja mitä oikeasti käytän tuotannossa.

Kolme amigoa vertaili

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

Mitä itse käytän: ViewBag maailmanlaajuisille asettelutiedoille

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:

  • Maailmanlaajuinen: Jokainen sivu tarvitsee kategoriat navigointi ja analytiikka
  • Laskettu kerran: BaseController kulkee ennen jokaista toimintoa
  • HTMX-optimointi: Ohita analytiikkakirjoitus osittaisista pyynnöistä (HTMX:ää ei tarvitse injektoida uudelleen)
  • Jäädytetty: Kategoriat ovat välimuistissa (ks. IMemoryCache-kuvio yllä), joten älä paina DB:tä joka pyynnöstä

Erehdyin aikaisessa vaiheessa: Minä vain asettelin. ViewBag.Categories Jokaisessa toimintamenetelmässä. DRY-rikkomus ja helppo unohtaa. OnActionExecutionAsync tukikohdassa ohjain ratkaisi tämän.

Näytä sivukohtaiset tiedot (hyväksyttävä kaava)

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

  • Yksinkertaiset scalar-arvot (jousitus, int)
  • Käytetään vain näkymässä, ei kuljetettu ympäri
  • Vaihtoehtoa olisi lisätä Title sekä Category ominaisuuksia jokaiseen näkymämalliin

Mitä välttelen: Monimutkaiset kohteet ViewBagissa

Mallinvastaisuus:

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

  • Ei koontiajan turvallisuutta (typo ViewBag.Usr epäonnistuu ajon aikana)
  • Vaikea jäljittää, mitä tietoja on saatavilla
  • Testaaminen on vaikeampaa (tarve tarkistaa ViewBag-sanakirja)
  • Ei IntelliSenseä

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

TempData: En käytä sitä (ja tässä miksi)

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:

  1. Käytän HTMX:ää ohjauksen sijaan: Lomakkeeni lähettävät HTMX:n kautta ja palauttavat osittaisia näkökulmia inline-menestykseen/terror-viesteihin. Ei uudelleenohjausta = ei tarvetta TempDatalle.
[HttpPost]
public async Task<IActionResult> Submit(ContactViewModel model)
{
    if (!ModelState.IsValid)
        return PartialView("_ContactForm", model); // Show errors inline

    await sender.SendEmailAsync(contactModel);

    // Return success view directly (no redirect)
    return PartialView("_Response", new ContactViewModel
    {
        Email = model.Email,
        Name = model.Name,
        Comment = "Message sent!"
    });
}
  1. **Perinteinen PRG (Post-Redirect-Get)**Käyttäisin TempDataa, mutta mieluummin vältän uudelleenohjausta, kun se on mahdollista paremman UX:n saavuttamiseksi.

Kun TempDatassa on järkeä:

  • Perinteiset MVC-sovellukset, joissa on koko sivun mittainen uudelleenohjaus POST:n jälkeen
  • Monivaiheiset ohjaimet, joihin suuntaat vaiheiden välillä
  • Flash-viestejä varmennusohjauksen jälkeen

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.

Nopea ratkaisupuu

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:

  1. ViewBag vain maailmanlaajuisille asetteluille (analyyttiset, navigaattorit, korppujauhot)
  2. ViewBag yksinkertaisille sivujen otsikoille/otsikoille (vapaaehtoinen; voisi käyttää ViewModelia)
  3. Älä koskaan katsoBagia monimutkaisille objekteille (käytä ViewModelsia)
  4. Älä koskaan katso dataa (ViewBagissa on mukavampi syntaksi)
  5. TempData vain, jos todella tarvitset PRG:tä (En, kiitos HTMX:n)

Malli: Velhot ja monivaiheiset virrat

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
  • Pieni, yksiistuntoinen: Session tai TempData vaiheiden välillä.
  • Cross-device/long-running: pysy DB:ssä, kanna avainta URL-osoitteessa.

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

Malli: Flash-viestejä TempDatalla

  • Aseta POST; lue kerran uudelleenohjauksen jälkeen.
TempData["Flash"] = "Profile saved";
return RedirectToAction("Index");

Razor-näkymä:

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

Malli: ostoskärry

  • Pienet kärryt: Session (jos asteikkosi on vaatimaton ja sinulla on tahmea/jakautunut istunto).
  • Suuremmat kärryt/monitoimilaitteet: DB + cart-id evästeessä tai URL-osoitteessa.
flowchart LR
  U[User] -- cart-id cookie --> S[Server]
  S --> DB[(Cart Table)]
  S <--> Cache[Distributed Cache]

Turvallisuus-, yksityisyys- ja vaatimustenmukaisuuden tarkistuslista

  • Validoidaan kaikki asiakkaan tarjoamat tiedot: kysely, otsikot, lomakkeet, evästeet, JWT väittää.
  • Suojele herkkää asiakasta: käytä tietosuojaa evästeisiin, joita julkaiset; älä koskaan tallenna salaisuuksia kyselyn kielissä.
  • Aseta evästeliput: Secure, HttpOnly, SameSite, IsEssential (jos suostumus/toimintatarve sitä edellyttää).
  • Luo uudelleen varmennusevästeet etuoikeuksien muutoksista; pidä vaateet mahdollisimman vähäisinä.
  • Salaa tarvittaessa palvelimen puolella olevien myymälöiden lepoon; varmista näppäinten kierto (tietosuoja-avaimet, JWT-allekirjoitusavaimet).
  • GDPR/CCPA: Toimita käyttäjätietojen vienti-/delection-polut, minimoi säilyttäminen.

Päätös Matrix (Cheat Sheet)

classDiagram
  class Items {
    +Per-request only
    +Great for middleware->endpoint handoff
  }
  class QueryRoute {
    +Explicit, linkable
    -User-controlled
  }
  class Headers {
    +Tracing, idempotency
    -Noisy, untrusted
  }
  class Cookies {
    +Persist small prefs
    -Size, perf, consent
  }
  class TempData {
    +One-redirect messages
    -Ephemeral
  }
  class Session {
    +Conversational state
    -Scaling complexity
  }
  class MemoryCache {
    +Fast, in-proc
    -Not shared across instances
  }
  class DistributedCache {
    +Shared across servers
    -Serialization, ops
  }
  class AuthCookieClaims {
    +Identity, roles
    -Don’t overstuff
  }
  class JWT {
    +Stateless, cross-domain
    -Revocation/rotation
  }
  class DB {
    +Durable, authoritative
    -Latency, complexity
  }

Pikavalinnat:

  • Tarvitsetko yhden suoran välähdyksen?
  • Tarvitsetko velhoa useisiin pyyntöihin yhdellä istunnolla? Istunto (tai DB + avain, jos kesto/monitoimilaite).
  • Tarvitsetko skaalautuvuutta ja kansalaisuudettomia API-rajapintoja? JWT henkilöllisyydelle, DB/DistributedCache for Statelle.
  • Tarve siirtää dataa vain putken sisälle? HttpContext.Items.
  • Tarvitsetko välimuistin tietokonehakuja varten? IMemoryCache paikallisesti; IDivedCache maatilan toisella puolella.

MVC vs Razor Pages vs Minimal APIs: Samat säätiöt, Eri muotoiset

Kaikki kolme pinoa istuvat samoilla primitiivisillä (HttpContext, mallisidonta, autaalinen, tietosuoja). Yllä olevat esimerkit osoittavat, että sovellusrajapinnat eroavat toisistaan lähinnä ergonomiassa:

  • Minimirajapinnat: parametrin sidonta reitistä/kyselystä/kehosta/pyynnöistä; paluu Results.*.
  • MVC: attribuutit, suodattimet, toimintaparametreihin/katselumalleihin sitova malli.
  • Razor-sivut: sivukäsittelijöitä, joilla on sidottuja ominaisuuksia ja tunnisteavustimia linkkien/lomakkeiden tuottamiseen.

Heillä kaikilla on samat valtion mekanismit, joista täällä keskustellaan.


Kapinalliset ja antipatterit

  • Suurien tai arkaluonteisten tietojen tallentaminen evästeisiin tai TempDataan.
  • Riippuen muistissa olevasta välimuistista oikeellisuudesta (se on välimuisti, ei totuus).
  • Rakennuslupa perustuu asiakkaan lähettämiin reitti-/kyselylippuihin ilman palvelintarkistuksia.
  • Ylität auth-keksejä tai JWT-keksejä epävakailla väitteillä.
  • Istunnossa ilman jakelustrategiaa (toimii paikallisesti, hajoaa mittakaavassa).

Real-World State Management: Mitä oikeasti käytän (ja älä käytä)

Näytettyäni sinulle kaikki nämä vaihtoehdot, tässä on rehellinen arvioni siitä, mikä toimii bloggausalustani tuotannossa.

Valtionhallinnon pino (taajuusjärjestyksessä)

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

Tuotantokokemuksen yksityiskohtaiset hyvät ja huonot puolet

IMemoryCache

Mihin käytän sitä:

  • Ryhmäluettelo (globaali, 30 min TTL)
  • Käyttäjäkohtainen käännöstehtävän seuranta (6 h absoluuttinen + 1h liukumäki)
  • Ulkoiset API-vastaukset (Umamimetrit, 1h TTL)

Käytännössä ammattilaiset:

  • Nopea räjähde (prosessin aikana)
  • Ei sarjanumerointia yleisellä tasolla
  • Vähennetty DB/API-kuorma ~95 %
  • Helppo toteuttaa ja ymmärtää
  • Havainnointi SerilogTracingin kautta

Miinukset, joihin olen törmännyt:

  • Muisti vuotaa, jos et rajoita käyttäjäkohtaisia välilyöntejä
  • Tyhjennetty sovelluksen uudelleenkäynnistyksessä (hyväksytään käyttökotelooni)
  • Ei jaeta palvelimille (yksittäin)
  • Välimuistin mitätöinti on manuaalinen (tarve poistaa () nimenomaisesti)

Kallistusraja: Jos pääsen useille palvelimille, tarvitsen jaetun tilan IDivedCache (Redis) -palvelun. Tällä hetkellä yksi palvelin + muistivälimuisti on täydellinen.

OutputCache + ResponseCache

Mihin käytän sitä:

  • Blog post renderointi (1 tunnin palvelin, 5 min asiakas/CDN)
  • Blogilista/kategoriasivut
  • Kalenteritiedot

Käytännössä ammattilaiset:

  • 25x Nopeutus (50ms → 2ms)
  • Skaalaa ruuhkaan hikoilematta
  • Erilliset TTL:t palvelimelle ja asiakkaalle
  • Toimii saumattomasti HTMX:n kanssa (VaryByHeader)
  • Mittaava törmäys Prometheus-metrien avulla

Miinukset, joihin olen törmännyt:

  • Vianetsintähämmennystä (unohtakaa välimuisti on päällä)
  • Cache-räjähdys monella kyselyparametriyhdistelmällä
  • Viivytysikkuna (5 min asiakkaille)
  • Sisällön päivitysten manuaalisen mitätöinnin tarve

Paras: Lukevat sovellukset, joissa on enimmäkseen staattista sisältöä. Ne eivät ole hyväksi henkilökohtaiselle tai reaaliaikaiselle datalle.

KatsoBag- sivua

Mihin käytän sitä:

  • Maailmanlaajuiset pohjapiirustustiedot (analyyttiset tiedot, kategoriat)
  • Sivun otsikot

Käytännössä ammattilaiset:

  • Yksinkertaisia ja nopeita layout-tason tietoja
  • Aseta kerran BaseController, saatavilla kaikkialla
  • Toimii hienosti välimuistissa olevien kategorialuetteloiden kanssa

Miinukset, joihin olen törmännyt:

  • Ei tyyppiturvallisuutta (tyypit eivät toimi ajon aikana)
  • Monimutkaisen datan liikakäyttö houkuttaa
  • Vaikeasti testattava

Noudatan seuraavaa sääntöä: ViewBag vain yksinkertaisille scalareille. Monimutkaiset esineet menevät ViewModelsiin.

Auth väittää, että

Mihin käytän sitä:

  • Käyttäjätunnus, nimi, sähköposti, avatar URL
  • Admin-lippu (tarkistus sub Vaatimus konfigurointia vastaan)

Käytännössä ammattilaiset:

  • Turvallinen (allekirjoitettu eväste, tietosuoja)
  • Automaattinen ASP.NET Core Identity/OAuthilla
  • Saatavilla osoitteessa User.Claims kaikkialla
  • Sliding Emergence pitää käyttäjät kirjautuneena sisään

Miinukset, joihin olen törmännyt:

  • Evästekokoraja (älä ylitä vaateita)
  • Vaateet pysyvät muuttumattomina, kunnes kirjaudut uudelleen
  • Jos lisään väitteen (kuten "IsEditor", sinun täytyy julkaista auth-eväste uudelleen.

Parhaita käytäntöjä: Pidä vaateet pieninä ja vakaina. Älä laita usein muuttuvia tietoja väitteisiin.

Asioita, joita en käytä (ja miksi)

Istunto (ei koskaan käytetty):

  • Tarvitsisi jaetun myymälän (Redis)
  • Lisää monimutkaisuutta vähäisen hyödyn vuoksi
  • Hyödyllisyystapauksiani palvelevat paremmin:
    • Authin väitteet (identiteetti)
    • IMemoryCache (lyhytaikainen tila)
    • Tietokanta (kestävä tila)

TempData (ei koskaan käytetty):

  • HTMX poisti uudelleenohjauksen jälkeisen kuvion
  • Lomakkeet palauttavat osittaisia näkökulmia inline-viesteillä
  • Ei tarvetta selvitä uudelleenohjauksista

HttpContext.Itsejä (ei koskaan käytetty):

  • Ei ole ollut käyttöoikeutta per-pyyntöön
  • Keskiohjelmistoni ei laske arvoja ohjaimille
  • Jos tarvitsisin vuokralaisen tunnistamista, käyttäisin sitä

IdistributedCache (ei koskaan käytetty):

  • Yhden palvelimen käyttöönotto
  • IMemoryCache täyttää kaikki tarpeet
  • Käyttäisin Redisiä, jos skaalaisin useille palvelimille

Lähestymiseni kehitys

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

Neuvoja sovellukseesi

Aloita tästä:

  1. Navigoinnin/suodatuksen reitti-/pyyhkäisevät paramit (aina kansalaisuudeton ensin)
  2. Authin identiteettiä koskevat väitteet
  3. Tietokanta kaikkea sitä varten, mitä on säilytettävä
  4. IMemoryCache-muistia raskaiden tietojen laskentaan
  5. OutputCache kalliille renderöitäville sivuille

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

  • Kompleksiset kohteet ViewBag/TempDatassa
  • Istunto ilman jaettua taustavarastoa
  • Välimuistin käyttäjäkohtainen tai usein muuttuva data
  • Ennenaikainen optimointi (toimenpide ensin!)

Tärkeimmät opetukset:

  • Aloita yksinkertainen, lisää monimutkaisuutta vain, kun mittaukset osoittavat, että tarvitset sitä
  • Havainnointi on tärkeää (tiedä välimuistin osumaprosentit!)
  • Miettikää aikaisessa vaiheessa asteikon rajoja (käyttäjäkohtaiset välimuistit tarvitsevat rajoja)
  • Testivälimuistin mitätöinti perusteellisesti (valaisu on todellinen ongelma)
  • Dokumentoi TTL-valintasi (tulevaisuus kysyy "miksi 30 minuuttia?")

Kääri

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.


Liite: Lisää kopioitavia esimerkkejä (korjauksia)

Nämä esimerkit syventävät aiempia jaksoja tuotantoluokan yksityiskohdilla, jotka voit liittää net9 minimaalisiin malleihin, MVC:hen tai Razor-sivuihin.

Evästeet: Suojaa arvoja tietosuojalla

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.

Väärennöksenesto MVC-, Razor- ja Minimal API -sivuilla

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: koristele toimet [ValidateAntiForgeryToken] ja käyttö @Html.AntiForgeryToken() muodoissa.
  • Razor-sivut: oletuksena käytössä lomakkeissa; käyttö asp-antiforgery="true" tarvittaessa.
  • Minimaalinen: validoidaan kautta IAntiforgery kuten on näytetty.

Istunto Redisin kanssa ja liukuva vs. absoluuttinen vanheneminen

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.

IMemoryCache sisäänpääsyvaihtoehdoilla ja häätökutsulla

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 Get- or-set kanssa jännittää välttää leimat

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.

Julkaistaan ja validoidaan JWT:t paikallisesti (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");

Ehdolliset päivitykset ETageilla (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: monimutkaisia esineitä JSONin kautta

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-ytimen rahayksikkö

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

Virkistää väitteitä julkaisemalla auth-keksin

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.


Deep Sukellus: HttpContext.Items (Käytännölliset mallit ja auttajat)

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.

  • Elinkaari: luotu pyydettäessä, hylätty, kun vastaus on valmis.
  • Soveltamisala: Vain nykyinen pyyntö – älä koskaan ylitä uudelleenohjausta tai taustatyötä.
  • Suorituskyky: O(1) lookups; ihanteellinen per-pyyntö välimuistiin.
  • Turvallisuus: vain palvelinpuolella; asiakas ei näe sitä.

Miksi kohteet eivät

  • Session/TempData: Nämä ristikkäiset pyynnöt ja jakeluongelmat. Tuotteet ovat ohimeneviä ja mittakaavaystävällisiä.
  • DI Laajennetut palvelut: Käytä näitä käyttäytymiseen ja jaettuihin riippuvuuksiin. Tuotteita on parempi käyttää ad hoc -, laskutusarvoihin (vuokralainen, käyttäjäpaikka, ominaisuusliput) ja pyyntöjä kohden laskettuihin välilyönteihin.
  • HttpContext.Ominaisuudet: Kehys-/kuljetustason ominaisuuksia varten (IEndpointFeature, IHtpUpgradeFeature). Tuotteita käytetään sovellustason dataan.

Vältä avainkolareita: vahvasti kirjoitetut avaimet

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

Malli: Laske keskiohjelmistoissa, kuluta päätepisteissä/ohjaimissa/sivuilla

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

Malli: Pyydä välimuisti toistuvien töiden välttämiseksi

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:

  • Threading: Yksi ainoa pyyntö suoritetaan tyypillisesti yhdellä loogisella polulla; Tuotteita ei ole suojattu rinnakkaiskirjoituksille. Jos aloitat rinnakkaiset tehtävät, jotka jakavat kappaleet, lisää oma synkronointisi.
  • Koko: Pidä arvot pieninä ja edullisina laskeaksesi/serialisoidaksesi.

Malli: Tuotteita kansoittavat suodattimet (MVC/Razor-sivut)

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

Malli: Enrique-lokeja ilman varauksia kaikkialla

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

Milloin ei saa käyttää kappaleita

  • Tiedot tarvitaan uudelleenohjauksen jälkeen tai pyynnöissä (käytä sen sijaan TempDataa/Sessiota/DB:tä).
  • Sovellettavat singletonit tai ristiinpyydetyt välimuistit (käytä IMemoryCachea/ID:tä, joka on addredCachea).
  • Identiteettiin/valtuutukseen kuuluvat arvot (käytä vaatimuksia/toimia).

Nopea sääntö: Jos se laskee tämän pyynnön aikana ja lukee tämän pyynnön omalla koodilla, kohteet ovat ihanteellisia.

Finding related posts...
logo

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