Back to "Mantenere lo stato tra le richieste in ASP.NET Core: una guida pratica, senza senso (MVC, pagine di Rasoio, API minime)"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

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

Mantenere lo stato tra le richieste in ASP.NET Core: una guida pratica, senza senso (MVC, pagine di Rasoio, API minime)

Sunday, 09 November 2025

AGGIORNAMENTO (2025-11-10): Aggiunti esempi più pratici dalla mia base di codice reale che mostrano modelli del mondo reale, trade-off, gotchas, e l'evoluzione da semplici e sofisticati approcci di gestione dello stato. Include dettagliati modelli IMemoryCache, VisualizzaBag utilizzo (buono e cattivo), ResponseCache / OutputCache strategie, e le lezioni apprese dalla produzione.

Introduzione

HTTP è notoriamente apolide. La tua app... non è. Gli utenti accedono, aggiungono elementi ai cestini, saltano tra le pagine, ritornano domani, e si aspettano che tu ricordi. In ASP.NET Core (MVC, Razor Pages, Minimal API), ci sono un sacco di modi per preservare e trasferire lo stato tra le richieste. Alcuni sono solo per-richiesta. Alcuni ultimi per una sessione. Alcuni vivono nel client. Alcuni sono distribuiti e sopravvivere server riavvia. Ogni scelta ha compromessi tra sicurezza, prestazioni, scala, e sviluppatore ergonomia.

Questo post cataloga le opzioni, mostra esempi concreti, copiabili in tutte e tre le pila, e ti fornisce la guida decisionale in modo da poter scegliere lo strumento giusto per il lavoro.

NOTA: Questo fa parte dei miei esperimenti con l'AI (elaborazione assistita) + il mio editing. Stessa voce, stesso pragmatismo; solo dita più veloci.

Il paesaggio ad un'occhiata

flowchart LR
  subgraph Client
    Q[Query String]
    R[Route Values]
    H[Headers]
    F[Form/Hidden Fields]
    CK[Cookies]
    LS[Local/Session Storage]
  end
  subgraph Server
    I[HttpContext.Items<br/>\nper request only]
    TD[TempData<br/>\none redirect]
    SS[Session]
    MC[IMemoryCache]
    DC[IDistributedCache]
    AU[Auth Cookie / Claims]
    JT[JWT / Bearer]
    DB[Database / Durable Store]
    BUS[Outbox / Queue]
  end
  Q --> Model[Model Binding]
  R --> Model
  F --> Model
  H --> Model
  CK --> App[Your Code]
  Model --> App
  App -->|Set| CK
  App -->|Set| TD
  App -->|Set| SS
  App -->|Set| MC
  App -->|Set| DC
  App -->|Issue| AU
  App -->|Issue| JT
  App -->|Persist| DB
  • Client-portato: query, route, header, form, cookies, JWT. Scala orizzontalmente ma è visibile al client (deve essere convalidato/firmato/ crittografato se necessario).
  • Server-portato: TempData, Sessione, cache, DB. Richiede affinità o strategia di distribuzione.
  • Per-richiesta: HttpContext.Items - utile per il passaggio interno dei dati durante una singola richiesta.

Regole d'oro (prima di tuffarci nelle API)

  1. La sicurezza prima di tutto: la fuga dello Stato è una minaccia reale. Stato lato server (Session, in-memory cache) può trapelare tra gli utenti attraverso errori di codifica, condizioni di gara, o la gestione del ciclo di vita non corretta. modelli senza stato non sono solo più scalabili sono più sicuri.
  2. Preferire i modelli di "server senza stato" quando è necessario scalare orizzontalmente (spinga stato a gettoni client, negozi durevoli, o cache distribuite).
  3. Non fidarti mai dello stato controllato dal client. Convalida, firma e/o crittografia.
  4. Tenere grandi dati fuori dai cookie e dalle intestazioni; gonfiano ogni richiesta.
  5. Utilizzare TempData solo per Post-Redirect-Get (PRG) one-shot come i messaggi flash.
  6. Usa Sessione solo quando devi mantenere lo stato di conversazione lato server e hai pianificato la distribuzione e essere paranoico sulla sicurezza.
  7. Le richieste riguardano l'identità e l'autorizzazione grossolana, non lo stato generale dell'app.
  8. La cache non è una fonte di verità. Indietro con un deposito durevole se i dati sono importanti.

Stato per richiesta: HttpContext.Items

  • Campo d'applicazione: richiesta attuale solo (teardown alla fine del gasdotto)
  • Uso per: passaggio di valori calcolati tra middleware e endpoint/controller
  • Scala: nessun impatto
  • Sicurezza: solo server
sequenceDiagram
  participant M as Middleware
  participant E as Endpoint/Controller
  M->>M: Compute TenantId
  M->>E: HttpContext.Items["TenantId"] = 42
  E->>E: Read Items["TenantId"]

Esempio di middleware (tutte le pile):

app.Use(async (context, next) =>
{
    var tenantId = context.Request.Headers["X-TenantId"].FirstOrDefault() ?? "public";
    context.Items["TenantId"] = tenantId;
    await next(context);
});
  • Endpoint API minimo:
app.MapGet("/whoami", (HttpContext ctx) => new { Tenant = ctx.Items["TenantId"] });
  • Controllore MVC:
public IActionResult WhoAmI() => Json(new { Tenant = HttpContext.Items["TenantId"] });
  • Gestore di Razor Page:
public IActionResult OnGet() => new JsonResult(new { Tenant = HttpContext.Items["TenantId"] });

Stato transitorio trasportato dal cliente: valori di rotta e stringa di interrogazione

  • Ambito di applicazione: richiesta corrente; il cliente lo porta esplicitamente.
  • Utilizzo per: contesto di navigazione, filtraggio, paginazione, identità delle risorse
  • Sicurezza: deve convalidare/autorizzare; non incorporare segreti

Esempi

  • API minime:
app.MapGet("/orders/{id:int}", (int id, int? page) => Results.Ok(new { id, page }));
// GET /orders/5?page=2
  • MVC:
[HttpGet("/orders/{id:int}")]
public IActionResult Details(int id, int? page)
  => View(new { id, page });
  • Pagine rasoio (Ordini/Dettagli.cshtml.cs):
public IActionResult OnGet(int id, int? page)
  => Page();

Generazione di collegamenti che conservano lo stato:

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

Intestazioni: ID di concordanza, Chiavi per l'impotenza, Flag di funzionalità

  • Campo di applicazione: richiesta attuale, facoltativamente riecheggiata nelle risposte
  • Uso per: tracciamento, retry-safety, bandiere A/B
  • Sicurezza: trattare come ingresso non attendibile, convalidare/whitelist
app.Use(async (ctx, next) =>
{
    var correlationId = ctx.Request.Headers["X-Correlation-Id"].FirstOrDefault()
                      ?? Guid.NewGuid().ToString("n");
    ctx.Response.Headers["X-Correlation-Id"] = correlationId;
    await next(ctx);
});

Idempotenza (modello):

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

Forme e campi nascosti (PRG)

  • Campo di applicazione: solo richiesta successiva (i messaggi client indietro i valori)
  • Utilizzare per: passaggi wizard, token anti-forgery, mantenendo piccoli bit di stato in PRG
  • Sicurezza: convalidare sempre; combinare con antiforgery

