Back to "Maintien de l'état entre les demandes dans ASP.Core NET:Un guide pratique ,NonM SK3Guide des non-sens MSC4MVCMST5Pages à razorMSL6API minimalesMSP7"

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

Maintien de l'état entre les demandes dans ASP.Core NET:Un guide pratique ,NonM SK3Guide des non-sens MSC4MVCMST5Pages à razorMSL6API minimalesMSP7

Sunday, 09 November 2025

Mise à jour (2025-11-10): Ajout d’exemples plus pratiques de ma base de code réelle montrant des modèles réels -world patternsM SK2 trade-offsMSC4 gotchasMSC5 et l’évolution de la gestion simple à une gestion d’État sophistiquéeMST6 Comprend des modèles IMemoryCache détaillées MST7 Utilisation du ViewBag SST8good and badM ST9 ResponseCacheM st10OutputCache strategiesS ST11 and lessons learned from productionS st12

Introduction

HTTP est connu pour son absence d’état. Votre application … n’est pas. Les utilisateurs s’inscrivent à l’aperçuM SK3 ajoutent des articles dans les paniersMSC4 font le passage entre pagesMST5 retournent demainM ST6 et vous attendez que vous vous souvenez de ce qui se passeM st7 Dans ASPMst8 Core NET M St9MVCMSt10 Pages RazorM SST11 API minimalesMSST12 il y a beaucoup de façons de préserver et de transférer l’état entre demandesMSS13 Certains sont par,MSSS14requête seulement,MSS15 Certains restent pendant une sessionM SS16 Certains vivent au client,M SSS17 Certains ont été distribués et survivront aux relais du serveur,M SS18 Chaque choix comporte un échangeMSP19 des compromis en matière de sécuritéMSR20 la performanceM SR21 l’échelle,MSR22 et l’ergonomie du développeur,M SR23

Ce post catalogue les options, montre des exemples concrets, copieM SK2exemples imprimables dans les trois piles , et vous donne une orientation décisionnelle pour choisir l'outil qui convient au travailMSC4

NOTE: C'est une partie des expériences que j'ai réalisées avec l'A.I. M SK1 rédaction assistée) + ma propre éditionMST4 La même voixM ST5 le même pragmatismeM st6 quelques doigts plus rapidesMst7

Le paysage à un coup d'œil

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-carryed: queueM SK2 route , headersMNK4 formesMMK5 cookiesMRK6 JWTMKK7 Évalue horizontalement mais est visible au client MBK8 doit être validéMGK9 signéMKB10 encrypté le cas échéantMKZ11
  • Server-carriedM SK1 TempData, SessionMST3 cachesMSC4 DBMSL5 Exige une stratégie d'affinité ou de distributionMSP6
  • Per-requestM SK1 HttpContexte .Items - utile pour transmettre les données internement pendant une seule demandeMSC4

Règles d’or (avant de nous plonger dans les APIs)

  1. Sécurité première: Les fuites d’État constituent une menace réelle . Server-État de côté (SessionM SK2 dans les caches de mémoire -MSC4 peuvent déverser entre les utilisateurs par des erreurs de codageMST5conditions de course MST6 ou gestion incorrecte du cycle de vieM ST7 Les modèles sans état ne sont pas seulement plus scalablesM st9ilsMst10ils sont plus sécuritairesMstr11
  2. Choisissez "serveur sans état"patrèmes lorsque vous avez besoin d'établir une échelle horizontale (état ponctuel à des tokens clientsM SK3entrepôts durablesMST4 ou caches distribuésM ST5
  3. Ne pas vous confier au client-État contrôlé.ValiditéM SK2signe , etMNK4ou chiffrerMRK5
  4. Maintenir les données importantes hors des cookies et des headers; ils bloat chaque demande.
  5. Utiliser TempData seulement pour Post-Redirect-Get (PRGM SK3 oneMSC4shots like flash messagesMST5
  6. Utiliser la session seulement lorsque vous devez maintenir le serveur-à côté de l’état de conversation et que vous'avez planifié la distributionM SK2et être paranoïde en ce qui concerne la sécurité
  7. Les revendications sont fondées sur l’identité et le caractère brut de la demande.
  8. Cache n’est pas une source de vérité. Retournez-le avec un entrepôt durable si les données sont importantes.

Per-État de la demande:Contexte d'HTTPM SK2Items

  • Portée: seule demande courante M SK1teardown à la fin du pipeline)
  • Utiliser pour: passer des valeurs calculées entre le middleware et les terminaux/contrôleurs
  • Étalonnage: sans incidence
  • Sécurité: serveur-seulement
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"]

Exemple de Middleware (toutes les piles):

app.Use(async (context, next) =>
{
    var tenantId = context.Request.Headers["X-TenantId"].FirstOrDefault() ?? "public";
    context.Items["TenantId"] = tenantId;
    await next(context);
});
  • Point final minimal de l'API:
app.MapGet("/whoami", (HttpContext ctx) => new { Tenant = ctx.Items["TenantId"] });
  • Contrôleur MVC:
public IActionResult WhoAmI() => Json(new { Tenant = HttpContext.Items["TenantId"] });
  • Traiteur de page Razor:
public IActionResult OnGet() => new JsonResult(new { Tenant = HttpContext.Items["TenantId"] });

Client transitoire-État transporté: Valeurs de route et chaîne de recherche

  • Portée: demande courante ; le client l'effectue explicitement.
  • Utiliser pour: contexte de navigation , filtration, paginationM SK3 identité des ressources
  • Sécurité: doit valider

Exemples

  • APIs minimaux:
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 });
  • Pages de Razor (Orders/DetailsM SK2cshtml .csMSC4
public IActionResult OnGet(int id, int? page)
  => Page();

Générer des liens qui préservent l’état:

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

En-têtes: IDs de corrélation , Keys d'idempotence, Fabrication des drapeaux

  • Portée: demande courante, écho optionnel dans les réponses
  • Utiliser pour les drapeaux : de traçage , d'essai renouvelé
  • Sécurité: traiter comme une entrée non fiableM SK1 valider/liste blanche
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);
});

Idempotency (pattern

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

Formulaires et champs cachés (PRGM SK1

  • Champ d’application: seule demande suivante (client affiche les valeurs de retour )
  • Utiliser pour les étapes de l'assistant:,antiM SK2tokens de faussetéMSC3conserver des petits bits d'état à travers la GRP
  • Sécurité: toujours valider; combiner avec la contrefaçon

Modèle de PRG dans le MVC/Pages de 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 });
}
  • Pages de Razor:
public IActionResult OnPost(SettingsModel model)
{
    return RedirectToPage("/Settings/Summary", new { tab = model.SelectedTab });
}