Modello PRG nelle pagine MVC/Razor:

sequenceDiagram
  participant U as User
  participant P as POST Action
  participant R as Redirect
  participant G as GET Action
  U->>P: POST form
  P-->>R: 302 Redirect
  U->>G: GET redirected
  G-->>U: Final page (no resubmits)
  • MVC:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Save(SettingsModel model)
{
    // validate & persist
    return RedirectToAction(nameof(Summary), new { tab = model.SelectedTab });
}
  • Pagine rasoio:
public IActionResult OnPost(SettingsModel model)
{
    return RedirectToPage("/Settings/Summary", new { tab = model.SelectedTab });
}

Cookies: Piccolo, Firmato, A volte criptato

  • Campo di applicazione: ogni richiesta dal browser fino alla scadenza
  • Utilizzo per: preferenze, flag non sensibili, consenso; cookies di autenticazione (sezione separata)
  • Contrassegni: limiti di dimensione (~4KB per cookie), impatto sulle prestazioni, devono rispettare le leggi sul consenso

Esempio minimo di API:

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

L'uso di MVC/Razor Pages è identico tramite HttpContext.

Per l'integrità/riservatezza, utilizzare il sistema ASP.NET Core Data Protection per proteggere i payload da soli inseriti nei cookie.


TempData: bus del messaggio One-Redirect

  • Campo di applicazione: sopravvive ad un reindirizzamento
  • Backing store: Cookie (default) o Sessione
  • Utilizzare per: messaggi flash, sommari di validazione dopo PRG

Setup (Program.cs):

builder.Services.AddControllersWithViews().AddSessionStateTempDataProvider(); // optional
builder.Services.AddSession();
var app = builder.Build();
app.UseSession();

Nel controller MVC:

TempData["StatusMessage"] = "Saved!";
return RedirectToAction("Index");

In Razor Page handler:

TempData["StatusMessage"] = "Saved!";
return RedirectToPage("/Index");

In vista/pagina:

@if (TempData["StatusMessage"] is string msg) {
  <div class="alert alert-success">@msg</div>
}
flowchart LR
  A[POST /save] -->|TempData set| B[302 Redirect]
  B --> C[GET /index]
  C -->|TempData read consumed| D[Render message]

Sessione: Stato Conversazionale Server-Side

  • Campo di applicazione: sessione del browser (key cookie + server store)
  • Utilizzo per: maghi multi-step, piccoli dati del carrello, contagiri
  • Trade-off: richiede sessioni appiccicose o store di supporto distribuito; può limitare la scala

Configura:

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

Uso della sessione (qualsiasi pila):

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

Aiutanti di sessione:

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

Un racconto di avvertimento: Quando lo stato della sessione diventa il collo di bottiglia

Una volta ho lavorato su un enorme progetto IT del governo del Regno Unito in cui l'uso improprio dello stato di sessione (tra molti altri peccati architettonici) è diventato un gioco da ragazzi. tutto in sessione: preferenze dell'utente, dati di forma multi-step, risultati di ricerca, calcoli temporanei, anche ricerche cache che avrebbero dovuto essere in una cache o in un database adeguato.

Il problema: Lo stato di sessione è stato memorizzato in-processo (ASP.NET stato di sessione in web.config, questo è stato pre-Core giorni). Ogni richiesta ha dovuto deserializzare oggetti di sessione di massa. Come il carico aumentato, stato di sessione mongolfizzato a decine di megabyte per utente. Con migliaia di utenti concorrenti, i server hanno esaurito la memoria.

La soluzione disperata: Abbiamo volato nella struttura di HP a Stoccarda per eseguire test di carico sul loro Superdome in quel momento, La macchina Windows più potente d'Europa. Era una bestia: decine di processori Itanium, centinaia di gigabyte di RAM. L'idea era di dimostrare che con abbastanza hardware, il sistema poteva soddisfare i requisiti.

Il risultato: Anche sul Superdome, non siamo riusciti a colpire gli obiettivi utente concorrenti richiesti. L'architettura dello stato di sessione era fondamentalmente rotta. La scalatura verticale non poteva salvare il disegno difettoso. La serializzazione/deserializzazione di sessione in overhead, combinata con la pressione di memoria da oggetti di sessione massicci, significava semplicemente che il sistema non poteva scalare non ad alcun costo ragionevole.

L'incubo della sicurezza: Peggio dei problemi di performance, abbiamo scoperto un errore di codifica che ha causato lo stato della sessione a perdita tra gli utenti. I dati di sessione dell'utente A appaiono occasionalmente nella sessione dell'utente B. Questo non era solo imbarazzante è stato catastrofico. Gli utenti erano Personale dell'NHS Abbiamo creato accidentalmente un meccanismo di violazione della protezione dei dati che potrebbe esporre informazioni mediche sensibili in diverse sessioni di operatori sanitari.

Cosa sarebbe dovuto succedere:

  1. Senza stato per impostazione predefinita: La maggior parte dei dati di sessione non sarebbe mai esistita
  2. Banca dati per uno stato durevole: Il progresso del modulo multi-step avrebbe dovuto essere nel database con un ID di workflow
  3. Cache per le ricerche: Lookup condivise appartenevano a IMemoryCache o a una cache distribuita
  4. Client-side per le preferenze: Le preferenze dell'utente potrebbero essere state in cookie o storage locale
  5. Se necessario, sessione distribuita: Se la sessione fosse veramente necessaria, la sessione Redis-backed avrebbe condiviso il carico

La lezione: Lo stato di sessione non scala verticalmente e appena scala orizzontalmente (anche con sessioni appiccicose o negozi distribuiti, stai ancora serializzando/deserializzando su ogni richiesta).L'esperimento Superdome ha dimostrato che lanciare hardware a problemi architettonici è costoso e spesso inutile.

Ma cosa più importante: bug di stato di sessione diventano vulnerabilità di sicurezza. Problemi di sicurezza del filo, condizioni di gara, gestione errata degli ID di sessione Questi non solo causano problemi di prestazioni, ma possono trapelare dati sensibili tra gli utenti. In un contesto sanitario (o bancario, o qualsiasi settore regolamentato), si tratta di un incubo di conformità e potenziale responsabilità penale.

Consulenza moderna:

  • Se vi trovate bisogno di più di alcuni KB di dati di sessione, probabilmente hai un problema di progettazione
  • Se stai memorizzando dati sensibili in sessione, stai creando una superficie di attacco di sicurezza
  • Architetture senza stato non solo scalare meglio sono intrinsecamente più sicuri perché non c'è alcun server-lato di stato da far trapelare
  • Ripensate la vostra strategia di gestione statale prima di volare a Stoccarda (o spiegare una violazione dei dati al Commissario per l'Informazione)

Caching: IMemoryCache e IDistributedCache

  • Campo di applicazione: processo server (IMEmoryCache) o distribuito (IDistributedCache)
  • Uso per: dati derivati/computati, ricerche, stato di breve durata
  • Contrassegni: invalidazione cache, serializzazione per distribuzione

ImemoryCache:

builder.Services.AddMemoryCache();

app.MapGet("/rates", (IMemoryCache cache) =>
{
    var key = "fx:usd:eur";
    if (!cache.TryGetValue(key, out decimal rate))
    {
        rate = 0.92m; // pretend fetch
        cache.Set(key, rate, TimeSpan.FromMinutes(5));
    }
    return Results.Ok(rate);
});

IDistributedCache (ad esempio, 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");
});

Avvertimento anti-pattern Cache-as-state: se deve essere durevole o autorevole, memorizzarlo in un database e opzionalmente cache.

Scegliere tra IMemoryCache e IDistributedCache

  • ImemoryCache:
    • Gli oggetti che brillano velocemente, in-processo, rimangono come oggetti (nessuna serializzazione).
    • Evizione per pressione della memoria, limite di dimensione, scadenza assoluta/scivolosa e priorità.
    • Non condiviso tra i nodi; eliminato sul riciclo/sfruttamento dell'app.
    • Ottimo per hotset per nodo, lookup computerizzate, TTL brevi.
  • IDistributedCache (Redis/SQL/etc.):
    • Condiviso in un'azienda agricola; sopravvive ai riavvii dell'app; richiede la serializzazione (stringhe/byte).
    • Latenza leggermente più elevata; il flusso dipende dalla rete e dal backend.
    • Supporta la scadenza assoluta/scivolosa (provider-dipendente; Redis provider aggiorna TTL sull'accesso per scorrevole).
    • Ideale per la consistenza cross-node, letture di grande fan-out, flag di funzionalità e sessione.

Strategie comuni per la cache

  • Cache-aside (più comune):

    1. Prova cache; 2) se manca, carica dalla sorgente; 3) scrivi alla cache; 4) torna.
    • Pro: semplice; fonte di verità rimane il database.
    • Punti negativi: prima richiesta dopo la scadenza è lenta; eventuali stampe.
  • Read-through (tramite una libreria/fornitore): la cache gestisce il caricamento su miss.

  • Write-through: le scritture vanno alla cache e al backing store in modo sincrono.

  • Write-behind: scrivi alla cache, ripulisci per memorizzare asincronamente (rischio: perdita/inconsistenza).

  • Aggiornare: aggiornare i tasti caldi prima della loro scadenza per evitare la mancanza di freddo.

Scadenza, sfratto e dimensionamento

  • Scadenza assoluta: scade sempre dopo una durata fissa (buono per la freschezza dei dati esterni).
  • Scadenza scorrevole: estende TTL all'accesso (buono per le sessioni/dati specifici dell'utente).
  • Sgombero basato sulle dimensioni (IMEmoryCache): imposta la voce.Dimensioni e configura SizeLimit alla memoria legata.
  • Priorità (IMEMORYCache): CacheItemPriority.High/Normal/Low/NeverRemove influenza lo sfratto sotto pressione.
  • Jitter: aggiungere piccoli offset casuali ai TTL per evitare la scadenza sincronizzata (stampe).

Prevenire i timpani della cache (mandria di fulmini)

  • Usa GetOrCreate/GetOrCreateAsync (IMEmoryCache) per garantire una popolazione a filo singolo per nodo.
  • Distribuito: utilizzare una chiave di blocco a vita breve (SET NX EX) o supporto per la libreria; aggiungere TTL jitter; considerare l'aggiornamento di sfondo.
  • Servire stantio-while-revalidate: mantenere una chiave secondaria con valore stantio ed estensione breve mentre viene calcolato un nuovo valore.

Progettazione chiave e namespacing

  • Preferisci le chiavi minuscole, delimitate dal colon: app:entità:123 o inquilino:us:utenti:42.
  • Includi il segmento di versione per invalidare intere classi di chiavi senza eliminare: v2:products:123.
  • Tenant-aware: chiavi prefisso con inquilino o organizzazione id per evitare collisioni e facilità di epurazione.
  • Mantenere le chiavi piccole ma descrittive; evitare l'ingresso grezzo controllato dall'utente senza normalizzazione.

Set di chiavi in scadenza (tag/gruppi)

Quando hai bisogno di invalidare molte voci correlate:

  • Prefisso versione (soft invalidation): bump a global version in a small key and compose keys with it.

    // version key: "v:products"; keys like $"{version}:product:{id}"
    var version = await cache.GetStringAsync("v:products") ?? "1";
    var key = $"{version}:product:{id}";
    

    Invalidare tutti i prodotti: incremento v:prodotti (i clienti perderanno naturalmente le vecchie chiavi prefisse).

  • Tag set per gruppo (Redis): mantenere un insieme di chiavi per tag; sulla invalidazione, recuperare i membri ed eliminare.

    // 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 invalidation: pubblica un messaggio "invalidate:key"; ogni nodo rimuove la chiave dalla sua IMemoryCache locale.

  • Scansione con i modelli: SCAN/KEYS dovrebbe essere evitato in percorsi hot prod; ok per lo strumento di amministrazione su piccoli spazi di tasti.

Aiutanti pratici

  • IMemoryCache get-or-set con le opzioni:
    T GetOrAdd<T>(IMemoryCache cache, string key, Func<ICacheEntry, T> factory)
      => cache.GetOrCreate(key, e =>
      {
          e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
          e.SlidingExpiration = TimeSpan.FromMinutes(2);
          e.Priority = CacheItemPriority.Normal;
          e.Size = 1;
          return factory(e);
      });
    
  • IDistributedCache con JSON e scadenza:
    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;
    }
    

Monitoraggio e visibilità

  • Track hit/miss rates and medial load time; exposure metriches (Prometeus counters) for key group.
  • Aggiungi il logging intorno alla popolazione della cache e le callback di sfratto per IMemoryCache.
  • Per Redis, guardare keyspace colpi / manca, latenza, e frammentazione della memoria; impostare le politiche di maxmemory, a seconda dei casi.

Real-World ImemoryModelli di cache dalla produzione

Ecco come in realtà uso IMemoryCache nella mia piattaforma blog, si è evoluto attraverso la prova e l'errore. Vi mostrerò tre modelli reali da semplice a sofisticato.

Modello 1: Semplice messa a riposo della cache per dati che cambiano raramente (Categorie)

Questa è stata la mia prima implementazione di caching. Le categorie dei blog non cambiano spesso, quindi cache per 30 minuti:

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

Perché funziona così:

  • Le categorie sono lette su ogni pagina (mostrate nella navigazione)
  • Raramente cambiano (solo quando aggiungo nuovi post sul blog con nuove categorie)
  • 30 minuti TTL va bene; se compaiono nuove categorie, gli utenti le vedono entro 30 minuti
  • Scadenza assoluta solo (senza scivolamento) perché non ci interessa quanto spesso si accede

Ho capito che ho colpito: Inizialmente ho usato una chiave stringa "Categorie." Funziona bene fino a quando non si dispone di più controller e uno accidentalmente riutilizza la stessa chiave. Ora uso costanti o tasti fortemente digitati (vedere la sezione precedente per evitare collisioni).

Modello 2: Stato per utente con limiti di dimensione e scadenza scorrevole (attività di traduzione)

Questo stato di traduzione caches per utente. E 'più complesso perché ha bisogno di limiti e dovrebbe rimanere in vita fino a quando l'utente è attivo:

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