Cookies: PetitsM SK1 Signés, Parfois encryptés

  • Portée: toutes les demandes du navigateur jusqu’à l’expiration
  • Utilisation pour les préférences: , nonM SK2 drapeaux sensibles, consentementMSC4 cookies d'auth MNK5 section séparéeMRK6
  • Commercialisation-offsM SK1limites de taille (~4KB par cookie),impact sur le rendementMSC4 doit se conformer aux lois relatives au consentement

Exemple minimal d’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"));

MVC/Razor L'utilisation des pages est identique par HttpContext.

Pour assurer l’intégrité/la confidentialité , utiliser le système de protection des données ASP.NET Core pour protéger les charges payantes que vous introduisez dans vos témoins personnelsM SK3


TempData: One-Redirect Message Bus

  • Scope: survie à une redirection
  • Store de sauvegarde: Cookie (defaultM SK2 ou Session
  • Utiliser pour les résumés de validation après le PRG: messages flash,

Établissement (Programme.csM SK2

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

Dans le contrôleur MVC:

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

Dans Razor Page handler:

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

En vue/page:

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

Session: Server-Side Conversational State

  • Champ d’application: session de navigateur M SK1 clé cookie + magasin du serveur)
  • Utilisation pour les compteurs de ralenti
  • Traitement-offsM SK1 nécessite des séances d’attache ou un magasin de soutien distribué ; peut limiter l’échelle

Configurer:

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

Utilisation de session (toute pile):

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

Aides à la session:

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

Une histoire de mise en garde: Quand l’état de session devient le créneau d’étranglement

Une fois, j’ai travaillé sur un énorme projet de TI du gouvernement britannique où l’État des séances a fait usage abusif d’un système. ( Parmi les nombreux autres fautes architecturales, ) est devenu une performance. tout dans la session: préférences de l'utilisateur , multiM SK2 données de formulaires à étapes, résultats de rechercheMNK4 calculs temporairesMRK5 même des recherches cachées qui auraient dû se trouver dans un cache ou une base de données appropriéeMMK6

Le problème: L'état de session était entreposé dans le processus -process (ASP.État de session NET sur le webM SK3configuration, il s'agissait d'une pré-sessionMST5jours de baseM ST6 Chaque requête devait déserialiser des objets de session massifsM st7 À mesure que la charge augmentaitM S ST8 l'État de sessions s'élevait à des dizaines de mégabytes par utilisateurM SP9 Avec des milliers d'utilisateurs concurrentsM SR10 les serveurs ont épuisé leur mémoireMSR11

La solution désespérée: Nous avions voté à l’usine HP de Stuttgart pour effectuer des essais de chargement sur leur Superdome, — et ,. Europe' la plus puissante machine Windows. Il s’agissait d’une bête : dizaines de processeurs Itanium

Le résultat: Même sur le Superdome, nous n’avons pu'atteindre les cibles d’utilisateur concurrentes requisesM SK2 L’architecture de l’état de session a été fondamentalement défectueuse . La mise en échelle verticale ne s’est pas avérée possible ' ne permettait pas de sauvegarder un mauvais design MSC5 La serialisation des séances

Le pesadeau de la sécurité: Plus grave que les problèmes de performance, nous avons découvert une erreur de codage qui a entraîné l’état de session à fuite entre les utilisateurs. Utilisateur A ' les données de session apparaissent occasionnellement dans l’utilisateur B M SK2 la session de . Ce n’était pas seulement embarrassant, mais catastrophique. Personnel du NHS d’accès aux dossiers des patients et aux systèmes cliniques. Nous avons accidentellement créé un mécanisme de violation de la protection des données qui pourrait exposer les renseignements médicaux sensibles à l’ensemble des professionnels de la santé.

Ce qui aurait dû se produire:

  1. Absence d'État par défaut: La plupart des données de cette session ne devraient jamais avoir existé
  2. Base de données pour l'état durable: Multi-Progrès de la formule à l'étape devrait avoir été dans la base de données avec un ID du flux de travail
  3. Cache pour les recherches: Les recherches partagées appartiennent à IMemoryCache ou à un cache distribué
  4. Client-side pour les préférences: Les préférences des utilisateurs auraient pu être dans les cookies ou le stockage local
  5. Séance distribuée si nécessaire: Si la session était vraiment requiseM SK1 Redis-la session appuyée aurait partagé le chargement

La leçon: (même avec les séances imprimées ou les magasins distribués, , vous ' vous continuez de serialiser,/de serialiser sur chaque demande,). L’expérience Superdome a démontré que lancer du matériel dans des problèmes architecturaux est coûteux et souvent futile.

Mais plus important encore: Les bogues d'état de session deviennent des vulnérabilités à la sécurité. Thread -safety issues, race conditionsMSC3 incorrect session ID handlingM SK4these donMST5t just cause performance problemsM ST6 they can leak sensitive data between usersM st7 Dans un contexte de soins de santé MST8or bankingMst9 or any regulated industryMSt10 this is a compliance nightmare and potential criminal liabilityM St11

Conseil moderne:

  • Si vous trouvez que vous avez besoin de plus d’un petit nombre de Kb de données de session, vous avez probablement un problème de conception.
  • Si vous stockez des données sensibles dans une session, vous créez une surface d’attaque de sécurité.
  • Les architectures sans état n’établissent pas simplement une meilleure échelle, mais sont intrinsèquement plus sûres parce qu’il n’y a pas de serveur à détruire, et que l’état côté - est moins dangereux.
  • Réexaminez votre stratégie de gestion de l’État avant de vous rendre à Stuttgart (ou expliquez une violation des données au commissaire à l’information)

Caching: IMemoryCache et IDistributedCache

  • Portée: Processus de serveur M SK1IMemoryCache) ou distribué (IDdistributedCache≥)
  • Utilisation pour : dérivés / données calculées lookups de , M SK3 courtes- état vivant
  • Traitement-offsM SK1invalidation du cache, serialisation pour distribués

ImémoireCache:

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 (e .gM SK2 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-en tant qu’anti--à l’état M SK2avertissement de caractères : s’il doit être durable ou authentique, le stocker dans une base de données et optionnellement le cacher

Choix entre IMemoryCache et IDistributedCache

  • ImémoireCache:
    • Les objets demeurent comme des objets (sans serialisation).
    • Évacuation par pression de mémoire, limite de taille , absolueM SK2 expiration en aval, et prioritéMSC4
    • Non partagé entre les nodes; corrigé lors du recyclage de l’application/déploiementM SK2
    • Idéaux pour les ensembles chauds de nodes1, 2 et 3
  • IDistributedCache (Redis /SQLM SK2etc.):
    • Partage sur une ferme; survivant le remarrage de l'application; nécessite la serialisation MSC2stringsM SK3bytes ).
    • La latence légèrement plus élevée; le débit dépend du réseau et de l’enveloppe arrière.
    • Soutien absolu/expiration de sliding M SK1fournisseur -dépendant; Mise à jour du TTL par le fournisseur Redis sur l'accès au slidingMSC4
    • Idéal pour le croisement

Stratégies de cache communes

  • Cache-à l’exception M SK1la plus commune):

    1. Essayer le cache; M SK1 si vous manquez de charger la source, charger à partir de la source
    • Pros: simple ; la source de la vérité reste la base de donnéesM SK2
    • Cons: la première demande après l’expiration est lente
  • Lire-à travers (par l'entremise d'une bibliothèque /providerM SK3 manipuler le cache pour charger les missesMSC4

  • Rédiger-par l'entremise: les écritures vont à la cache et au stockage de sauvegarde synchronementM SK2

  • Write-behind:write to cacheMSC2 flush to store asynchronously M SK3riskMST4 lossM ST5inconsistenceM st6

  • Renouveler les clés chaudes avant leur expiration afin d’éviter des erreurs de froid.

Expiration, éviction, et taille

  • Expiration absolue: expire toujours après une durée déterminée M SK1avantageux pour la fraîcheur des données externes).
  • L'expiration en cascade: prolonge le TTL sur l'accès (bon pour les sessionsM SK2utilisateur -données spécifiquesMSC4
  • Taille-évacuation basée M SK1IMemoryCache):entrée de configurationMSC3taille et configure TailleLimit à la mémoire liée .
  • Priorité (IMemoryCache ): CacheItemPriorité.HighM SK3NormalMST4LowM ST5NeverRemove affects eviction under pressureMSC6
  • Jitter: ajouter de petits ajustements aléatoires aux TTL pour éviter l'expiration synchronisée (stampedes).

Prévention des stampedes dans les caches (élevage de troupe

  • Utiliser GetOrCreate/GetOrCreatAsync (IMemoryCacheM SK2 pour assurer une seule population de fil par node -thread
  • Distribué: Utiliser une clé de fermeture courte-livable lock key (SET NX EXM SK3 ou un support bibliographiqueMST4 ajouter TTL jitterMSC5 considérer le renouvellement de l’arrière-planMSL6
  • Serve stale-whileM SK1revalidate: garde une clé secondaire avec un valeur inachevée et une extension courte pendant que la nouvelle valeur est calculée .

La conception des clés et le classement des noms

  • Choisissez le caractère inférieur, colonM SK1les clés délimitées:appMSC3entitéMNK4ou locataireMRK5nousMMK6utilisateursMKK7
  • Inclure le segment de version pour invalider toutes les classes de clés sans supprimer
  • Tenant-aware: clés précursives avec l'ID du locataire ou de l'organisation pour éviter les collisions et faciliter la purge
  • Maintenir les clés petites mais descriptives; éviter l'utilisateur-entrée brute contrôlée sans normalisationM SK2

Échéance des ensembles de clés (tagsM SK1groupes)

Lorsque vous devez invalider plusieurs entrées connexes:

  • Préfixes versionnés (invalidation douce):battre une version globale dans un petit clavier et composer des clés avec elleM SK2

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

    Pour invalider tous les produits: increment v :products M SK2les clients manqueront naturellement des anciennes clés préfixées).

  • Établissement de l'étiquette par groupe (Redis): conserver un ensemble de clés pour chaque étiquetteM SK2 lors d'invalidation , récupérer les membres et supprimerMSC4

    // 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/Invalidation subséquenteM SK1 publier un message "invalidité:keyMST4MSC5 chaque node retient la clé de sa localisation IMemoryCacheMSL6

  • Scanner avec des patrons: SCAN/Les clés doivent être évitées dans les pistes chaudes prod ; ok for admin tooling on small keyspaces

Aides pratiques

  • IMemoryCache get-orM SK1set with options:
    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 avec JSON et expiration:
    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;
    }
    

Surveillance et visibilité

  • Atterrissage de la piste/ taux d'erreur et temps moyen de chargement ; métriques d'exposition M SK2 compteurs Prometheus) par groupe cléMSC4
  • Ajouter l'affichage autour de la population cache et les appels d'éviction pour IMemoryCache.
  • Pour Redis, surveillance des fractures de l’espace-clés / manquées M SK2 latence , et fragmentation de la mémoire MSC4 définir les politiques maximales de la mÃame mémorie selon qu’il conviendra

Real-World IMemoryCache Patterns from Production

Voici la façon dont j’utilise IMemoryCache dans ma plateforme de blog.

Modèle 1: Cache simple-à l'exception de rares occasionsM SK2change des données (CatégoriesMSC4

C’est la première mise en cache que j’ai réalisée. Les catégories de blogs ne changent pas souventM SK1 , donc les cachez pour 30 minutesMSC4

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

Pourquoi cela fonctionne:

  • Les catégories sont lues sur chaque page (montées dans la navigationM SK1
  • Ils changent rarement (seulement lorsque je crée de nouveaux articles sur un blog avec de nouvelles catégories)
  • 30-minute TTL est bon; si de nouvelles catégories apparaissentM SK2 les utilisateurs les voient dans un délai de 30 minutes
  • Exécution absolue seulement (pas de sliding) parce que nous n’avons pas d’importance pour 'on ne se soucie pas du nombre de fois qu’il est accessible

J’ai reçu votre message: Au début, j’ai utilisé une clé de chaîne "Catégories ". Fonctionne bien jusqu’à ce que vous ayez plusieurs contrôleurs et qu’un d’entre eux réutilise accidentellement la même clefM SK2 Maintenant, je suis à l’aise avec des constantes ou fortement.

Modèle 2: Par -État d'utilisateur avec limites de taille et expiration en glissement (Taches de traduction)

Ce fichier cache le statut de la traduction par utilisateur. Il est plus complexe ' parce qu’il a besoin de limites et devrait rester en vie tant que l’utilisateur est actif:

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

Pourquoi cela est différent:

  • L'expiration des Sliding: Si l'utilisateur garde à vérifier le statut de la traduction, conserver le cache en vie jusqu'à 6 heures max
  • Limites de taille: Gardez seulement les tâches les plus récentes de 5 par utilisateur afin d'empêcher le bloat de la mémoire
  • Gestion manuelle: Je limite explicitement la taille parce que MemoryCache n’applique pas les limites de dénombrement d’articles de niveau.

J’ai commis une erreur: Au départ, je n’ai pas limité le nombre de tâches.

Commerce-off Cela won't échelle à des millions d'utilisateurs . Si cela devient un problème, IM SK3d déménager au cache IDistributedCache MSC4RedisMST5 ou stocker dans la base de données avec un index sur l'ID de l'utilisateur.

Modèle 3: Cache avec observabilité (Cache métriqueM SK2

Ce fichier cache les métriques d'analyse et suit l'efficacité du cache en utilisant le traçage Serilog:

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

Qu’est-ce qui fait de cette production?

  • Observabilité: SerilogL'activité de Traçage permet d'effectuer des suivis sur les caches / taux d'erreur et nombre de métriques enregistrées
  • Mot clé composite: Comprend la gamme de dates et le préfixe pour éviter les collisions des clés
  • Formatage de la date: Utilisations yyyyMMdd format en clé à des moments différents le même jour partager la cache
  • Traitement nulle: Retourne nulle si le service externe échoue ; ne l'est pas
  • Cache filtrée: Cache le résultat filtré, non la réponse brute

Evolution: Au début, j’ai caché pour 10 minutes . Mais les métriques Umami sont lourdes à fetcher et ne changent pas beaucoupM SK2 , donc MSC4 heure est bonne. J’ai découvert cela en regardant les données de SerilogTracing et en voyant des erreurs excessives dans le cache.

Surveillance en action: Dans Seq ( mon agrégateur de logsM SK1 Je peux faire une demande de renseignements:

ActivityName = "GetMetricsWithPrefix" and CacheHit = false

Cela m’indique le taux d’erreur de cache que j’ai eue. Si ce taux est élevéM SK1 , Je modifie la TTL ou la stratégie clé.

Comparaison des trois approches

Pattern Case d'utilisation MSC2 Expiration MSC3 Contrôle de la taille SMC4 Observabilité MESC5
Catégories Globale, rarementM SK2changement 30 min absolu Pas nécessaire SpetiteMSC7 \MSC8 Enregistrement de base |
Travaux de traduction ParM SK1User, bounded 6h absolu + \1h glissage S Manuel M5 articles maxMSC9 Aucune Rne devrait être ajoutée
Metriques Les appels externes coûteux MSC2h absolus SMC3 naturels SMC4tempsS MC5windowedSCM6 7 Traçage complet 8

Leçons clés:

  1. Démarrer simple (patterne 1), ajouter de la complexité seulement lorsque nécessaire
  2. Pensez toujours à l’utilisation de la mémoire cache - ajouter des limites de taille pour les caches d’utilisateur par -
  3. Pour les opérations coûteuses, ajouter l’observabilité dès le premier jour
  4. Adjuster le TTL en fonction de la fréquence de changement réel des données, ne fait pas de suppositions

Lorsque je ne utilise pas IMemoryCache:

  • Pour l'état d'authentification de l'utilisateur (réclamations dans le cookie auth plutôtM SK1
  • Pour le cart d'achat (utilisera DB + cache distribué dans la production)
  • Pour le contenu du post de blog (actuellement dans la base de données , chargé une fois par requête
  • Cross-état de serveur M SK1 nécessiterait un cache IDdistributedCache/RedisMSC3

Authentification Cookies et revendications

  • Portée: à travers les demandes jusqu’à l’expiration de la période de validité
  • Utilisation d’une petite quantité de données sur le profil
  • Commerce-offsM SK1 taille de cookie; donMSC3t overstuff . Les revendications devraient être stablesSSK5

Établissement de cookie auth:

builder.Services.AddAuthentication("Cookies")
    .AddCookie("Cookies", o =>
    {
        o.LoginPath = "/login";
        o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        o.SlidingExpiration = true;
    });
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

Signature-in avec les revendications (MVCM SK2minimale ):

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

Résumé des revendications (toute pile):

[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 / Cotisations de Bearer

  • Portée: le client transporte un token; serveur sans état
  • Utilisation pour les microservices
  • Traitement-offsM SK1 taille du token; rotation /refreshMSC4 entreposage des revendications minimalesMST5 utilisation d’une introspection si nécessaire
builder.Services.AddAuthentication("Bearer")
   .AddJwtBearer("Bearer", o =>
   {
       o.Authority = "https://demo.identityserver.io"; // example
       o.Audience = "api";
       o.RequireHttpsMetadata = true;
   });

Utiliser:

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

Aperçu de la mermaid:

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

État durable: Base de données et amis

  • Portée: pour toujours ( jusqu’à ce que vous le supprimez
  • Utiliser pour: tout ce qui ne doit pas être perduM SK1 les carts , les commandes
  • Patterns: CRUD standard avec EF Core; CQRSM SK2 source d'événements ; outbox pattern pour la fiabilité

Ébauche du noyau EF:

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

Cache des réponses, ETags , et Demandes conditionnelles

  • Il n’est pas strictement “port d’états,”, mais réduit le travail répété en permettant au client de réutiliser les réponses antérieures.
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 exemple:

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

RéponseCache vs OutputCache: RealM SK1 Usage mondial dans la production

I use both ResponseCache and OutputCache together on my blog for different purposes. HereM SK1s why you might want both and how they differ.

La confusion: Deux attributs de caching?

ASP.NET Core a Deux similaire- systèmes de caching semblables:

  1. ResponseCache (HTTP caching): Établit les titres HTTP (Cache-Control, Vary) indique aux navigateurs et CDNs comment cacher
  2. OutputCache (server-sideM SK2: Cache la sortie rendue sur le serveur pour passer complètement à l'exécution de l'action

Ils se complètent mutuellement. RéponseCache gère le client/Cache CDN;OutputCache génie le serveurM SK2cache côtière .

Mes actions de post sur mon blog (les deux caches sont appliquées)

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

Que se passe-t-il lorsque quelqu'un demande /blog/my-post:

  1. OutputCache vérifie d'abord: Dois-je avoir une réponse cachée pour my-post + en langue?

    • Perpétuer: Retour cached HTML, la méthode d'action n'est jamais appliquée.
    • Miss: Exécuter l'action, vue de renduM SK2 résultat du cache pour 3600 secondes
  2. ResponseCache fixe les titres: Après que OutputCache génère la réponseM SK1 ResponseCache ajoute:

    Cache-Control: public, max-age=300
    Vary: hx-request
    
  3. Cache du navigateur: Le navigateur cache la réponse pour 300 secondes

  4. Cache du CDN (si vous utilisez CloudflareM SK1Fastly): Caches CDN pour 5 minutesMST4 Les utilisateurs du monde entier ont atteint le CDNMSC5 non mon serveurMSL6

Pourquoi les durées différentes? M SK1 vs 3600)

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

Justification:

  • Cache du serveur est plus longue (1 heure): Je contrôle mon serveur; Je peux supprimer le cache si je mets à jour un article
  • Le cache du client est plus court (5 minutes): Je peux ' ne nettoyer pas facilement les navigateurs d'utilisateurs ou les CDNs
  • Commerce-off: Si j’édite un post, de nouveaux contenus apparaissent
    • Server-sideM SK1 Immédiatement (Je peux invalider le cache)
    • CDN/browsersM SK1 Dans les 5 minutes (ou j’enlève manuellement le CDN

Evolution: À l’origine, j’avais les deux à 5 minutes . Mais cela signifiait que mon serveur était en train de réexaminer-render tous les 5 minutes même si le contenu change rarementMSC4 MaintenantM SK5

  • Le serveur sert happily cached HTML pour 1 heure
  • Les clients reçoivent chaque minute suffisamment de contenu fresh-

VaryByHeader pour HTMX

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

Pourquoi cela importe: Les demandes HTMX comprennent hx-request: true header. Je retourne différentes réponses:

  • Demande complète: Page HTML complète avec layout
  • Demande HTMX: Vue partielle sans layout

Sans VaryBy, le cache retournerait le format erroné . avec VaryBy, Je cache deux versions de chaque page.

Exemple:

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 pour la langue et les pages

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

Problème sans cela: /blog/my-post?language=fr servirait la version cache anglaise.

Avec VaryByQueryKeys: Separer les entrées de cache:

  • /blog/my-post?language=en → La clé de cache inclut :
  • /blog/my-post?language=fr → La clé de cache inclut :

Utilisation réelle de ma liste de 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, /* ... */)

Avertissement d'explosion de cache: Chaque combinaison unique des paramètres = entrée de cache distincteM SK1

  • page=1&pageSize=20&language=en → Une rubrique
  • page=2&pageSize=20&language=en → Autre entrée
  • page=1&pageSize=10&language=en → Autre entrée

Mesures d'atténuation

  • Largeur maximale raisonnable du cache (OutputCache auto-evicts le moinsM SK2recentlyMST3usedM ST4
  • Paramètres communs cachés (page 1, page par défautSize)
  • Les combinaisons rares pourraient manquer le cache (acceptableM SK1

Mise en place requise

RéponseCache fonctionne hors de la boîte, mais pour OutputCache vous avez besoin de configuration:

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

Lorsque OutputCache n’aide pas'

OutputCache est passé pour:

  • Demandes authentiquées (d'autres utilisateurs voient des données différentes)
  • POSTE/PUTM SK1DELETE (GET seulement /HEAD cachedMSC4
  • Réponses avec Set-Cookie en-tête
  • Réponses explicitement définies Cache-Control: no-store

Exemple où je ne l’utilise pas

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

Mesure de l'efficacité

I use Prometheus metrics (exposed via my appM SK1 to track:

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

Ma vitesse de saisie du cache: ~98% pour les articles sur le blog (le plus grand trafic attire les mêmes articles populaires à maintes reprises

Répercussions

  • Sans cache: M SK1ms temps de réponse moyen (requête DB ≥+ Rendement de marquage)
  • Avec OutputCache: ~1-2ms pour les réponses cachées
  • 25x accélération

Lorsque je disable temporairement le caching

Parfois, j’effectue des débuggings et j’ai besoin de nouvelles réponses chaque fois.

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

Or use environment-specific settings:

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

Une meilleure approche: Utiliser la configuration:

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

où responseCacheDuration est 0 au cours de l’élaborationM SK1 300 dans la production.

Avantages et inconvénients de cette stratégie de cache double

Pros:

  • Efficacité du serveur: OutputCache réduit la charge de CPU/DB par 98%
  • Avantages du client/CDN: ResponseCache réduit ma largeur de bande et améliore la latence globale
  • Flexibilité: Différents TTL pour le serveur contre le client
  • Économises de coûts: Moins de demandes DB, moins de largeur de bande

Cons:

  • Épaisseur: Les mises à jour du contenu prennent jusqu’à 5 minutes pour être diffusées aux clients
  • Complexité de l'invalidation du cache: La mise à jour d'un post exige l'invalidation des caches du serveur et du CDN
  • Utilisation de la mémoire: OutputCache tient le HTML rendu dans la mémoire du serveur
  • Débugging de la confusion: Il faut parfois oublier que le cache est activé et se demander pourquoi les changements ne se produisent pas

Lorsque I'd interrompt le caching:

  • Données réelles-détails de temps M SK1prix à la bourse,sports en direct )
  • Contenu personnalisé (utilisateur-recommandations spécifiquesM SK2
  • Pages de trafic faibles- pages de trafic M SK1 cache à frais généraux
  • Pages qui changent très fréquemment

Mon verdict: Pour un blog dont le contenu est principalement statique et la lecture est élevée, il faut utiliser une double cache.


ViewBag, ViewDataM SK1 et TempData : Contrôleur-àMSC4État de la visualisation Mska5et pourquoi je évite surtout deux d'entre ellesM Ska6

Ces trois sont souvent confuses.

Les trois amis comparés

// 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");
Caractéristiques Données de visualisation
Type ViewDataDictionary dynamique ITempDataDictionary
La durée de vie Requête courante Demande courante
Accès clé Mots clés de chaîne Syntaxe des propriétés
Sécurité du type Aucune (les émissions requises) Aucunes dynamiquesM SK5 Aucune
Compiler- vérification du temps Non Non
Survives redirect Non Non

Ce que j’utilise réellement: ViewBag pour les données globales de layout

Dans mon blog, J'utilise ViewBag exclusivement pour transmettre les données des contrôleurs à la mise en place partagée M SK1analyses, catégories , etcMSC4

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

Ensuite, dans mon schéma (_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>
}

Pourquoi ce modèle fonctionne:

  • Globalement: Chaque page a besoin de catégories nav et analyse
  • Calculé une fois: BaseController fonctionne avant chaque action
  • Optimisation HTMX: Passer le script d'analyse sur les demandes partielles
  • Caché: Les catégories sont cachées (voir mon modèle IMemoryCache ci-dessus), afin de ne pas saisir la DB chaque demande

J’ai commis une erreur tôt sur le site. J'étais en train de ViewBag.Categories dans chaque méthode d’action. Violation de la DRY et facile à oublier OnActionExecutionAsync dans le contrôleur de base a résolu ce.

Pièce de visualisation pour la page-données spécifiques M SK1patterne acceptable)

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

En vue:

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

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

Cela va bien parce que:

  • Valeurs scalaires simples (stringM SK1 int)
  • Utilisée uniquement dans la vue, n'est pas passée autour
  • La solution de rechange consisterait à ajouter Title et Category propriétés pour chaque modèle de vue

Ce que je évite: Objets complexes dans 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 };

Problèmes:

  • Aucune compilation-sécurité du temps M SK1typo ViewBag.Usr fails at runtime)
  • Il est difficile de suivre quelles données sont disponibles en vue
  • Les tests sont plus difficiles (il faut inspecter le dictionnaire ViewBag)
  • Il n'y a pas d'intelligence

Améliorer: fortement - modèles de visualisation typés:

// 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: Je ne l’utilise pas 'je n’en utilise pas (et iciM SK3c’est pourquoi

Cas d'utilisation TempData standard:

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

Pourquoi ne pas utiliser TempData dans mon blog:

  1. J'utilise HTMX au lieu des redirections: Je soumets mes formulaires par le biais de HTMX et retourne des vues partielles avec succès en ligne.
[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. Pour la GRP traditionnelle (Post-RedirectM SK2Get ), J'utiliserais TempData. Mais je préfère éviter les redirections lorsque c'est possible pour un meilleur UX

Quand TempData a sens:

  • Applications traditionnelles MVC avec des redirections de page complète après POST
  • Multi-wizards d'étape où vous redirigez entre les étapes
  • Messages Flash après les redirections d'authentification

TempData vous a reçu: Par défaut, supporté par des cookies (since ASP.NET Core 2.0). Si vous mettez de grands objets dans TempDataM SK3 vousMSC4bloutez le cookie envoyé avec chaque demandeMNK5 Pour un grand étatMRK6 utilisez Session with a backing store or DBMسک7

L'arbre de décision rapide

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]

Mes règles:

  1. Appareil de visualisation pour les questions relatives à la planification globale seulement (analyticsM SK1 nav, breadcrumbs )
  2. ViewBag pour les titres de pages simples/entêtes (optionnelM SK1 pourrait utiliser ViewModel)
  3. Never ViewBag pour les objets complexes (utiliser les modèles de visualisation)
  4. N'afficher jamais les données (ViewBag a une meilleure syntaxe)
  5. TempData seulement si vous avez vraiment besoin de PRG (I donM SK1t, grâce à HTMXMSC3

Pattern: Wizards and Multi-Step Flows

Quel détenteur d'État utiliser?

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
  • Petits, uniqueM SK1session: Session ou TempData entre les étapes .
  • Cross-deviceM SK1long-running : persister à DBMSC4 porter une clé dans l’URLMST5

Exemple (DB + clé de routeM SK2

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

Pattern: Messages Flash avec TempData

  • Établir dans POST; lire une fois après redirection.
TempData["Flash"] = "Profile saved";
return RedirectToAction("Index");

Vue du Razor:

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

Pattern: Cart d'achat

  • Petites cartouches: Séance M SK1si votre échelle est modeste et que vous avez des taches sticky /séance distribuée).
  • Carts plus gros/multiM SK1dispositifs: DB + cart≥-id dans le cookie ou l'URLMSC5 Cache pour la vitesseMST6
flowchart LR
  U[User] -- cart-id cookie --> S[Server]
  S --> DB[(Cart Table)]
  S <--> Cache[Distributed Cache]

Sécurité, Protection des renseignements personnels, et Liste de vérification de la conformité

  • Valider tous les clients-État fourniM SK1question de demande,en-têtes ,formulairesMSC4cookiesMST5 revendications JWTMNK6
  • Protéger le client sensible-état conservé: utiliser la protection des données pour les cookies que vous émettez ; ne stocker jamais les secrets dans les chaînes de demandesM SK3
  • Établir des drapeaux de témoins: Secure, HttpOnly, SameSite, IsEssential (si nécessaire par consentementM SK1necessité fonctionnelle).
  • Régénérer les cookies d'authentification lors des changements de privilèges; maintenir les revendications minimales.
  • Encrypter en temps de repos pour le serveur-entrepôts à côté selon les besoins; assurer la rotation des clés ( clés de protection des données
  • Règlement sur la protection des données à caractère personnel/CCPA: fournir l’exportation de données par les utilisateurs / voies de suppression M SK3 réduire au minimum la conservation .

Matrice de décision (Fiche de chevauchementM SK1

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
  }

Choix rapides:

  • Besoin d’un flash redirectionnel-?TempDataM SK2
  • Besoin d'assistant sur plusieurs demandes en une seule session? Session M SK1or DB + clé si longue-runningMska4multiM Ska5deviceMka6
  • Besoin de scalability et d'APIs sans état? JWT pour l'identité, DBM SK2DistributedCache pour le état .
  • Besoin de transmettre les données à l'intérieur du pipeline seulement? HttpContexteM SK1Items.
  • Besoin de cache pour les recherches calculées? IMemoryCache localementM SK1 IDistributedCache à travers une ferme.

MVC vs Razor Pages vs Minimal APIs: Les mêmes fondations, Différents formes

Les trois piles se trouvent sur les mêmes primitives (HttpContexte,Binding du modèleM SK2 authMSC3 protection des données ). Les exemples ci-dessus montrent que les API diffèrent principalement en ergonomie

  • APIs minimaux: liaison des paramètres à partir de la route /query/bodyM SK3claimsMSC4 retour Results.*.
  • MVC: attributsM SK1 filtres , liaison du modèle à des paramètres d'action/ modèles de visualisationMSC4
  • Pages Razor: gestionnaire de pages avec propriétés liées et aides à l'étiquetage pour générer des liensM SK1forms.

Ils partagent tous les mêmes mécanismes d’État dont il est question ici.


Pitfalls et anti-Patterns

  • Entreposage de données importantes ou sensibles dans des cookies ou TempData.
  • Selon le cache de mémoire - pour la correction (itM SK2est un cache, non vraiMSC4
  • Autorisation de construction fondée sur le client-route d’envoi/flags de demande sans vérification du serveurM SK2
  • Au-dessus-consommation de cookies auth ou de JWT avec des allégations volatilesM SK1
  • Séance sans stratégie de distribution ( fonctionne localement , interrompt à l'échelle

Réel-Gestion de l'État dans le monde: Ce que j'utilise réellement (et ne pas utiliser

Après avoir montré toutes ces options, ici ' est mon évaluation honnête de ce qui fonctionne dans la production pour ma plateforme de blog.

Mon stade de gestion des états (en ordre de fréquenceM SK1

Mécanisme Fréquence MSC2 Cas d'utilisation MSC3 Satisfaction SMC4
ImémoireCache Très élevée Catégories, métriquesM SK3 tâches de traduction
OutputCache élevée Postes de blog rédigésM SK2 listes MSC3 MSC4 victoire massive parf SMC5
RéponseCache Haut En-têtes de cache HTTP SCP2 MSC3 Fonctionne avec OutputCache
Trousse de visualisation Moyen Paramètres d'analyseM SK2 Titres de pages S⭐⭐⭐ OK pour des choses simples SSK5
Demandes d'avocat Moyen Identité de l'utilisateur, drapeau administratif MSC3 MSC4 Outil approprié pour les auths SMC5
Route/Requête Moyen Pagement, filtrationM SK3 boues de sciage MSC4 S⭐⭐⭐⭐ Établissement sans état et linéaire SMC6
Base de données Source de la vérité
Cookies Faible Préférences de l'utilisateur
Séance N'a jamais - 3 4 Aucune mise en place de magasins distribués
TempData N'a jamais - 3 4 L'HTMX élimine le besoin
HttpContexte.Items N'a jamais eu de cas d'utilisation - ≥ \❌ Ne l'a pas fait
IDistributedCache N'a jamais été 2 3 4 Un seul serveur 5 pour l'instant 6 7

Précisions détaillées des pros/cons de l'expérience en production

IMemoryCache ⭐⭐⭐⭐⭐

Ce que je l'utilise pour:

  • Liste des catégories (globalM SK1 30 min TTL)
  • Par -traçage des tâches de traduction par l'utilisateur (6h absolu
  • Réponses de l'API externe (Métriques UmamiM SK1 1h TTL)

Pros dans la pratique:

  • Brûlant rapidement (in-processM SK2
  • Il n'y a pas de frais généraux de serialisation
  • Réduction de la charge DB/API par ~95%
  • Facile à mettre en œuvre et à comprendre
  • Observabilité via SerilogTracing

Cons I've hit:

  • Les fuites de mémoire si vous ne limitez pas les caches d'utilisateurs par '
  • Éliminé lors du redémarrage de l'application (acceptable pour mon cas d'utilisation)
  • N'est pas partagé entre les serveurs (excellent pour une seule instance)
  • L'invalidation du cache est manuelle (ne doit être suppriméeM SK1 explicitement)

Limite d'étalonnage: Si j'arrive à plusieurs serveurs, I'd besoin d'un cache IDdistributedCache M SK2RedisMSC3 pour l'état partagé . Pour l'instantMNK5 un seul serveur MNK6 le cache de mémoire est parfaitMRK7

OutputCache + ResponseCach ⭐⭐⭐⭐⭐

Ce que je l'utilise pour:

  • Rendement des articles sur le blog (1 serveur d’heures, 5 client minM SK3CDNMSC4
  • Liste des blogs/pages de la catégorie
  • Données du calendrier

Pros dans la pratique:

  • 25x accélération (50ms → 2msM SK3
  • Scales à un trafic élevé sans ralentir le sueur
  • Separer les TTLs pour le serveur contre le client
  • Travaille sans fil avec HTMX (VaryByHeader)
  • Impact mesurable par le biais des mesures Prometheus

Cons I've hit:

  • Débugging de la confusion (forget cache is on)
  • Explosion de cache avec plusieurs combinaisons de paramètres de demande
  • fenêtre de régularité (5 min pour les clients)
  • Il faut invalider manuellement les mises à jour du contenu

Le meilleur pour: Lire-applications lourdes dont le contenu est principalement statiqueM SK1 Pas bon pour les données personnalisées ou réelles-données de tempsMSC3

Trousse de vision ⭐⭐⭐

Ce que je l'utilise pour:

  • Données relatives à la planification globale (analysesM SK1 catégories )
  • Titre de la page

Pros dans la pratique:

  • Facile et rapide pour les données de niveau de layout-
  • Établir une fois dans BaseController, disponible partout
  • Travaille bien avec la liste des catégories cachées

Cons I've hit:

  • Aucun type d’innocuité (faillance du dispositif à l’heure de démarrageM SK1
  • Tentation à utiliser trop de données complexes
  • Difficile à tester

Règle I: ViewBag pour les scalars simples seulement. Les objets complexes passent dans ViewModels.

Demandes d'asile ⭐⭐⭐⭐⭐

Ce que je l'utilise pour:

  • ID de l'utilisateur, nomM SK1 courriel, URL avatar
  • Flage d'administrateur (checking sub réclamation contre config)

Pros dans la pratique:

  • Sécurité ( témoin signéM SK1 Protection des données)
  • Automatique avec ASP.NET Core Identity/OAuth
  • Disponible par l'intermédiaire User.Claims dans tous les endroits
  • L'expiration progressive permet aux utilisateurs d'être enregistrés

Cons I've hit:

  • Limite de taille des témoins (donM SK1t allégations d'excès de matières
  • Les revendications sont statiques jusqu’à ce que re-login
  • Si j'ajoute une revendication (comme "IsEditor"),, il faut réexaminer -d'émettre le cookie auth

Pratiques exemplaires: Maintenir les revendications minimales et stables

Les choses que je n’utilise pas't use (and whyM SK2

Session (ne jamais utilisée):

  • Nécessiterait un magasin distribué (Redis)
  • Ajout de complexité pour un avantage minimal
  • Mes cases d'utilisation sont mieux servies par:
    • Demandes d'authentification (identitéM SK1
    • IMemoryCache (shortM SK1lived state)
    • Base de données (état durableM SK1

TempData (ne jamais utiliséM SK1

  • HTMX éliminé Post-Redirection-Paternité de démarrage
  • Les formulaires retournent des vues partielles avec les messages en ligne
  • Il n'est pas nécessaire de survivre aux redirections

HttpContexte.Items (ne jamais utiliséM SK2

  • Haven't avait un cas d'utilisation pour chaque État de demande
  • Mon outil de middleware ne calcule pas les valeurs des contrôleurs
  • Si j’avais besoin de détection du locataire, je l’utiliserai.

IDistributedCache (ne jamais utiliséM SK1

  • Déploiement d'un seul serveur
  • IMemoryCache répond à tous les besoins
  • Utiliserait Redis si je fais l'échelle à plusieurs serveurs

Évolution de mon approche

Phase 1 (initialM SK2 Aucun cache à tout moment. Chacune des demandes atteigne la base de données et produit un marquage de démarrageM SK1 Fait l’objet d’une amende pour faible trafic.

Phase 2 (première optimisationM SK2 Added IMemoryCache for categories. Saw immediate DB load reductionM SK1 Kept it simple: 30 min TTLMST4 no fancy logicMSC5

Phase 3 Added OutputCache for blog posts when traffic spiked. Massive performance improvementMSC1 Initial mistake: cached for M SK3 minutes onlyMST4 Increased to 1 hour after monitoring showed content rarely changesMSL6

Phase 4 (observabilitéM SK2 Added SerilogTracing to metrics cache. Discovered cache misses were high due to date formatting in keys. Fixed key format to yyyyMMdd instead of full timestamps. Le taux d’atterrissage est passé de M SK1 à 95%.

Phase 5 ( Intégration HMXM SK2 Ajout VaryByHeader pour hx-request. À l'origine, j'ai oublié ce message et je lui ai fourni des pages complètes pour les demandes de HTMX.

État actuel: Satisfait de l’enveloppe. IMemoryCache + OutputCach + ResponseCache handle 98% des besoins en gestion de mon étatM SK4 Base de données pour un état durableMSC5 Demandes d’authe pour l’identitéMska6 QueM Ska7 c’est celaM ska8

Conseils pour votre application

Commencez ici:

  1. Route/parmi les paramètres de demande pour la navigationM SK1filtration (aucune condition initiale
  2. Demandes d'identité présentées par les auteurs
  3. Base de données pour tout ce qui doit persister
  4. IMemoryCache pour lire-données calculées lourdes
  5. OutputCache pour les pages de remise coûteuses-à-

Ajouter si nécessaire: 6. Séance seulement si vous devez avoir un serveur-état conversationnel de la partie extérieureM SK3 7. IDistributedCache 8. Cookies pour le clientM SK2périodes de préférence des parties, consentementMSC4

Éviter:

  • Objets complexes dans ViewBag/TempData
  • Séance sans stockage d'appui distribué
  • Cachement de l’utilisateur-de données spécifiques ou fréquemment transmises-change des données
  • Optimisation prématurée (mesure premièreM SK1

Leçons clés:

  • Démarrer simple, ajouter la complexité seulement lorsque les mesures montrent que vous en avez besoin
  • La surveillance est essentielle (connaissez vos taux de démarrage du cache!)
  • Pensez aux limites de l’échelle tôt (per-les caches d’utilisateur ont besoin de limitesM SK2
  • Vérifier l'invalidité du cache de façon approfondie (la persistance est un problème réel)
  • Documentez vos choix de TTL (à l’avenir, vous demanderez "de quelle façon 30minutesMSC3

Envelopper-Up

État dans les applications Web n’est pas't one size fit all. Choisissez l’option la plus légère qui répond à vos besoins , préférez des modèles sans état lorsque vous pouvezM SK3 et soyez explicites au sujet de la sécurité et du cycle de vie

Si vous voulez aller plus loin sur la façon dont ces pièces s’écoulent dans le pipeline,, lisez ma série comme suit : Partie 1: Aperçu et fondation et particulièrement les parties de middleware et de routage.

Bonne construction.


Annexe: Autres exemplaires ‑Exemples passibles M SK2Réfinitions)

Ces exemples approfondissent les sections antérieures avec la production‑details de niveau que vous pouvez imposer dans le net9templates minimales , MVCM SK3 ou Pages RazorMSC4

Cookies: Protéger les valeurs grâce à la protection des données

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

Conseils: Dans les déploiements de nodes multiples ‑Node2les clés de protection des données persistantes M SK3e.gMNK5 vers un système de fichier partagéMSC6 RedisMMK7 ou Azure Key VaultMRK8 afin que les cookies puissent être lus à travers les instances

Antiforgery in MVC, Razor Pages, and Minimal APIs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddAntiforgery(o => o.HeaderName = "X-CSRF-TOKEN");
var app = builder.Build();

app.MapGet("/antiforgery/token", (IAntiforgery af, HttpContext ctx) =>
{
    var tokens = af.GetAndStoreTokens(ctx);
    return Results.Json(new { token = tokens.RequestToken });
});

app.MapPost("/submit", (HttpContext ctx) => Results.Ok("posted"))
   .AddEndpointFilter(async (efiContext, next) =>
   {
       var af = efiContext.HttpContext.RequestServices.GetRequiredService<IAntiforgery>();
       await af.ValidateRequestAsync(efiContext.HttpContext);
       return await next(efiContext);
   });

app.MapControllers();
app.MapRazorPages();
  • MVC: décorer les actions avec [ValidateAntiForgeryToken] et d'utiliser @Html.AntiForgeryToken() dans les formulaires.
  • Pages Razor: activé par défaut sur les messages de formulaire; utilisation asp-antiforgery="true" si nécessaire.
  • Validation minimale: via IAntiforgery comme il est indiqué.

Séance avec Redis et sliding vs expiration absolue

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

Store small, only compressible dataM SK1 Persist real carts /orders to a DB.

IMemoryCache avec options d'entrée et appel de retrait

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

IDdistributedCache get‑orM SK1set with jitter to avoid stampedes

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

Pour le fermeture robuste, préférer les primitives de Redis (SET NX EXM SK2 via StackExchange .RedisMSC4

Émettre et valider les DTJ localement (demoM SK1

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

Mise à jour conditionnelle avec ETags (SiM SK1Match)

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: objet complexe par 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");

Coefficient de cohérence d'EF Core

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

Cela devrait combler les lacunes.


Deep Dive: HttpContexte.Items M SK2Paterns et aides pratiques )

HttpContexte.Items est un sac de requête par -object ( IDictionnaire Object object 4 Objekt 5 qui n'existe que pour la durée d'une seule demande. It is perfect for passing computed values from middleware filter 8 to your endpoints controller 9 Controller 10 and Razor Pages handlers without touching global state or long

  • Cycle de vie: créé à la demande au début de l’essai; discardé lorsque la réponse est terminéeM SK2
  • Portée: seule demande courante M SK1 ne traverse jamais les redirections ou le travail de fond.
  • Efficacité: OM SK1 lookups; idéal pour chaque appareil de traitement des demandes
  • Sécurité: serveur - seule la partie extérieure; non visible au clientM SK3

Pourquoi les articles au lieu de…

  • Session/TempData: Ces demandes croisées et introduisent des préoccupations de distributionM SK2 Les articles sont éphémères et à l’échelle ‑friendlyMSC4
  • DI Services étendus: Utiliser ces services pour le comportement et les dépendances partagées. Les articles sont meilleurs pour l'affichage -hocM SK3 valeurs computées MSC4tenantMST5 localisation de l'utilisateur MST6 drapeaux de fonctions Mst7 et par M st8caches de demandes
  • HttpContexte.FeaturesM SK1 Pour le cadre /transport-Fréquences de niveau MSC4IEndpointFeatureMNK5IHttpUpgradeFeaTUREMRK6 Les articles sont destinés à l'applicationMMK7données de niveauMEK8

Évitez les collisions de clés: fortement-key typed

Parce que les éléments utilisent des clés d’objet, préfèrent les clés static privées ou un type de clé dédié pour éviter les collisions de noms.

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

Or create a typed wrapper with extensions:

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

Pattern: Compute in middleware, consume in endpointsM SK2controllers /pages

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

Pattern: Per-request cache to avoid repeated work

Utiliser les éléments comme un cache mince de sorte que les lectures répétées dans la même demande n’t re

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

Notes:

  • Threading: Une seule requête est généralement exécutée sur un seul chemin logique ; Les postes ne sont pas’t threadM SK3securitaire pour les écritures parallèlesMSC4 Si vous lancez des tâches parallales qui partagent les postesMska5 ajouter votre propre synchronisationMka6
  • Taille: Maintenir les valeurs petites et peu coûteuses pour calculerM SK1sérialiser. Il ’s enMSC4mémoire par demandeMST5

Pattern: Filtres qui englobent les articles (MVCM SK2Pages de 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>());

Pattern: Enrichir les registres sans affectation partout

Calculer une fois, puis lire dans les champs d’enregistrement ou le 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);
    }
});

Quand ne pas utiliser les articles

  • Données nécessaires après redirection ou à travers des demandes (utiliser TempData/SessionM SK2DB au lieu de ).
  • App-singletons à largeur ou croisésM SK1caches de demande (utiliser IMemoryCache /IDistributedCache≥).
  • Valeurs qui appartiennent à l’identité/authorisation (utilisation des revendicationsM SK2politiques).

Règle rapide: Si elleM SK1 est calculée pendant cette demande et lue dans cette demande par votre propre code, Les articles sont idéauxMSC3

logo

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