Perché questo è diverso:

  • Scadenza scorrevole: Se l'utente continua a controllare lo stato della traduzione, mantenere viva la cache fino a 6 ore max
  • Limiti di dimensione: Mantenere solo 5 attività più recenti per utente per prevenire il gonfiore della memoria
  • Gestione manuale: Limitare esplicitamente la dimensione perché MemoryCache non impone limiti di conteggio degli elementi di livello entry (solo la dimensione complessiva della cache)

Errore che ho fatto: Inizialmente non ho limitato il numero di attività. Un utente di alimentazione ha attivato 50 più traduzioni e ho avuto una perdita di memoria. Ora tengo max 5 per utente.

Trade-off: Questo non scala a milioni di utenti. Se questo diventa un problema, mi trasferirei a IDistributedCache (Redis) o archivierei nel database con un indice su userId + startTime.

Modello 3: Cache con osservabilità (Metrics cacheing)

Questo caches analytics metriche e traccia l'efficacia della cache utilizzando Serilog tracciamento:

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

Che cosa rende questa produzione-pronta:

  • Osservabilità: SerilogTracing attività traccia cache hit/miss rate e registra il conteggio delle metriche
  • Chiave composita: Include intervallo di date e prefisso per evitare collisioni chiave
  • Formattazione data: Usi yyyyMMdd formattazione in tempi così diversi nello stesso giorno condividere la cache
  • Manipolazione null: Restituisce nulla se il servizio esterno fallisce; non fallisce la cache
  • Caching filtrato: Caches il risultato filtrato, non la risposta grezza

Evoluzione: Inizialmente ho cached per 10 minuti. Ma le metriche di Umami sono pesanti per recuperare e non cambiare molto, quindi 1 ora va bene. Ho scoperto questo guardando i dati SerilogTracing e vedendo manca cache eccessiva.

Monitoraggio in azione: In Seq (il mio aggregatore di log), posso interrogare:

ActivityName = "GetMetricsWithPrefix" and CacheHit = false

Questo mi dice il mio tasso di mancanza di cache. Se è alto, regolare TTL o strategia chiave.

Confrontare i tre approcci

Schema Caso d'uso Scadenza Controllo delle dimensioni Osservabilità
Categorie Global, rare-changing Assoluto di 30 min Non necessario (piccolo) Registrazione di base
Attività di traduzione Per utente, delimitato Assoluto 6h + 1h scorrevole Manuale (5 elementi max) Nessuno (dovrebbe aggiungere!)
MetricsCity name (optional, probably does not need a translation) Chiamate esterne costose Assoluto 1h Naturale (finestrato a tempo) Tracciato completo

Lezioni chiave:

  1. Avviare semplice (modello 1), aggiungere complessità solo quando necessario
  2. Pensare sempre all'utilizzo della memoria cache - aggiungere limiti di dimensione per le cache per utente
  3. Per operazioni costose, aggiungere osservabilità dal primo giorno
  4. Regolare TTL in base alla frequenza effettiva di cambio dati, non congetture

Quando non uso IMemoryCache:

  • Per lo stato di autenticazione dell'utente (richiede invece nel cookie auth)
  • Per il carrello della spesa (userebbe DB + cache distribuita in produzione)
  • Per il contenuto del blog post (già in DB, caricato una volta per richiesta)
  • Stato cross-server (necessita di IDistributedCache/Redis)

  • Campo di applicazione: tutte le richieste fino alla scadenza/firma
  • Uso per: identità, ruoli/permessi grossolani, una piccola quantità di dati di profilo
  • Contrassegni: dimensione dei cookie; sovraccoperta di don-tit. I reclami dovrebbero essere stabili.

Imposta l' auth dei cookie:

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

Iscrizione con crediti (MVC/minimale):

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

Leggi i reclami (qualsiasi pila):

[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

  • Ambito di applicazione: cliente porta token; server apolide
  • Uso per: SPA/mobile/API, dominio trasversale, microservizi
  • Trade-off: dimensione token; rotazione / aggiornamento; memorizzare crediti minimi, utilizzare l'introspezione se necessario
builder.Services.AddAuthentication("Bearer")
   .AddJwtBearer("Bearer", o =>
   {
       o.Authority = "https://demo.identityserver.io"; // example
       o.Audience = "api";
       o.RequireHttpsMetadata = true;
   });

Uso:

[Authorize(AuthenticationSchemes = "Bearer")]
app.MapGet("/secure", () => "ok");

Panoramica delle sirene:

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

Stato durevole: banca dati e amici

  • Ambito di applicazione: per sempre (fino a quando non lo cancelli)
  • Utilizzo per: tutto ciò che non deve essere perso: carrelli, ordini, profili, flussi di lavoro a lungo termine
  • Modelli: CRUD standard con EF Core; CQRS; approvvigionamento di eventi; pattern outbox per l'affidabilità

Schizzo del nucleo dell'impronta ambientale:

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

Caching di risposta, ETag e richieste condizionali

  • Non strettamente il trasporto dello stato di NPH, ma riduce il lavoro ripetuto lasciando che il client/proxies riutilizzi le risposte precedenti. Spesso abbinato allo stato di query/route.
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();

Esempio ETag:

app.MapGet("/resource", (HttpContext ctx) =>
{
    var version = "W/\"abc123\""; // compute based on data hash
    ctx.Response.Headers.ETag = version;
    if (ctx.Request.Headers.IfNoneMatch == version)
        return Results.StatusCode(StatusCodes.Status304NotModified);
    return Results.Text("payload");
});

ResponseCache vs OutputCache: Utilizzo del Real-World nella produzione

Uso ResponseCache e OutputCache insieme sul mio blog per diversi scopi. Ecco perché si potrebbe desiderare entrambi e come differiscono.

La confusione: Due attributi di cache?

ASP.NET Core ha due sistemi di cache simili:

  1. ResponseCache (HTTP cacheing): Imposta le intestazioni HTTP (Cache-Control, Vary) dire ai browser e ai CDN come fare la cache e
  2. OutputCache (server-side): Caches l'output reso sul server per saltare l'esecuzione dell'azione interamente

Si completano a vicenda. ResponseCache gestisce la cache client/CDN; OutputCache gestisce la cache lato server.

Il mio blog post azione (entrambe cache applicate)

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

Cosa succede quando qualcuno chiede /blog/my-post:

  1. Controlla prima OutputCache: Ho una risposta cache per my-post + en Linguaggio?

    • Colpisci: Ritorna HTML cached, il metodo di azione non viene mai eseguito (veloce! ~1ms)
    • Signorina: Esegui azione, renderizza vista, risultato cache per 3600 secondi (1 ora)
  2. ResponseCache imposta le intestazioni: Dopo OutputCache genera la risposta, ResponseCache aggiunge:

    Cache-Control: public, max-age=300
    Vary: hx-request
    
  3. Caching del browser: Browser caches la risposta per 300 secondi (5 minuti). Le richieste successive da parte dello stesso utente non hanno nemmeno colpito il server.

  4. Caching CDN (se si utilizza Cloudflare/Fastly): cache CDN per 5 minuti. Gli utenti in tutto il mondo hanno colpito il CDN, non il mio server.

Perché diverse durate? (300 vs 3600)

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

Motivazione:

  • La cache del server è più lunga (1 ora): Controllo il mio server; posso eliminare la cache se aggiornerò un post
  • La cache dei clienti è più corta (5 minuti): Non posso eliminare facilmente browser utente o CDN; 5 minuti è una finestra di stabilità ragionevole
  • Trade-off: Se edito un post, appare un nuovo contenuto:
    • lato server: Immediatamente (Posso invalidare la cache)
    • CDN/browser: entro 5 minuti (o spurgo manualmente CDN)

Evoluzione: Inizialmente avevo entrambi a 5 minuti. Ma questo significava che il mio server stava re-rendering ogni 5 minuti, anche se il contenuto cambia raramente. Ora:

  • Il server serve felicemente HTML cached per 1 ora
  • I clienti ricevono contenuti freschi ogni 5 minuti

VaryByHeader per HTMX

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

Perche' questo e' importante: Le richieste HTMX includono hx-request: true Intestazione. Rendo risposte diverse:

  • Richiesta completa: Pagina HTML completa con layout
  • Richiesta HTMX: Vista parziale senza layout

Senza VaryBy, la cache restituisce il formato sbagliato. Con VaryBy, I cache due versioni di ogni pagina.

Esempio:

User requests /blog/my-post → Cache key: "blog/my-post:en:hx=false" → Full HTML cached
HTMX requests /blog/my-post → Cache key: "blog/my-post:en:hx=true" → Partial HTML cached

VaryByQueryKeys per la lingua e la paginazione

[ResponseCache(VaryByQueryKeys = new[] { nameof(slug), nameof(language) })]
[OutputCache(VaryByQueryKeys = new[] { nameof(slug), nameof(language) })]

Problema senza questo: /blog/my-post?language=fr servirebbe la versione in cache inglese.

Con VaryByQueryKeys: Voci separate della cache:

  • /blog/my-post?language=en → La chiave Cache include "en"
  • /blog/my-post?language=fr → La chiave Cache include "fr"

Utilizzo reale dalla mia lista di blog:

[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, /* ... */)

Avviso di esplosione della cache: Ogni combinazione unica di parametri = voce separata della cache:

  • page=1&pageSize=20&language=en → Una voce
  • page=2&pageSize=20&language=en → Un'altra voce
  • page=1&pageSize=10&language=en → Un'altra voce

Mitigazione:

  • Dimensione cache massima ragionevole (OutputCache auto-evicts meno recente-utilizzato)
  • Parametri comuni in cache (pagina 1, pagina predefinitaDimensione)
  • Combinazioni non comuni potrebbero perdere la cache (accettabile)

Configurazione richiesta

ResponseCache lavora fuori dalla scatola, ma per OutputCache è necessario configurare:

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

Quando OutputCache non aiuta

OutputCache viene saltato per:

  • Richieste autenticate (diversi utenti vedono diversi dati)
  • POST/PUT/DELETE (solo cache GET/HEAD)
  • Responses with Set-Cookie intestazione
  • Risposte che impostano esplicitamente Cache-Control: no-store

Esempio in cui non lo uso:

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

Misurare l'efficacia

Uso Prometheus metriche (espostato tramite la mia app) per tracciare:

// Pseudo-code for metrics
cache_hits_total{cache="output"} 45230
cache_misses_total{cache="output"} 892

Il mio tasso di cache colpito: ~98% per i post del blog (la maggior parte del traffico colpisce gli stessi post popolari ripetutamente).

Impatto:

  • Senza cache: ~50ms tempo medio di risposta (query DB + rendering Markdown)
  • Con OutputCache: ~1-2ms per le risposte in cache
  • 25x speedup

Quando momentaneamente disabilito la cache

A volte sto debug e ho bisogno di risposte nuove ogni volta:

// During development, comment out caching
// [ResponseCache(Duration = 300, ...)]
// [OutputCache(Duration = 3600, ...)]
public async Task<IActionResult> Show(string slug, string language = "en")

O utilizzare impostazioni specifiche per l'ambiente:

#if DEBUG
    // No caching in development
#else
    [ResponseCache(Duration = 300, ...)]
    [OutputCache(Duration = 3600, ...)]
#endif

Approccio migliore: Usa configurazione:

[ResponseCache(Duration = responseCacheDuration, ...)]

dove responseCacheDuration è 0 in sviluppo, 300 in produzione.

Pro e contro di questa strategia dual-cache

Pro:

  • Efficienza del server: OutputCache riduce il carico CPU/DB del 98%
  • Vantaggi Client/CDN: ResponseCache riduce la mia larghezza di banda e migliora la latenza globale
  • Flessibilità: TTL diversi per server vs client
  • Risparmio sui costi: Meno richieste DB, meno larghezza di banda

Punti negativi:

  • Instabilità: Gli aggiornamenti dei contenuti impiegano fino a 5 minuti per propagarsi ai clienti
  • Complessità della invalidazione della cache: L'aggiornamento di un post richiede l'annullamento delle cache sia server che CDN
  • Uso della memoria: OutputCache contiene l'HTML reso nella memoria del server
  • Debug confusione: A volte dimentica cache è su e si chiede perché i cambiamenti non appaiono

Quando saltavo la cache:

  • Dati in tempo reale (prezzi delle scorte, sport vivi)
  • Contenuti personalizzati (raccomandazioni specifiche per l'utente)
  • Pagine a basso traffico (caching overhead > benefit)
  • Pagine che cambiano molto frequentemente

Il mio verdetto: Per un blog con contenuti per lo più statici e alto rapporto lettura/scrittura, la doppia cache è una vittoria enorme. Non lo userei su pannelli di amministrazione o dashboard con dati in rapida evoluzione.


ViewBag, ViewData e TempData: Controller-to-View State (e perché per lo più evito due di loro)

Questi tre sono spesso confusi. Ecco come differiscono e ciò che effettivamente uso nella produzione.

I tre amigos confrontati

// 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");
Caratteristica Visualizzazione dati Visualizzazione busta TempData
Tipo ViewDataDictionary dinamica ITempDataDictionary
Durata della vita Richiesta corrente Richiesta corrente Un reindirizzamento
Accesso chiave Chiavi stringa Sintassi proprietà Chiavi stringa
Sicurezza del tipo Nessuno (fusione necessaria) Nessuno (dinamica) Nessuno (fusione necessaria)
Controllo dell'orario di compilazione No No No
Sopravvive reindirizzare No No

Che cosa effettivamente uso: ViewBag per i dati di layout globale

Nel mio blog, utilizzo ViewBag esclusivamente per il passaggio dei dati dai controllori al layout condiviso (analisi, categorie, ecc.):

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

Poi nel mio layout (_Layout.cshtml):

@if (ViewBag.Categories is List<string> categories)
{
    <nav>
        @foreach (var cat in categories)
        {
            <a asp-controller="Blog" asp-action="Category" asp-route-category="@cat">@cat</a>
        }
    </nav>
}

@if (!string.IsNullOrEmpty(ViewBag.UmamiPath))
{
    <script async src="@ViewBag.UmamiScript"
            data-website-id="@ViewBag.UmamiWebsiteId"></script>
}

Perché questo modello funziona:

  • Globale: Ogni pagina ha bisogno di categorie nav e analytics
  • Calcolato una volta: BaseController viene eseguito prima di ogni azione
  • Ottimizzazione HTMX: Salta lo script di analisi su richieste parziali (HTMX non ha bisogno di re-iniettarlo)
  • Cached: Le categorie sono cached (vedi il mio modello IMemoryCache sopra), in modo da non colpire DB ogni richiesta

Errore che ho fatto in anticipo: Stavo impostando ViewBag.Categories in ogni singolo metodo di azione. DRY violazione e facile da dimenticare. OnActionExecutionAsync nel controller di base risolto questo.

VisualizzaBag per i dati specifici della pagina (modello accettabile)

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

In considerazione di quanto segue:

@{
    ViewData["Title"] = ViewBag.Title; // Standard MVC convention for <title>
}

<h1>Category: @ViewBag.Category</h1>

Va bene perche'

  • Semplici valori scalari (stringa, int)
  • Usato solo nella vista, non passato intorno
  • Alternativa sarebbe aggiungere Title e Category proprietà per ogni modello di visualizzazione

Cosa evito: Oggetti complessi in ViewBag

Anti-pattern:

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

Problemi:

  • Nessuna sicurezza dei tempi di compilazione (typo) ViewBag.Usr fallisce al runtime)
  • Difficile tracciare quali dati sono disponibili in vista
  • Rende più difficile il test (bisogno di ispezionare il dizionario ViewBag)
  • No IntelliSense

Meglio: modelli di visualizzazione fortemente digitati:

// 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: Non lo uso (ed ecco perché)

Caso di utilizzo standard di TempData:

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

Perché non uso TempData nel mio blog:

  1. Uso HTMX invece di redirect: I miei moduli inviano tramite HTMX e restituiscono visualizzazioni parziali con messaggi di successo/errore in linea. Nessun reindirizzamento = nessuna necessità di TempData.
[HttpPost]
public async Task<IActionResult> Submit(ContactViewModel model)
{
    if (!ModelState.IsValid)
        return PartialView("_ContactForm", model); // Show errors inline

    await sender.SendEmailAsync(contactModel);

    // Return success view directly (no redirect)
    return PartialView("_Response", new ContactViewModel
    {
        Email = model.Email,
        Name = model.Name,
        Comment = "Message sent!"
    });
}
  1. Per la PRG tradizionale (Post-Redirect-Get), Userei TempData. Ma preferisco evitare reindirizzamenti quando possibile per meglio UX.

Quando TempData ha senso:

  • Tradizionale app MVC con pagina intera reindirizza dopo POST
  • Maghi multi-passo in cui si reindirizza tra i passaggi
  • Messaggi lampeggianti dopo reindirizzamenti di autenticazione

TempData gotcha: Per impostazione predefinita supportata da cookies (dal ASP.NET Core 2.0). Se metti oggetti di grandi dimensioni in TempData, stai gonfiando il cookie inviato con ogni richiesta. Per un grande stato, usa Sessione con un backup store o DB.

Albero di decisione rapido

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]

Le mie regole:

  1. VisualizzaBag solo per layout globale (analisi, navigazione, pane grattugiato)
  2. VisualizzaEtichetta per titoli/ruote di pagine semplici (opzionale; potrebbe usare ViewModel)
  3. Mai VisualizzaEtichetta per oggetti complessi (usare ViewModels)
  4. Mai Visualizza dati (ViewBag ha una sintassi più piacevole)
  5. TempData solo se hai davvero bisogno di PRG (Io no, grazie alla HTMX)

Schema: Wizards e flussi multi-step

Quale detentore dello Stato usare?

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
  • Piccola sessione singola: Sessione o TempData tra i passi.
  • Cross-device/long-running: continua a DB, porta una chiave nell'URL.

Esempio (DB + route key):

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

Modello: Messaggi flash con TempData

  • Impostare in POST; leggere una volta dopo redirect.
TempData["Flash"] = "Profile saved";
return RedirectToAction("Index");

Vista rasoio:

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

Schema: Carrello

  • Piccoli carrelli: Sessione (se la tua scala è modesta e hai una sessione appiccicosa/distribuita).
  • Carrelli più grandi/multi-device: DB + cart-id in cookie o URL. Cache per velocità.
flowchart LR
  U[User] -- cart-id cookie --> S[Server]
  S --> DB[(Cart Table)]
  S <--> Cache[Distributed Cache]

Lista di controllo Sicurezza, Privacy e Compliance

  • Convalida tutto lo stato fornito dal client: query, header, form, cookies, richieste di JWT.
  • Proteggere lo stato sensibile memorizzato dal client: utilizzare la protezione dei dati per i cookie emessi; non memorizzare mai segreti nelle stringhe di query.
  • Imposta i flag dei cookie: Secure, HttpOnly, SameSite, IsEssential (se richiesto dal consenso/esigenza funzionale).
  • Rigenerare i cookie di autenticazione sulle modifiche dei privilegi; mantenere i reclami minimi.
  • Cifrare a riposo per i server-side store se necessario; garantire la rotazione delle chiavi (tasti Protezione dati, chiavi firma JWT).
  • GDPR/CCPA: fornire percorsi di esportazione/eliminazione dei dati agli utenti; ridurre al minimo la conservazione.

Matrice di decisione (lamiera di calore)

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
  }

Scelte rapide:

  • Hai bisogno di un flash reindirizzato?
  • Bisogno di wizard su più richieste in una sessione? Sessione (o chiave DB + se long-running/multi-device).
  • Hai bisogno di scalabilità e API apolide? JWT per l'identità, DB / DistributedCache per lo stato.
  • Hai bisogno di passare i dati solo all'interno della pipeline? HttpContext.Items.
  • Hai bisogno di cache per le ricerche calcolate? IMemoryCache localmente; IDistributedCache attraverso una fattoria.

MVC vs Razor Pages vs Minimal API: Stesse fondazioni, diverse forme

Tutti e tre gli stack si trovano sulle stesse primitive (HttpContext, model binding, auth, data protection). Gli esempi di cui sopra mostrano che le API differiscono per lo più nell'ergonomia:

  • API minime: rilegatura parametri da route/query/body/reclames; ritorno Results.*.
  • MVC: attributi, filtri, rilegatura di modelli in parametri di azione/modelli di visualizzazione.
  • Pagine Razor: gestori di pagine con proprietà limitate e aiutanti di tag per la generazione di link/forme.

Tutti condividono gli stessi meccanismi statali discussi in questa sede.


Pitfalls e anti-patterns

  • Conservazione di dati di grandi dimensioni o sensibili in cookie o TempData.
  • A seconda della cache in-memory per la correttezza (si tratta di una cache, non la verità).
  • Autorizzazione di costruzione basata sui flag route/query inviati dal client senza controlli del server.
  • Cookie di auth o JWT sovra-stuffi con rivendicazioni volatili.
  • Sessione senza strategia di distribuzione (lavora localmente, rompe in scala).

Real-World State Management: quello che effettivamente uso (e non uso)

Dopo avervi mostrato tutte queste opzioni, ecco la mia onesta valutazione di ciò che funziona in produzione per la mia piattaforma blog.

Il mio stack di gestione dello stato (in ordine di frequenza)

Meccanismo Frequenza Casi d'uso Soddisfazione
ImemoryCache Altissimo Categorie, metriche, attività di traduzione Essential Hoppenstedt
OutputCache Alta Rendered blog posts, liste Vittoria enorme perf
ResponseCache Alta Intestazioni di cache HTTP Funziona con OutputCache
VisualizzaEtichetta Medium Analytics settings, page titles OK for simple stuff
Auth Claims Medium User identity, admin flag Right tool for auth
Route/Query Medium Pagination, filtration, slugs Stateless and linkable
Banca dati Medium Blog post, commenti, stato Sorgente di verità
Cookie Basso Preferenze dell'utente (futuro) Non sono ancora necessari
Sessione Mai - • Setup negozio senza distribuzione
TempData Mai - • HTMX elimina la necessità
HttpContext.Items Mai - Non hanno avuto caso d'uso
IDistributedCache Mai - • Single server (per ora)

Pro/cons dettagliati dall'esperienza di produzione

IMemoryCache

Per cosa lo uso:

  • Elenco categorie (globale, 30 min TTL)
  • Traduzione per utente task tracking (6h assoluto + 1h scorrevole)
  • Risposte API esterne (metriche Umami, 1h TTL)

Professionisti in pratica:

  • Veloce lampeggiante (in-processo)
  • Nessuna serializzazione
  • Ridotto carico DB/API di ~95%
  • Facile da implementare e comprendere
  • Osservabilità tramite SerilogTracing

Contro che ho colpito:

  • Le perdite di memoria se non si limita cache per utente
  • Cancellato il riavvio dell'app (accettabile per il mio caso d'uso)
  • Non condiviso tra server (fine per singolo esempio)
  • La invalidazione della cache è manuale (necessario rimuovere () esplicitamente)

Limite di scala: Se arrivassi a più server, avrei bisogno di IDistributedCache (Redis) per lo stato condiviso. Per ora, un singolo server + cache di memoria è perfetto.

OutputCache + ResponseCache

Per cosa lo uso:

  • Rendering post del blog (1 ora di server, 5 min client/CDN)
  • Lista di blog/pagine di categoria
  • Dati di calendario

Professionisti in pratica:

  • 25x speedup (50ms → 2ms)
  • Scala ad alto traffico senza rompere un sudore
  • TTL separati per server vs client
  • Funziona perfettamente con HTMX (VaryByHeader)
  • Impatto misurabile tramite Prometheus metriche

Contro che ho colpito:

  • Debug confusione (forget cache is on)
  • Esplosione cache con molte combinazioni di parametri di query
  • Finestra di stalness (5 min per i clienti)
  • Necessità di invalidare manualmente gli aggiornamenti dei contenuti

Meglio per: Applicazioni leggere con contenuti statici per lo più. Non buone per dati personalizzati o in tempo reale.

VisualizzaEtichetta

Per cosa lo uso:

  • Dati di layout globale (analytics, categorie)
  • Titoli delle pagine

Professionisti in pratica:

  • Semplice e veloce per i dati a livello di layout
  • Impostare una volta in BaseController, disponibile ovunque
  • Funziona bene con l'elenco delle categorie in cache

Contro che ho colpito:

  • Nessun tipo di sicurezza (typos non riesce a runtime)
  • Tentativo di sovrautilizzo per dati complessi
  • Difficile da testare

Seguo l'articolo: VisualizzaBorsa per scalari semplici. Oggetti complessi vanno in ViewModels.

Auth Claims [49]

Per cosa lo uso:

  • ID utente, nome, email, URL avatar
  • Bandiera dell'amministratore (checking sub reclamo contro la config)

Professionisti in pratica:

  • Sicuro (cookie firmato, protezione dei dati)
  • Automatico con ASP.NET Core Identity/OAuth
  • Disponibile tramite User.Claims ovunque
  • La scadenza scorrevole mantiene gli utenti registrati

Contro che ho colpito:

  • Limite di dimensione dei cookie (non sovraccaricare le richieste)
  • I crediti sono statici fino al re-login
  • Se aggiungo un reclamo (come "IsEditor"), è necessario ristampare il cookie di autenticazione

Migliori pratiche: Mantenere i reclami minimi e stabili. Non mettere i dati che cambiano frequentemente nei reclami.

Cose che non uso (e perché)

Sessione (mai usata):

  • Richiederebbe il deposito distribuito (Redis)
  • Aggiunge complessità per un beneficio minimo
  • I miei casi d'uso sono meglio serviti da:
    • Auth claims (identità)
    • IMemoryCache (stato di breve durata)
    • Banca dati (stato durevole)

TempData (mai usato):

  • HTMX eliminato il pattern Post-Redirect-Get
  • Forms restituisce viste parziali con messaggi in linea
  • Nessuna necessità di sopravvivere reindirizzamenti

HttpContext.Items (mai usato):

  • Non ho avuto un caso d'uso per lo stato per-richiesta
  • Il mio middleware non calcola i valori per i controller
  • Se avessi bisogno di rintracciare l'inquilino, lo userei.

IDistributedCache (mai usato):

  • Implementazione di un singolo server
  • IMemoryCache soddisfa tutte le esigenze
  • Userebbe Redis se scalassi su più server

Evoluzione del mio approccio

Fase 1 (iniziale): Non caching a tutti. Ogni richiesta ha colpito il database e reso Markdown. Ha funzionato bene per il traffico basso.

Fase 2 (prima ottimizzazione): Aggiunto IMemoryCache per le categorie. Sega immediata riduzione del carico DB. Tenerlo semplice: 30 min TTL, nessuna logica fantasia.

Fase 3 (scaling up): Aggiunto OutputCache per i post del blog quando il traffico è aumentato. Miglioramento delle prestazioni massivo. Errore iniziale: cached solo per 5 minuti. Aumento a 1 ora dopo il monitoraggio ha mostrato raramente cambiamenti di contenuto.

Fase 4 (osservabilità): Aggiunto SerilogTracciamento alla cache delle metriche. Le mancate cache scoperte erano alte a causa della formattazione della data nelle chiavi. yyyyMMdd Il tasso di Hit è passato dal 60% al 95%.

Fase 5 (integrazione HTMX): Aggiunto VaryByHeader per hx-request. Inizialmente dimenticato questo e servito pagine complete per le richieste HTMX. Debugging incubo fino a quando non ho capito.

Stato attuale: Happy with the stack. IMemoryCache + OutputCache + ResponseCache gestisce il 98% delle mie esigenze di gestione dello stato. Database per lo stato durevole.

Consigli per la tua app

Inizia da qui:

  1. Parami di rotta/query per la navigazione/filtraggio (sempre prima apolide)
  2. Auth reclames for identity
  3. Banca dati per tutto ciò che deve persistere
  4. IMemoryCache per i dati di lettura pesante
  5. OutputCache per pagine costose da rendere

Aggiungere se necessario: 6. Sessione (solo se è necessario avere stato di conversazione lato server) 7. IDistributedCache (solo quando si scala su più server) 8. Cookies (per le preferenze lato client, consenso)

Evitare:

  • Oggetti complessi in VisualizzaBag/TempData
  • Sessione senza store di supporto distribuito
  • Caching di dati specifici per l'utente o che cambiano frequentemente
  • Ottimizzazione precoce (misura prima!)

Lezioni chiave:

  • Avvia semplice, aggiungi complessità solo quando le misurazioni mostrano che ne hai bisogno
  • L'osservabilità è critica (sappiate che la vostra cache colpisce i tassi!)
  • Pensa ai limiti di scala in anticipo (le cache per utente hanno bisogno di limiti)
  • Provare a fondo la nullità della cache (la storia è un vero problema)
  • Documenta le tue scelte TTL (futuro ti chiederai "perché 30 minuti?")

Avvolgi-up

Stato in applicazioni web non è una taglia adatta a tutti. Scegli l'opzione più leggera che soddisfa le tue esigenze, preferisci modelli apolide quando puoi, ed essere esplicito sulla sicurezza e il ciclo di vita.

Se volete approfondire il flusso di questi pezzi attraverso la pipeline, vedete la mia serie iniziare con Parte 1: Panoramica e Fondazione e soprattutto il middleware e le parti di routing.

Felice edificio.


Appendice: Altri esempi copiabili (Rifinimenti)

Questi esempi approfondiscono le sezioni precedenti con dettagli di produzione-grado è possibile incollare in net9 modelli minimi, MVC, o Razor Pages.

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

Suggerimento: Nelle implementazioni multi-node, persistono le chiavi di protezione dei dati (ad esempio, in un file system condiviso, Redis, o Azure Key Vault) in modo che i cookie possano essere letti attraverso le istanze.

Antiforgerie nelle pagine MVC, Razor e API minime

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: decorare azioni con [ValidateAntiForgeryToken] e uso @Html.AntiForgeryToken() in forme.
  • Pagine rasoio: abilitato per impostazione predefinita sui post del modulo; uso asp-antiforgery="true" se necessario.
  • Minimale: convalida tramite IAntiforgery come mostrato.

Sessione con Redis e scorrevole vs scadenza assoluta

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

Conservare solo piccoli dati comprimibili. Persistere carrelli/ordini reali a un DB.

IMemoryCache con opzioni di entry e callback di sfratto

builder.Services.AddMemoryCache();

app.MapGet("/fx/{pair}", (IMemoryCache cache, string pair) =>
{
    var key = $"fx:{pair.ToLowerInvariant()}";
    return Results.Ok(cache.GetOrCreate(key, entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
        entry.SlidingExpiration = TimeSpan.FromMinutes(2);
        entry.Size = 1; // enable size-based eviction if configured
        entry.RegisterPostEvictionCallback((k, v, reason, state) =>
        {
            Console.WriteLine($"Evicted {k} because {reason}");
        });
        return 0.92m; // fetch from external service in real life
    }));
});

IDistributedCache get-or-set con jitter per evitare stampe

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

Per il bloccaggio robusto, preferisci Redis primitives (SET NX EX) tramite StackExchange.Redis.

Emissione e convalida dei JWT localmente (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");

Aggiornamenti condizionali con ETags (If-Match)

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

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

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

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

TempData: oggetti complessi via JSON

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

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

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

EF Core Concurrency token

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    [Timestamp] public byte[] RowVersion { get; set; } = default!;
}

// On update
try
{
    await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
    return Results.StatusCode(StatusCodes.Status412PreconditionFailed);
}
app.MapPost("/promote", async (HttpContext ctx) =>
{
    var u = ctx.User;
    var claims = u.Claims.ToList();
    claims.Add(new Claim(ClaimTypes.Role, "Editor"));
    var id = new ClaimsIdentity(claims, "Cookies");
    await ctx.SignInAsync("Cookies", new ClaimsPrincipal(id));
    return Results.Ok();
});

Ciò dovrebbe coprire le lacune: maggiori inadempimenti in materia di sicurezza, disponibilità multinodo e modelli reali di cache, gettoni e richieste condizionali.


Deep Dive: HttpContext.Items (Practical Patterns and Helpers)

HttpContext.Items è una borsa per-richiesta (IDictionary<object, object?>) che vive solo per la durata di una singola richiesta. È perfetta per passare i valori calcolati dai middleware/filter ai tuoi endpoint, controller e Razor Pages senza toccare lo stato globale o i negozi long-lived.

  • Ciclo di vita: creato all'inizio della richiesta; scartato al completamento della risposta.
  • Campo d'applicazione: richiesta attuale solo di non incrociare mai reindirizzamenti o lavori di background.
  • Prestazioni: O(1) di ricerca; ideale per la cache a richiesta.
  • Sicurezza: solo lato server; non visibile al client.

Perché gli oggetti invece di

  • Sessione/TempData: quelle richieste incrociate e introdurre problemi di distribuzione. Articoli è effimero e scala-friendly.
  • Servizi DI Scoped: Usali per il comportamento e le dipendenze condivise. Gli elementi sono migliori per i valori ad-hoc, calcolati (tenant, utente locale, flag di funzionalità) e per le cache di richiesta.
  • HttpContext.Caratteristiche: Per le funzioni framework/transport-level (IEndpointFeature, IHttpUpgradeFeature).

Evitare collisioni chiave: tasti fortemente digitati

Poiché Oggetti usa chiavi oggetto, preferiscono chiavi oggetti statici privati o un tipo di chiave dedicata per evitare collisioni di nomi.

public static class ItemKeys
{
    public static readonly object TenantId = new();
    public static readonly object UserLocale = new();
    public static readonly object PerRequestCache = new();
}

Oppure creare un wrapper digitato con le estensioni:

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

Schema: Calcola in middleware, consuma in endpoint/controller/pagine

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

Schema: cache per richiesta per evitare lavori ripetuti

Utilizzare elementi come una cache piccola così ripetuto legge all'interno della stessa richiesta don't re-hit database / servizi.

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

Note:

  • Threading: Una singola richiesta tipicamente esegue su un percorso logico; Items isn-safe thread-safe per writes paralleli. Se si avviano attività parallele che condividono Items, aggiungere la propria sincronizzazione.
  • Dimensione: mantenere i valori piccoli ed economici per calcolare/serializzare.

Modello: Filtri che popolano gli elementi (pagine MVC/Razor)

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

Schema: Arricchire i tronchi senza allocazioni ovunque

Computa una volta, poi leggi nell'ambito di registrazione o middleware.

app.Use(async (ctx, next) =>
{
    var correlationId = ctx.Request.Headers["X-Correlation-Id"].FirstOrDefault() ?? Guid.NewGuid().ToString("n");
    ctx.Items["CorrelationId"] = correlationId; // string key acceptable for app-local use

    using (logger.BeginScope(new { CorrelationId = correlationId }))
    {
        await next(ctx);
    }
});

Quando non usare elementi

  • Dati necessari dopo il reindirizzamento o attraverso le richieste (usare invece TempData/Sessione/DB).
  • Singoli app o cache a richiesta incrociata (usare IMemoryCache/IDistributedCache).
  • Valori che appartengono all'identità/autorizzazione (utilizzare crediti/politiche).

Regola veloce: Se si calcola durante questa richiesta e si legge all'interno di questa richiesta dal proprio codice, Articoli è l'ideale.

logo

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