Back to "Κράτηση κράτους μεταξύ αιτήσεων σε ASP.NET Core: Ένας Πρακτικός, No‐Nonsense Οδηγός (MVC, Razor Pages, Minimal APIs)"

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

Κράτηση κράτους μεταξύ αιτήσεων σε ASP.NET Core: Ένας Πρακτικός, No‐Nonsense Οδηγός (MVC, Razor Pages, Minimal APIs)

Sunday, 09 November 2025

UPDATE (2025-11-10): Προστέθηκε πιο πρακτικά παραδείγματα από την πραγματική βάση κώδικα μου που δείχνει πρότυπα πραγματικού κόσμου, συναλλαγές-offs, gatchas, και η εξέλιξη από απλές σε εξελιγμένες προσεγγίσεις διαχείρισης κράτους. Περιλαμβάνει λεπτομερή πρότυπα IMemoryCache, ViewΧρήση ετικέτας (καλό και κακό), ReplyCache/OutputCache στρατηγικές, και τα μαθήματα που αντλήθηκαν από την παραγωγή.

Εισαγωγή

HTTP είναι γνωστό απάτριδες. Η εφαρμογή σας... δεν είναι. Οι χρήστες συνδεθείτε, προσθέστε αντικείμενα στα καλάθια, πηδήξτε μεταξύ των σελίδων, επιστρέψτε αύριο, και περιμένετε να θυμηθείτε. Στο ASP.NET Core (MVC, Razor Pages, Minimal APIs), υπάρχουν πολλοί τρόποι για τη διατήρηση και τη μεταφορά της κατάστασης μεταξύ των αιτήσεων. Μερικοί είναι ανά αίτημα μόνο. Μερικοί τελευταίοι για μια συνεδρία. Μερικοί ζουν στον πελάτη. Μερικοί διανέμονται και επιβιώνουν server επανεκκινεί.

Αυτή η δημοσίευση καταλογίζει τις επιλογές, παρουσιάζει συγκεκριμένα, αντιγραφέα παραδείγματα και στις τρεις στοίβες, και σας δίνει οδηγίες απόφασης ώστε να μπορείτε να επιλέξετε το σωστό εργαλείο για τη δουλειά.

ΣΗΜΕΙΩΣΗ: Αυτό είναι μέρος των πειραμάτων μου με AI (βοηθητική σύνταξη) + δική μου επεξεργασία. Ίδια φωνή, ίδιος πραγματισμός? απλά γρηγορότερα δάχτυλα.

Το Τοπίο με Μια Ματιά

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
  • Πελάτης: ερώτηση, διαδρομή, κεφαλίδες, μορφές, cookies, JWT. Κλίμακα οριζόντια, αλλά είναι ορατή από τον πελάτη (πρέπει να επικυρώνεται/υπογράφεται/κρυπτογραφείται κατά περίπτωση).
  • Διακομιστής-διακομιστής: TempData, Συνεδρία, caches, DB. Απαιτεί συγγένεια ή στρατηγική διανομής.
  • Ανά αίτημα: HttpContext.Items - χρήσιμο για τη διαβίβαση δεδομένων εσωτερικά κατά τη διάρκεια ενός μόνο αιτήματος.

Χρυσοί κανόνες (πριν βουτήξουμε σε APIs)

  1. Πρώτα η ασφάλεια: Η διαρροή του κράτους είναι πραγματική απειλή. Η κατάσταση Server-side (Session, in-memory caches) μπορεί να διαρρεύσει μεταξύ των χρηστών μέσω της κωδικοποίησης σφαλμάτων, συνθήκες φυλής, ή λανθασμένη διαχείριση κύκλου ζωής.
  2. Προτιμήστε τα μοτίβα "statless server" όταν χρειάζεται να κλιμακώσετε οριζόντια (πίεση κατάσταση σε μάρκες πελάτη, ανθεκτικά καταστήματα, ή κατανεμημένα caches).
  3. Ποτέ μην εμπιστεύεσαι την ελεγχόμενη από τον πελάτη κατάσταση.
  4. Κρατήστε τα μεγάλα δεδομένα μακριά από τα cookies και τις κεφαλές · φουσκώνουν κάθε αίτημα.
  5. Χρησιμοποιήστε το TempData μόνο για το Post-Redirect-Get (PRG) one-shots όπως τα μηνύματα flash.
  6. Χρησιμοποιήστε το Session μόνο όταν πρέπει να διατηρήσετε το server-side συνομιλία κατάσταση και έχετε προγραμματίσει τη διανομή και να είναι παρανοϊκή σχετικά με την ασφάλεια.
  7. Οι απαιτήσεις αφορούν την ταυτότητα και την άδεια για χοντροκομμένη χρήση, όχι τη γενική κατάσταση εφαρμογής.
  8. Η κρυψώνα δεν είναι πηγή αλήθειας.

Per-Request State: HttpContext.Items

  • Πεδίο εφαρμογής: μόνο τρέχουσα αίτηση (επιβίβαση στο τέλος του αγωγού)
  • Χρήση για: Περνώντας υπολογισμένες τιμές μεταξύ μέσου λογισμικού και τελικών σημείων/ελεγκτών
  • Κλίμακα: καμία επίπτωση
  • Ασφάλεια: μόνο διακομιστή
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"]

Παράδειγμα μεσαίου λογισμικού (όλες οι στοίβες):

app.Use(async (context, next) =>
{
    var tenantId = context.Request.Headers["X-TenantId"].FirstOrDefault() ?? "public";
    context.Items["TenantId"] = tenantId;
    await next(context);
});
  • Ελάχιστο τελικό σημείο API:
app.MapGet("/whoami", (HttpContext ctx) => new { Tenant = ctx.Items["TenantId"] });
  • MVC Controller:
public IActionResult WhoAmI() => Json(new { Tenant = HttpContext.Items["TenantId"] });
  • Χειριστής σελίδας Razor:
public IActionResult OnGet() => new JsonResult(new { Tenant = HttpContext.Items["TenantId"] });

Παροδική κατάσταση πελάτη: Τιμές διαδρομής και συμβολοσειρά ερώτησης

  • Πεδίο εφαρμογής: τρέχουσα αίτηση· ο πελάτης την μεταφέρει ρητά.
  • Χρήση για: πλαίσιο πλοήγησης, φιλτράρισμα, σελιδοποίηση, ταυτότητα πόρων
  • Ασφάλεια: πρέπει να επικυρώνει/εγκρίνει· να μην ενσωματώνει μυστικά

Παραδείγματα

  • Minimal APIs:
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 });
  • Σελίδες ξυραφιού (Διαταγές/Λεπτομέρειες.cshtml.cs):
public IActionResult OnGet(int id, int? page)
  => Page();

Δημιουργία συνδέσμων που διατηρούν την κατάσταση:

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

Επικεφαλίδες: ταυτότητες αντιστοιχίας, Κλειδιά Idempotency, Σημαίες χαρακτηριστικών

  • Πεδίο εφαρμογής: τρέχουσα αίτηση, προαιρετικά αντηχεί στις απαντήσεις
  • Χρήση για: ιχνηλάτηση, retry-safety, A/B σημαίες
  • Ασφάλεια: αντιμετωπίζουν ως μη αξιόπιστη εισαγωγή, επικύρωση/λευκή λίστα
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);
});

Αδιευκρίνιστος χαρακτήρας (σύνδεσμος):

flowchart TD
  C[Client POST /pay\nIdempotency-Key:k] --> S{Seen k?}
  S -- No --> E[Execute charge]
  E --> P[Persist result by k]
  P --> R[Return 200 + result]
  S -- Yes --> L[Load result by k]
  L --> R

Μορφές και κρυμμένα πεδία (PRG)

  • Πεδίο εφαρμογής: μόνο επόμενη αίτηση (πελάτης αναρτήσει πίσω τις τιμές)
  • Χρήση για: βήματα μάγου, αντι-αξέχαστες μάρκες, κρατώντας μικρά κομμάτια της πολιτείας σε όλη την PRG
  • Ασφάλεια: πάντα επικύρωση? Συνδυάστε με την αντιαξέχαστη

PRG μοτίβο σε σελίδες 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 });
}
  • Σελίδες ξυραφιού:
public IActionResult OnPost(SettingsModel model)
{
    return RedirectToPage("/Settings/Summary", new { tab = model.SelectedTab });
}

Cookies: Μικρά, Υπογεγραμμένα, Μερικές φορές Κρυπτογραφημένα

  • Πεδίο εφαρμογής: κάθε αίτηση από το πρόγραμμα περιήγησης έως τη λήξη
  • Χρήση για: προτιμήσεις, μη ευαίσθητες σημαίες, συγκατάθεση· cookies ταυτοποίησης (χωριστό τμήμα)
  • Εμπορικά: όρια μεγέθους (~4KB ανά cookie), επιπτώσεις απόδοσης, πρέπει να συμμορφώνονται με τους νόμους συγκατάθεσης

Ελάχιστο παράδειγμα 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 είναι πανομοιότυπη μέσω HttpContext.

Για ακεραιότητα/εμπιστοσύνη, χρησιμοποιήστε το σύστημα προστασίας δεδομένων πυρήνα ASP.NET για να προστατεύσετε τα φορτία που βάζετε μόνοι σας στα cookies.


TempData: Λεωφορείο ενός μηνύματος

  • Πεδίο εφαρμογής: επιβιώνει από μία ανακατευθυνόμενη γραμμή
  • Κατάστημα υποστήριξης: Cookie (προεπιλογή) ή συνεδρία
  • Χρήση για: μηνύματα flash, περιλήψεις επικύρωσης μετά το PRG

Ρύθμιση (Program.cs):

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

Στο χειριστήριο MVC:

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

Στο Razor Page χειριστής:

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

Προβολή/σελίδα:

@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]

Σύνοδος: Server-Side Conversational State

  • Πεδίο εφαρμογής: συνεδρία προγράμματος περιήγησης (κλειδί cookie + κατάστημα διακομιστών)
  • Χρήση για: μάγους πολλαπλών βημάτων, δεδομένα μικρού καροτσιού, μετρητής throttling
  • Ανταλλαγές: απαιτεί κολλώδεις συνεδρίες ή κατανεμημένο κατάστημα υποστήριξης; μπορεί να περιορίσει την κλίμακα

Ρύθμιση:

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

Χρήση συνεδρίας (κάθε στοίβα):

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

Βοηθοί συνεδρίας:

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

Μια προειδοποιητική ιστορία: Όταν η κατάσταση της συνεδρίας γίνεται το μπουκάλι

Κάποτε δούλεψα πάνω σε ένα τεράστιο πρόγραμμα πληροφορικής της κυβέρνησης του Ηνωμένου Βασιλείου, όπου το κράτος της συνεδρίας κατάχρηση (μεταξύ πολλών άλλων αρχιτεκτονικών αμαρτιών) έγινε μια απόδοση-σκοτώνοντας μπουκάλι. Τα πάντα. σε συνεδρία: προτιμήσεις χρήστη, δεδομένα πολλαπλών βημάτων, αποτελέσματα αναζήτησης, προσωρινούς υπολογισμούς, ακόμη και εγκλωβισμένα lookups που θα έπρεπε να ήταν σε μια σωστή κρύπτη ή βάση δεδομένων.

Το πρόβλημα: Session state was septed in- process (ASP.NET session state in web.config, this was pre-Core days). Κάθε αίτημα έπρεπε να απενεργοποιήσει μαζικά αντικείμενα session. Καθώς το φορτίο αυξήθηκε, κατάσταση session αερόστατο σε δεκάδες megabytes ανά χρήστη.

Η απελπισμένη λύση: Πετάξαμε στις εγκαταστάσεις του HP στη Στουτγάρδη για να κάνουμε δοκιμές φορτίου στο Superdome τους εκείνη την εποχή, Η πιο ισχυρή μηχανή Windows της Ευρώπης. Ήταν ένα θηρίο: δεκάδες επεξεργαστές Itanium, εκατοντάδες γιγαμπάιτ της RAM. Η ιδέα ήταν να αποδειχθεί ότι με αρκετό υλικό, το σύστημα θα μπορούσε να ανταποκριθεί στις απαιτήσεις.

Το αποτέλεσμα: Ακόμη και στο Superdome, δεν μπορούσαμε να χτυπήσουμε τους απαιτούμενους ταυτόχρονα στόχους χρηστών. Η αρχιτεκτονική της κατάστασης συνεδρίας ήταν ουσιαστικά χαλασμένη. Κάθετη κλιμάκωση δεν μπορούσε να σώσει το κακό σχεδιασμό. Η σειριακή / απελπισία της συνεδρίας από πάνω, σε συνδυασμό με την πίεση μνήμης από μαζικά αντικείμενα συνεδρίας, σήμαινε ότι το σύστημα απλά δεν θα μπορούσε να κλιμακωθεί με οποιοδήποτε λογικό κόστος.

Ο εφιάλτης της ασφάλειας: Χειρότερα από τα θέματα απόδοσης, ανακαλύψαμε ένα σφάλμα κωδικοποίησης που προκάλεσε κατάσταση συνεδρίας διαρροή μεταξύ των χρηστών. Τα δεδομένα συνεδρίας του χρήστη Α θα εμφανίζονται περιστασιακά στη συνεδρία του χρήστη Β. Αυτό δεν ήταν μόνο ντροπιαστικό ~ ήταν καταστροφικό . Οι χρήστες ήταν Προσωπικό NHS Είχαμε δημιουργήσει κατά λάθος ένα μηχανισμό παραβίασης της προστασίας δεδομένων που θα μπορούσε να εκθέσει ευαίσθητες ιατρικές πληροφορίες σε διάφορες συνεδρίες επαγγελματιών υγείας.

Τι θα έπρεπε να είχε συμβεί:

  1. Ατελής εξ ορισμού: Τα περισσότερα από αυτά τα δεδομένα συνεδρίας δεν θα έπρεπε ποτέ να υπάρχουν
  2. Βάση δεδομένων για διαρκή κατάσταση: Η πρόοδος σε μορφή πολλών βημάτων θα έπρεπε να ήταν στη βάση δεδομένων με ταυτότητα ροής εργασίας
  3. Κρύσταλλο για αναζήτηση: Shared lookups ανήκε σε IMemoryCache ή μια κατανεμημένη κρύπτη
  4. Πελάτης-πλευρά για τις προτιμήσεις: Οι προτιμήσεις των χρηστών θα μπορούσαν να είναι σε cookies ή τοπική αποθήκευση
  5. Κατανεμημένη συνεδρία εάν χρειάζεται: Εάν η συνεδρία ήταν πραγματικά απαραίτητη, η συνεδρία που υποστηρίζεται από το Redis θα είχε μοιραστεί το φορτίο

Το μάθημα: Η κατάσταση συνεδρίας δεν κλιμακώνεται κάθετα και ίσα που ζυγίζει οριζόντια (ακόμα και με κολλώδεις συνεδρίες ή κατανεμημένα καταστήματα, εξακολουθείτε να ταξινομείτε/επιβράβευση σε κάθε αίτημα).Το πείραμα Superdome απέδειξε ότι ρίχνοντας υλικό σε αρχιτεκτονικά προβλήματα είναι ακριβό και συχνά μάταιο.

Αλλά το πιο σημαντικό: bugs κατάστασης συνεδρίας γίνονται ευάλωτα σημεία ασφαλείας. Thread-safety θέματα, φυλετικές συνθήκες, εσφαλμένος χειρισμός ID συνεδρίας αυτά δεν προκαλούν μόνο προβλήματα απόδοσης, μπορούν να διαρρεύσουν ευαίσθητα δεδομένα μεταξύ των χρηστών. Σε ένα πλαίσιο υγειονομικής περίθαλψης (ή τραπεζικό, ή οποιαδήποτε ρυθμιζόμενη βιομηχανία), αυτό είναι ένας εφιάλτης συμμόρφωσης και πιθανή ποινική ευθύνη.

Σύγχρονη συμβουλή:

  • Αν βρεθείτε να χρειάζεστε περισσότερα από μερικά KB των στοιχείων συνεδρίας, έχετε πιθανώς ένα πρόβλημα σχεδιασμού
  • Αν αποθηκεύετε ευαίσθητα δεδομένα σε συνεδρία, δημιουργείτε μια επιφάνεια επίθεσης ασφαλείας
  • Οι ανήθικες αρχιτεκτονικές δεν κλιμακώνουν καλύτερα. Είναι εγγενώς πιο ασφαλείς γιατί δεν υπάρχει κατάσταση διακομιστή για να διαρρεύσει.
  • Ξανασκέψου τη στρατηγική διαχείρισης του κράτους σου πριν χρειαστείς να πετάξεις στη Στουτγάρδη (ή να εξηγήσεις μια παραβίαση δεδομένων στον Επίτροπο Πληροφοριών)

Caching: IMemoryCache και IDistributedCache

  • Πεδίο εφαρμογής: διαδικασία διακομιστή (IMemoryCache) ή κατανεμημένο (IDistributedCache)
  • Χρήση για: παράγωγα/υπολογισμένα δεδομένα, αναζητήσεις, κατάσταση βραχείας διάρκειας
  • Ανταλλαγές: ακύρωση κρυφής μνήμης, καταργήσεις για διανομή

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 (π.χ. 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");
});

Προειδοποίηση κατά του περιγράμματος: εάν πρέπει να είναι ανθεκτικό ή έγκυρο, να το αποθηκεύσετε σε μια βάση δεδομένων και να το κρύψετε προαιρετικά.

Επιλέγοντας μεταξύ IMemoryCache και IDistributedCache

  • IMemoryCache:
    • Φλεγόμενα γρήγορα, κατά τη διαδικασία, αντικείμενα παραμένουν ως αντικείμενα (καμία σειρά).
    • Έξωση από την πίεση μνήμης, το όριο μεγέθους, απόλυτη/απομακρυσμένη λήξη, και προτεραιότητα.
    • Δεν μοιράζονται στους κόμβους· εκκαθαρίζονται κατά την ανακύκλωση/ανάπτυξη εφαρμογών.
    • Υπέροχα για κάθε κόμβο, υπολογισμένα lookups, μικρά TTLs.
  • IDistributedCache (Redis/SQL/etc.):
    • Μοιραστείτε σε ένα αγρόκτημα; επιβιώνει εφαρμογή επανεκκινεί; απαιτεί τη σειρά (strings/bytes).
    • Λίγο υψηλότερη καθυστέρηση? throughput εξαρτάται από το δίκτυο και backend.
    • Υποστηρίζει την απόλυτη/απομακρυσμένη λήξη (εξαρτημένη από τον προμηθευτή· ενημερώσεις του παρόχου Redis TTL σχετικά με την πρόσβαση για ολίσθηση).
    • Ιδανικό για συνοχή cross-node, μεγάλα fan-out διαβάσεις, χαρακτηριστικό σημαίες, και συνεδρία.

Κοινές στρατηγικές κρυφής μνήμης

  • Κρηπιδώματα (πιο συχνές):

    1. Δοκιμάστε κρύπτη; 2) αν χάσετε, φορτίο από την πηγή; 3) γράψτε στην κρύπτη; 4) επιστροφή.
    • Pros: απλή; πηγή της αλήθειας παραμένει η βάση δεδομένων.
    • Κατά: η πρώτη αίτηση μετά τη λήξη είναι αργή; πιθανές σφυροκοπίες.
  • Read-through (μέσω βιβλιοθήκης/προμηθευτή): cache χειρίζεται τη φόρτωση σε αστοχίες.

  • Write-through: γράφει πηγαίνει σε κρύπτη και backing κατάστημα συγχρονικά.

  • Εγγραφή-πίσω: γράψτε στην κρύπτη, ξεπλύνετε για να αποθηκεύσετε ασύγχρονα (κίνδυνος: απώλεια/ασυνέπεια).

  • Ανανέωση: αναζωογονήστε τα ζεστά κλειδιά πριν λήξει για να αποφύγετε τις κρύες αστοχίες.

Λήξη, έξωση και μέγεθος

  • Απόλυτη λήξη: πάντα λήγει μετά από σταθερή διάρκεια (καλό για εξωτερική φρεσκάδα δεδομένων).
  • Συρόμενη λήξη: επεκτείνει TTL στην πρόσβαση (καλό για συνεδρίες / δεδομένα ειδικά για τον χρήστη).
  • Size-based έξωση (IMemoryCache): set entry.size and configure SizeLimit to linked memory.
  • Προτεραιότητα (IMemoryCache): CacheItemPriority.High/Normal/Low/NeverRemove επηρεάζει την έξωση υπό πίεση.
  • Jitter: προσθέστε μικρές τυχαίες αντισταθμίσεις στα TTLs για να αποφύγετε συγχρονισμένη λήξη (σφραγίδες).

Πρόληψη πτωμάτων κρύπτης (βροντή αγέλη)

  • Χρησιμοποιήστε το GetOrCreate/GetOrCreateAsync (IMemoryCache) για να εξασφαλίσετε τον πληθυσμό ενός νήματος ανά κόμβο.
  • Διανεμήθηκε: χρησιμοποιήστε ένα σύντομο κλειδί κλειδαριά (SET NX EX) ή υποστήριξη βιβλιοθήκης; προσθέστε TTL jitter; εξετάσει την ανανέωση φόντου.
  • Σερβίρετε μπαγιάτικο-ενώ-revalidate: κρατήστε ένα δευτερεύον κλειδί με μπαγιάτικη τιμή και σύντομη επέκταση, ενώ υπολογίζεται νέα τιμή.

Βασικός σχεδιασμός και namespacing

  • Προτιμότερη κάτω περίπτωση, colon-περιορίζεται κλειδιά: app:οντότητα:123 ή ενοικιαστής:us:us:users:42.
  • Συμπεριλάβετε τμήμα έκδοσης για να ακυρώσει ολόκληρες κατηγορίες κλειδιών χωρίς διαγραφές: v2: προϊόντα:123.
  • Tenant-aware: Πρόθεμα κλειδιά με τον ενοικιαστή ή την ταυτότητα οργάνωσης για την αποφυγή συγκρούσεων και εύκολο καθαρισμό.
  • Κρατήστε τα κλειδιά μικρά, αλλά περιγραφικά, αποφύγετε την ελεγχόμενη από το χρήστη πρώτη είσοδο χωρίς ομαλοποίηση.

Εκχώρηση σετ κλειδιών (tags/groups)

Όταν χρειάζεται να ακυρώσετε πολλές σχετικές καταχωρήσεις:

  • Εκδοθέντα προθέματα (μαλακή ακύρωση): χτυπήστε μια παγκόσμια έκδοση σε ένα μικρό κλειδί και συνθέστε τα πλήκτρα με αυτό.

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

    Για να ακυρώσετε όλα τα προϊόντα: αύξηση κατά: προϊόντα (οι πελάτες θα χάσουν φυσικά παλιά προκαθορισμένα κλειδιά).

  • Σετ ετικετών ανά ομάδα (Redis): κρατήστε ένα Σετ πλήκτρων ανά ετικέτα· στην ακύρωση, φέρτε τα μέλη και διαγράψτε.

    // 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/Υπογραφή: δημοσιεύστε ένα μήνυμα "μη έγκυρη: κλειδί"; κάθε κόμβος αφαιρεί το κλειδί από την τοπική IMemoryCache.

  • Σάρωση με μοτίβα: SCAN/KEYS θα πρέπει να αποφεύγεται σε prod hot μονοπάτια; εντάξει για τη διαχείριση εργαλείων σε μικρούς χώρους κλειδιά.

Πρακτικοί βοηθοί

  • IMemoryCache get- or-set με επιλογές:
    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);
      });
    
  • IDistriptedCache με JSON και λήξη:
    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;
    }
    

Παρακολούθηση και ορατότητα

  • Παρακολουθήστε τα ποσοστά χτυπήματος/αστοχίας και το μέσο χρόνο φορτίου· εκθέστε τις μετρήσεις (μετρητές Prometheus) ανά ομάδα κλειδιών.
  • Προσθέστε την καταγραφή γύρω από τον πληθυσμό cache και τις εκκλήσεις έξωσης για IMemoryCache.
  • Για Redis, παρακολουθήστε χτυπήματα/αστοχίες κλειδιών, καθυστέρηση, και τον κατακερματισμό της μνήμης· ρυθμίστε τις maxmemory πολιτικές, ανάλογα με την περίπτωση.

Real-World IMemoryCache Μοτίβα από την παραγωγή

Εδώ είναι το πώς χρησιμοποιώ πραγματικά IMemoryCache στην πλατφόρμα blog μου, εξελίχθηκε μέσω της δοκιμής και του λάθους. Θα σας δείξω τρία πραγματικά πρότυπα από απλή έως εξελιγμένη.

Μοτίβο 1: Απλή παύση καλλιέργειας για σπάνια μεταβαλλόμενα δεδομένα (Κατηγορίες)

Αυτό ήταν το πρώτο μου caching εφαρμογή. Οι κατηγορίες blog δεν αλλάζουν συχνά, έτσι ώστε να cache τους για 30 λεπτά:

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

Γιατί αυτό λειτουργεί:

  • Κατηγορίες διαβάζονται σε κάθε σελίδα (shown in navigation)
  • Σπάνια αλλάζουν (μόνο όταν προσθέτω νέα άρθρα blog με νέες κατηγορίες)
  • 30 λεπτά TTL είναι μια χαρά. Αν εμφανιστούν νέες κατηγορίες, οι χρήστες τους βλέπουν μέσα σε 30 λεπτά
  • Απόλυτη λήξη μόνο (χωρίς ολίσθηση) γιατί δεν μας νοιάζει πόσο συχνά έχει πρόσβαση

Σε χτύπησα. Αρχικά χρησιμοποίησα ένα κλειδί συμβολοσειράς "Categories" . Λειτουργεί μια χαρά μέχρι να έχετε πολλούς ελεγκτές και ένα κατά λάθος ξαναχρησιμοποιεί το ίδιο κλειδί . Τώρα χρησιμοποιώ σταθερές ή έντονα δακτυλογραφημένα πλήκτρα (δείτε την προηγούμενη ενότητα για την αποφυγή συγκρούσεων).

Μοτίβο 2: Κατάσταση ανά χρήστη με όρια μεγέθους και συρόμενη λήξη (Δραστηριότητες μετάφρασης)

Είναι πιο περίπλοκο επειδή χρειάζεται όρια και θα πρέπει να παραμείνει ζωντανός όσο ο χρήστης είναι ενεργός:

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

Γιατί αυτό είναι διαφορετικό:

  • Λήξη ολίσθησης: Εάν ο χρήστης συνεχίζει να ελέγχει την κατάσταση μετάφρασης, κρατήστε την κρύπτη ζωντανή έως 6 ώρες το μέγιστο
  • Όρια μεγέθους: Κρατήστε μόνο 5 πιο πρόσφατες εργασίες ανά χρήστη για να αποτρέψετε το bloat μνήμης
  • Χειροκίνητη διαχείριση: Περιορίζω ρητά το μέγεθος επειδή το MemoryCache δεν επιβάλλει όρια μέτρησης στοιχείων επιπέδου εισόδου (μόνο το συνολικό μέγεθος κρυφής μνήμης)

Λάθος που έκανα: Αρχικά δεν περιόρισα τον αριθμό των εργασιών. Ένας χρήστης ενέργειας πυροδότησε τις μεταφράσεις 50+ και είχα διαρροή μνήμης. Τώρα κρατάω το μέγιστο 5 ανά χρήστη.

Ανταλλαγές: Αν αυτό γίνει πρόβλημα, θα μετακομίσω στο IDistributedCache (Redis) ή θα αποθηκεύσω στη βάση δεδομένων με έναν δείκτη στο userId + startTime.

Μοτίβο 3: Cache με παρατηρητικότητα (Metrics caching)

Αυτό cache analytics metrics και παρακολουθεί την αποτελεσματικότητα cache χρησιμοποιώντας 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;
    }
}

Τι κάνει αυτή την παραγωγή έτοιμη:

  • Παρατηρησιμότητα: SerilogTracing activity tracks cache hit/mis rate and records metrics measure
  • Σύνθετο κλειδί: Περιλαμβάνει εύρος ημερομηνίας και πρόθεμα για την αποφυγή βασικών συγκρούσεων
  • Μορφοποίηση ημερομηνίας: Χρήσεις yyyyMMdd μορφή σε πλήκτρο τόσο διαφορετικές φορές την ίδια ημέρα μοιραστείτε την κρύπτη
  • Μητρικός χειρισμός: Επιστρέφει άκυρα σε περίπτωση αποτυχίας της εξωτερικής υπηρεσίας· δεν αποτυγχάνει η αποθήκευση
  • Φίλτρα για την αποθήκευσή τους: Συγκρατεί το φιλτραρισμένο αποτέλεσμα, όχι την ακατέργαστη απόκριση

Εξέλιξη: Αρχικά κόλλησα για 10 λεπτά. Αλλά Umami μετρήσεις είναι βαριά για να φέρει και να μην αλλάξει πολλά, έτσι 1 ώρα είναι μια χαρά. Ανακάλυψα αυτό με την εξέταση των δεδομένων SerilogΑκολουθώντας και βλέποντας υπερβολική κρύπτη αστοχίες.

Παρακολούθηση της δράσης: Στο Seq (το αρχείο καταγραφής μου), μπορώ να ρωτήσω:

ActivityName = "GetMetricsWithPrefix" and CacheHit = false

Αν είναι ψηλά, προσαρμόζω το TTL ή τη βασική στρατηγική.

Συγκρίνοντας τις τρεις προσεγγίσεις

Το μοτίβο χρήσης της υπόθεσης λήξης του μεγέθους του ελέγχου |---------|----------|------------|--------------|---------------| | Κατηγορίες Σε παγκόσμιο επίπεδο, σπάνια αλλάζει 30 λεπτά απόλυτο ~ Δεν χρειάζεται (μικρό) ~ Basic loging ~ | Μεταφραστικά καθήκοντα Ανά χρήστη, ορίζεται 6h απόλυτο + 1h συρόμενο εγχειρίδιο (5 items max) Κανένα (θα πρέπει να προσθέσετε!) | Μετρικός Ακριβές εξωτερικές κλήσεις 1h απόλυτη ~ Φυσική (χρόνος-παράκαμψη) ~ Πλήρης ιχνηλάτηση ~

Μαθήματα κλειδιά:

  1. Ξεκινήστε απλά (pattern 1), προσθέστε την πολυπλοκότητα μόνο όταν χρειάζεται
  2. Πάντα να σκέφτεστε τη χρήση μνήμης μνήμης cache - προσθέστε τα όρια μεγέθους για τις κρύπτες ανά χρήστη
  3. Για δαπανηρές εργασίες, προσθέστε παρατηρητικότητα από την πρώτη ημέρα
  4. Ρύθμιση TTL με βάση την πραγματική συχνότητα αλλαγής δεδομένων, όχι μαντεψιές

Όταν δεν χρησιμοποιώ IMemoryCache:

  • Για κατάσταση ταυτοποίησης χρήστη (απαιτήσεις σε cookie auth αντ' αυτού)
  • Για το καλάθι αγορών (θα χρησιμοποιούν DB + κατανεμημένη κρύπτη στην παραγωγή)
  • Για περιεχόμενο στο blog (ήδη σε DB, φορτωμένο μία φορά ανά αίτημα)
  • Cross-server κατάσταση (θα χρειαστεί IDistributedCache/Redis)

Cookies και απαιτήσεις ταυτοποίησης

  • Πεδίο εφαρμογής: σε όλες τις αιτήσεις έως τη λήξη/υπογραφή
  • Χρήση για: ταυτότητα, χονδροειδείς ρόλους/άδεια, μικρό ποσό δεδομένων προφίλ
  • Ανταλλαγές: μέγεθος cookie? dont overstuff. Ισχυρισμοί θα πρέπει να είναι σταθερή.

Ρυθμίστε το 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();

Υπογραφή με απαιτήσεις (MVC/minimal):

app.MapPost("/login", async (HttpContext ctx) =>
{
    var claims = new[]
    {
        new Claim(ClaimTypes.NameIdentifier, "123"),
        new Claim(ClaimTypes.Name, "Alice"),
        new Claim(ClaimTypes.Role, "Admin")
    };
    var identity = new ClaimsIdentity(claims, "Cookies");
    await ctx.SignInAsync("Cookies", new ClaimsPrincipal(identity));
    return Results.Redirect("/");
});

Απαιτήσεις ανάγνωσης (κάθε στοίβα):

[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

  • Πεδίο εφαρμογής: ο πελάτης μεταφέρει το σήμα; απάτριδος διακομιστής
  • Χρήση για: ΖΕΠ/κινητό/APIs, cross-domain, microservices
  • Ανταλλαγές: συμβολικό μέγεθος· περιστροφή/ανανέωση· αποθήκευση ελάχιστων απαιτήσεων, χρήση ενδοσκόπησης, εάν χρειάζεται
builder.Services.AddAuthentication("Bearer")
   .AddJwtBearer("Bearer", o =>
   {
       o.Authority = "https://demo.identityserver.io"; // example
       o.Audience = "api";
       o.RequireHttpsMetadata = true;
   });

Χρήση:

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

Γοργόνα επισκόπηση:

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

Ανθεκτική κατάσταση: Βάση δεδομένων και φίλοι

  • Πεδίο εφαρμογής: για πάντα (μέχρι να το διαγράψετε)
  • Χρήση για: οτιδήποτε δεν πρέπει να χαθεί: καροτσάκια, παραγγελίες, προφίλ, μακρινές ροές εργασίας
  • Μοτίβα: πρότυπο CRUD με πυρήνα EF; CQRS; προμήθεια συμβάντων; μοτίβο outbox για την αξιοπιστία

Σχεδίαση πυρήνα 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);
});

Απάντηση Caching, ETags, και αιτήματα υπό αίρεση

  • Όχι αυστηρά η μεταφορά της κατάστασης, αλλά μειώνει τις επαναλαμβανόμενες εργασίες αφήνοντας τον πελάτη/προξενείς να επαναλάβει προηγούμενες απαντήσεις.
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:

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

ΑπάντησηCache vs OutputCache: Πραγματική-Παγκόσμια Χρήση στην Παραγωγή

Χρησιμοποιώ τόσο ResponseCache και OutputCache μαζί στο blog μου για διαφορετικούς σκοπούς.

Η σύγχυση: Δύο χαρακτηριστικά caching;

Το ASP.NET Core έχει δύο παρόμοιας όψης συστήματα caching:

  1. ΑπάντησηCache (HTTP caching): Ορίζει κεφαλίδες HTTP (Cache-Control, Vary) λέγοντας browsers και CDNs πώς να κρυφτείτε ε
  2. OutputCache (πλευρά-server): Συγκρατεί την παρεχόμενη έξοδο στο διακομιστή για να αποφύγει την εκτέλεση της δράσης εξ ολοκλήρου

Συμπληρώνονται μεταξύ τους. ResponseCache χειρίζεται client/CDN caching; OutputCache hands server-side caching.

Το blog μου post action (και οι δύο caches εφαρμόζονται)

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

Τι συμβαίνει όταν κάποιος ζητάει /blog/my-post:

  1. Έλεγχος εξόδουCache πρώτα: Έχω μια cached απάντηση για my-post + en Τη γλώσσα;

    • Χτύπημα: Return cached HTML, action method never runs (fast! ~1ms)
    • Δεσποινίς: Εκτέλεση δράσης, απόδοση προβολής, αποτέλεσμα κρύπτης για 3600 δευτερόλεπτα (1 ώρα)
  2. Η απάντησηCache θέτει κεφαλίδες: Μετά το OutputCache παράγει την απάντηση, η απάντησηCache προσθέτει:

    Cache-Control: public, max-age=300
    Vary: hx-request
    
  3. Caching browser: Browser caches την απάντηση για 300 δευτερόλεπτα (5 λεπτά). Επακόλουθε αιτήματα από τον ίδιο χρήστη δεν χτυπήσει καν το διακομιστή.

  4. Caching CDN (αν χρησιμοποιείτε Cloudflare/Fastly): CDN caches για 5 λεπτά. Χρήστες σε όλο τον κόσμο χτύπησε το CDN, όχι τον διακομιστή μου.

Γιατί διαφορετικές διάρκειες; (300 έναντι 3600)

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

Λογισμός:

  • Η κρύπτη του εξυπηρετητή είναι μεγαλύτερη (1 ώρα): Ελέγχω τον διακομιστή μου, μπορώ να καθαρίσω την κρύπτη αν ενημερώσω μια δημοσίευση
  • Η κρύπτη πελατών είναι μικρότερη (5 λεπτά): I can't clean user browsers or CDNs easy; 5 λεπτά είναι ένα λογικό παράθυρο μπαγιάτικο
  • Αμοιβές: Εάν επεξεργαστώ μια θέση, εμφανίζεται νέο περιεχόμενο:
    • Side-server: Αμέσως (μπορώ να ακυρώσω την κρύπτη)
    • CDN/browsers: Μέσα σε 5 λεπτά (ή εγώ καθαρίζω χειροκίνητα CDN)

Εξέλιξη: Αρχικά είχα και τα δύο στα 5 λεπτά... αλλά αυτό σήμαινε ότι ο διακομιστής μου ξανάρχιζε κάθε 5 λεπτά... ακόμα κι αν το περιεχόμενο σπάνια αλλάζει.

  • Server σερβίρει ευτυχισμένο HTML για 1 ώρα
  • Οι πελάτες παίρνουν φρέσκο περιεχόμενο κάθε 5 λεπτά

VaryBy Header για HTMX

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

Γιατί αυτό μετράει: Οι αιτήσεις HTMX περιλαμβάνουν hx-request: true Επιστρέφω διαφορετικές απαντήσεις:

  • Πλήρης αίτηση: Πλήρης σελίδα HTML με διάταξη
  • Αίτηση HTMX: Μερική θέα χωρίς διάταξη

Χωρίς VaryByΗ κρύπτη θα επέστρεφε σε λάθος μορφή. VaryBy, Κρύβω δύο εκδόσεις κάθε σελίδας.

Παράδειγμα:

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 για τη γλώσσα και pagination

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

Πρόβλημα χωρίς αυτό: /blog/my-post?language=fr θα σερβίρει την αγγλική έκδοση.

Με VaryByQueryKeys: Ξεχωριστές καταχωρήσεις κρυφής μνήμης:

  • /blog/my-post?language=en → Το κλειδί Cache περιλαμβάνει το "en"
  • /blog/my-post?language=fr → Το κλειδί Cache περιλαμβάνει "fr"

Πραγματική χρήση από τη λίστα 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, /* ... */)

Προειδοποίηση έκρηξης Cache: Κάθε μοναδικός συνδυασμός παραμέτρων = ξεχωριστή εγγραφή κρύπτης:

  • page=1&pageSize=20&language=en → Μία καταχώρηση
  • page=2&pageSize=20&language=en → Μια άλλη καταχώρηση
  • page=1&pageSize=10&language=en → Μια άλλη καταχώρηση

Μιτιξοποίηση:

  • Λογικό μέγιστο μέγεθος κρυφής μνήμης (OutputCache auto-evicts λιγότερο-πρόσφατα χρησιμοποιείται)
  • Κοινές παράμετροι cached (σελίδα 1, προεπιλεγμένη σελίδαΜέγεθος)
  • Όχι συχνές συνδυασμούς μπορεί να παραλείψουν την κρύπτη (αποδεκτό)

Απαιτείται ρύθμιση

ΑπάντησηCache λειτουργεί έξω από το κουτί, αλλά για OutputCache Χρειάζεσαι στήσιμο:

// Program.cs
builder.Services.AddOutputCache(options =>
{
    options.MaximumBodySize = 64 * 1024 * 1024; // 64 MB max response size
    options.SizeLimit = 100 * 1024 * 1024; // 100 MB total cache size
});

var app = builder.Build();
app.UseOutputCache(); // Must be in middleware pipeline

Όταν OutputCache δεν βοηθά

OutputCache φεύγει για:

  • Επιβεβαιωμένες αιτήσεις (διαφορετικοί χρήστες δείτε διαφορετικά δεδομένα)
  • POST/PUT/DELETE (μόνο GET/HEAD Cached)
  • Απαντήσεις με Set-Cookie κεφαλίδα
  • Απαντήσεις που έχουν οριστεί ρητά Cache-Control: no-store

Παράδειγμα όπου δεν το χρησιμοποιώ:

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

Μέτρηση της αποτελεσματικότητας

Χρησιμοποιώ τις μετρήσεις του Προμηθέα (εκτεθειμένες μέσω της εφαρμογής μου) για να εντοπίσω:

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

Το χτύπημα της κρύπτης μου: ~98% για τα blog posts (το μεγαλύτερο μέρος της κυκλοφορίας χτυπά τις ίδιες δημοφιλείς θέσεις επανειλημμένα).

Αντίκτυπος:

  • Χωρίς αποθήκευση: ~50ms μέσος χρόνος απόκρισης (DB query + Markdown rendering)
  • With OutputCache: ~1-2ms for cached reactions
  • 25x επιτάχυνση

Όταν αχρηστεύω προσωρινά το caching

Μερικές φορές αποσφαλματώνω και χρειάζομαι νέες απαντήσεις κάθε φορά:

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

Ή χρησιμοποιήστε τις ειδικές για το περιβάλλον ρυθμίσεις:

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

Καλύτερη προσέγγιση: Χρήση ρυθμίσεων:

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

όπου responseCacheDuration 0 στην ανάπτυξη, 300 στην παραγωγή.

Πλεονεκτήματα και μειονεκτήματα αυτής της στρατηγικής διπλής κασέ

Pros:

  • Αποδοτικότητα εξυπηρετητή: OutputCache μειώνει το φορτίο CPU/DB κατά 98%
  • Πελάτης/πλεονεκτήματα CDN: Η απάντησηCache μειώνει το εύρος ζώνης μου και βελτιώνει την παγκόσμια καθυστέρηση
  • Ευελιξία: Διαφορετικά TTLs για server vs client
  • Αποταμίευση κόστους: Λιγότερες ερωτήσεις DB, λιγότερο εύρος ζώνης

Κατά:

  • Καθυστέρηση: Οι ενημερώσεις περιεχομένου διαρκούν έως 5 λεπτά για να διαδοθούν στους πελάτες
  • Περιπλοκότητα ακύρωσης Cache: Ενημέρωση μιας θέσης απαιτεί ακύρωση τόσο του διακομιστή όσο και του CDN caches
  • Χρήση μνήμης: OutputCache κρατάει που αποδίδεται HTML στη μνήμη διακομιστή
  • Συγχώνευση αποσφαλμάτωσης: Μερικές φορές ξεχνάς την κρύπτη και αναρωτιέσαι γιατί οι αλλαγές δεν εμφανίζονται

Όταν έκανα κοπάνα

  • Στοιχεία πραγματικού χρόνου (τιμή αποθέματος, ζωντανά αθλήματα)
  • Εξατομικευμένο περιεχόμενο (ειδικές από τον χρήστη συστάσεις)
  • Σελίδες χαμηλής κυκλοφορίας (επιπλεονέκτημα > όφελος)
  • Σελίδες που αλλάζουν πολύ συχνά

Η ετυμηγορία μου: Για ένα blog με κυρίως στατικό περιεχόμενο και υψηλή αναλογία ανάγνωσης/γραφής, διπλή caching είναι μια τεράστια νίκη.


ViewBag, ViewData, and TempData: Controller-to-View State (και γιατί κυρίως αποφεύγω δύο από αυτά)

Αυτοί οι τρεις συχνά είναι μπερδεμένοι. 'κου πώς διαφέρουν και τι χρησιμοποιώ στην παραγωγή.

Οι τρεις φίλοι σε σύγκριση

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

Περιγραφή Χαρακτηριστικό ΔείτεData ViewBag TempData & TempData

--------- ---------- --------- ----------
Ζωή Τρέχων αίτημα
Πρόσβαση στο κλειδί Κλειδιά συμβολοσειρών Ακινήτου ~ Κλειδιά συμβολοσειράς ~
Ασφάλεια των τύπων Δεν υπάρχει (χρειάζεται) κανένας (δυναμικός) κανένας (χρειάζεται)
Έλεγχος του χρόνου πληρωμής Αpiό τι piεριpiτώσει piου piροκύpiτουν αpiό τι piεριpiτώσει piου piροκύpiτουν αpiό τι piεριpiτώσει piου piροκύpiτουν αpiό τι piεριpiτώσει piου piρέpiει να υpiοβάλετε.
Ανακατευθύνσεις επιβίωσης Όχι, όχι, όχι, όχι, ναι, ναι

Τι πραγματικά χρησιμοποιώ: ViewBag για τα παγκόσμια δεδομένα διάταξης

Στο blog μου, χρησιμοποιώ το ViewBag αποκλειστικά για τη διαβίβαση δεδομένων από ελεγκτές σε κοινή διάταξη (analytics, κατηγορίες κ.λπ.):

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

Στη συνέχεια, στη διάταξη μου (_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>
}

Γιατί αυτό το μοτίβο λειτουργεί:

  • Καθολική: Κάθε σελίδα χρειάζεται κατηγορίες nav και analytics
  • Υπολογιζόμενη μία φορά: BaseController τρέχει πριν από κάθε δράση
  • Βελτιστοποίηση HTMX: Skip analytics script on partial requests (HTMX doesn't need it re-expected)
  • Φραγκομαϊντανός: Κατηγορίες είναι cached (δείτε IMemoryCache μοτίβο μου παραπάνω), έτσι ώστε να μην χτυπήσει DB κάθε αίτημα

Λάθος που έκανα νωρίς: Ήμουν έτοιμος. ViewBag.Categories σε κάθε μέθοδο δράσης. Παραβίαση DRY και εύκολα ξεχνάμε. OnActionExecutionAsync Στο χειριστήριο της βάσης το έλυσε αυτό.

Προβολή ετικέτας για συγκεκριμένα δεδομένα σελίδας (αποδεκτό μοτίβο)

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

Ενόψει:

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

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

Αυτό είναι εντάξει επειδή:

  • Απλές τιμές βαθμολόγησης (string, int)
  • Χρησιμοποιείται μόνο στην θέα, δεν περνάει γύρω
  • Εναλλακτική λύση θα ήταν η προσθήκη Title και Category ιδιότητες σε κάθε μοντέλο προβολής

Τι αποφεύγω: Συγκρότημα αντικειμένων στο ViewBag

Αντικολλητικός δίσκος:

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

Προβλήματα:

  • Δεν υπάρχει ασφάλεια χρόνου σύνταξης (typo ViewBag.Usr αποτυγχάνει κατά τη διάρκεια του τρεξίματος)
  • Δύσκολο να εντοπίσεις ποια δεδομένα είναι διαθέσιμα υπόψιν
  • Κάνει τη δοκιμή δυσκολότερη (ανάγκη για να επιθεωρήσει το λεξικό ViewBag)
  • Δεν IntelliSense

Καλύτερα: Ισχυρά δακτυλογραφημένα μοντέλα προβολής:

// 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: Δεν το χρησιμοποιώ (και να γιατί)

Τυπική περίπτωση χρήσης 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();
}

Γιατί δεν χρησιμοποιώ το TempData στο blog μου:

  1. Χρησιμοποιώ HTMX αντί για ανακατευθύνσεις: Οι φόρμες μου υποβάλλονται μέσω HTMX και επιστρέφουν μερικές απόψεις με inline success/error μηνύματα.
[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. **Για την παραδοσιακή PRG (Post-Redirect-Get)**Αλλά προτιμώ να αποφεύγω τις ανακατευθύνσεις όταν είναι δυνατόν για καλύτερη UX.

Όταν το TempData βγάζει νόημα:

  • Παραδοσιακές εφαρμογές MVC με πλήρη ανακατευθύνσεις σελίδας μετά POST
  • Μάγοι πολλών βημάτων όπου ανακατευθύνεστε μεταξύ βημάτων
  • Μήνυμα Flash μετά τις επανακατευθύνσεις ταυτοποίησης

TempData gotcha: Από προεπιλογή υποστηρίζεται από cookies (από ASP.NET Core 2.0). Αν βάλετε μεγάλα αντικείμενα σε TempData, είστε φουσκώνοντας το cookie που αποστέλλονται με κάθε αίτημα. Για μεγάλη κατάσταση, χρησιμοποιήστε συνεδρία με ένα κατάστημα υποστήριξης ή DB.

Γρήγορο δέντρο αποφάσεων

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]

Οι κανόνες μου:

  1. Προβολή ετικέτας μόνο για παγκόσμια διάταξη (analytics, nav, breadcrumbs)
  2. ViewBag για απλούς τίτλους/κεφαλές σελίδων (προαιρετικά; θα μπορούσε να χρησιμοποιήσει ViewModel)
  3. Ποτέ ViewBag για σύνθετα αντικείμενα (use ViewModels)
  4. Ποτέ μην δείτε τα δεδομένα (ViewBag έχει καλύτερη σύνταξη)
  5. TempData μόνο αν χρειάζεστε πραγματικά PRG (Δεν το κάνω, χάρη στην HTMX)

Μοτίβο: Wizards και Multi-Step Ροές

Ποιος κάτοχος κράτους να χρησιμοποιήσει;

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
  • Μικρή, μονοσυνεδρία: Συνεδρία ή TempData μεταξύ των βημάτων.
  • Διασταυρούμενη συσκευή/με μακρύ τρέξιμο: επιμένουν να DB, φέρουν ένα κλειδί στο URL.

Παράδειγμα (DB + κλειδί διαδρομής):

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

Μοτίβο: Flash μηνύματα με TempData

  • Τοποθετήστε σε POST; διαβάστε μία φορά μετά την ανακατευθύνσεις.
TempData["Flash"] = "Profile saved";
return RedirectToAction("Index");

Θέα ξυραφιού:

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

Μοτίβο: Καλάθι αγορών

  • Μικρά καροτσάκια: Συνεδρία (αν η κλίμακα σας είναι μέτρια και έχετε κολλώδη / διανεμημένη συνεδρία).
  • Μεγαλύτερα καλάθια/πολυ-συσκευή: DB + cart-id σε cookie ή URL. Cache για ταχύτητα.
flowchart LR
  U[User] -- cart-id cookie --> S[Server]
  S --> DB[(Cart Table)]
  S <--> Cache[Distributed Cache]

Κατάλογος Ελέγχου Ασφάλειας, Ιδιωτικότητας και Συμμόρφωσης

  • Επιβεβαιώστε όλες τις καταστάσεις πελατών: ερωτήσεις, κεφαλίδες, έντυπα, cookies, διεκδικήσεις JWT.
  • Προστατέψτε την ευαίσθητη κατάσταση πελατών: χρησιμοποιήστε την Προστασία Δεδομένων για τα cookies που εκδίδετε, ποτέ μην αποθηκεύετε μυστικά σε συμβολοσειρές ερωτήσεων.
  • Ορισμός σημαιών cookie: Secure, HttpOnly, SameSite, IsEssential (εάν απαιτείται από τη συγκατάθεση/λειτουργική ανάγκη).
  • Αναγεννήστε τα cookies ταυτοποίησης σχετικά με τις αλλαγές προνομιούχων· κρατήστε τις απαιτήσεις ελάχιστες.
  • Κρυπτογράφηση σε κατάσταση ηρεμίας για καταστήματα server-side όπως απαιτείται· διασφάλιση της περιστροφής κλειδιών (Κλειδιά προστασίας δεδομένων, κλειδιά υπογραφής JWT).
  • GDPR/CCPA: παρέχουν διαδρομές εξαγωγής/διαγραφής δεδομένων χρήστη, ελαχιστοποιούν τη διατήρηση.

Απόφαση Matrix (Φάκελος εξαπατήσεως)

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
  }

Γρήγορες επιλογές:

    • Χρειάζεσαι μια ανάλαφρη αναλαμπή;
  • Χρειάζεστε μάγο σε πολλαπλά αιτήματα σε μία συνεδρία; Συνεδρία (ή DB + κλειδί εάν μακρύ-τρέξιμο / πολυ-συσκευή).
  • Χρειάζεστε κλιμακότητα και απάτριδες APIs; JWT για την ταυτότητα, DB / DistributedCache για το κράτος.
  • Χρειάζεται να περάσει δεδομένα μόνο μέσα στον αγωγό; HttpContext.Items.
  • Χρειάζεστε κρύπτη για υπολογισμένες αναζητήσεις; IMemoryCache τοπικά; IDistributedCache σε μια φάρμα.

MVC vs Razor Pages vs Minimal APIs: Ίδια θεμέλια, διαφορετικά σχήματα

Και οι τρεις στοίβες κάθονται στα ίδια πρωτόγονα (HttpContext, σύνδεση μοντέλου, auth, προστασία δεδομένων). Τα παραπάνω παραδείγματα δείχνουν ότι οι API διαφέρουν κυρίως στην εργονομία:

  • Minimal APIs: παράμετρος σύνδεσης από τη διαδρομή/διεύθυνση/σώμα/απαιτήσεις· επιστροφή Results.*.
  • MVC: χαρακτηριστικά, φίλτρα, μοντέλο σύνδεσης σε παραμέτρους δράσης / μοντέλα προβολής.
  • Razor Σελίδες: σελίδα χειριστές με δεσμευμένες ιδιότητες και tag helpers για τη δημιουργία συνδέσμων/μορφών.

Όλοι μοιράζονται τους ίδιους κρατικούς μηχανισμούς που συζητούνται εδώ.


Pitfalls και Anti-Patterns

  • Αποθήκευση μεγάλων ή ευαίσθητων δεδομένων σε cookies ή TempData.
  • Ανάλογα με τη μνήμη cache για την ορθότητα (είναι μια κρύπτη, όχι αλήθεια).
  • Εξουσιοδότηση κατασκευής με βάση την πελάτισσα-απεσταλμένη διαδρομή/διαγραφή σημαιών χωρίς ελέγχους διακομιστή.
  • Υπερφορτώνοντας μπισκότα ή JWT με ασταθείς ισχυρισμούς.
  • Συνεδρία χωρίς στρατηγική διανομής (λειτουργεί τοπικά, σπάει σε κλίμακα).

Real-World State Management: What I actually Use (and Don't Use)

Αφού σας δείξω όλες αυτές τις επιλογές, ορίστε η ειλικρινής εκτίμησή μου για το τι λειτουργεί στην παραγωγή για την πλατφόρμα blog μου.

Στοίβα διαχείρισης της πολιτείας μου (κατά σειρά συχνότητας)

~ Μηχανισμός ~ Συχνότητα ~ Χρησιμοποίησε υποθέσεις ~ Ικανοποίηση ~ |-----------|-----------|-----------|--------------| | IMemoryCache Πολύ υψηλές κατηγορίες, μετρικές, μεταφραστικές εργασίες | OutputCache ~ Υψηλή ~ Rendered blog posts, lists ~ Τεράστιο perf win ~ | ΑπάντησηCache Η υψηλή κεφαλίδα HTTP λειτουργεί με το OutputCache | Προβολή ετικέταςName Ρυθμίσεις Μεσαίων Αναλυτών, τίτλοι σελίδων Εντάξει για απλά πράγματα | Ισχυρισμοί επωνυμίας Μεσαία ταυτότητα χρήστη, σημαία διαχειριστή | Διαδρομή/Κορυφή Μεσαίος κορμός, φιλτράρισμα, γυμνοσάλιαγκες | Βάση δεδομένων Μεσαίο ~ Blog αναρτήσεις, σχόλια, state ~ Πηγή της αλήθειας ~ | Cookies Οι χαμηλές προτιμήσεις των χρηστών (μελλοντικά) δεν χρειάζονται ακόμα. | Σύνοδος Δεν υπάρχει κατανεμημένο store setup | TempData Ποτέ δεν είναι έτσι η HTMX εξαλείφει την ανάγκη | HttpContext.Items Ποτέ δεν είχες χρησιμοποιήσει την υπόθεση. | IDistributedCache Ποτέ δεν θα είναι ένας ενιαίος διακομιστής (για τώρα)

Λεπτομερή πλεονεκτήματα/cons από την εμπειρία παραγωγής

IMemoryCache

Για ποιο λόγο το χρησιμοποιώ:

  • Κατάλογος κατηγοριών (συνολικός, 30 λεπτά TTL)
  • Εντοπισμός εργασιών μετάφρασης ανά χρήστη (6h απόλυτη + 1h συρόμενη)
  • Εξωτερικές απαντήσεις API (μετρικές Umami, 1h TTL)

Πλεονεκτήματα στην πράξη:

  • Γρήγορη έκρηξη (κατά τη διαδικασία)
  • Χωρίς serialization over
  • Μειωμένο φορτίο DB/API κατά ~95%
  • Εύκολο στην εφαρμογή και την κατανόηση
  • Παρατήρηση μέσω SerilogTracing

Κονς Έχω χτυπήσει:

  • Διαρροές μνήμης αν δεν περιορίσετε τις κρύπτες ανά χρήστη
  • Καθαρίστηκε κατά την επανεκκίνηση της εφαρμογής (αποδεκτό για την περίπτωση χρήσης μου)
  • Δεν μοιράζονται σε όλους τους διακομιστές (καλά για ένα παράδειγμα)
  • Η ακύρωση Cache είναι χειροκίνητη (ανάγκη αφαίρεσης() ρητά)

Όριο κλιμάκωσης: Αν φτάσω σε πολλούς διακομιστές, θα χρειαστώ IDistributedCache (Redis) για κοινή κατάσταση. Για τώρα, single server + μνήμη κρύπτη είναι τέλεια.

OutputCache + ResponseCache

Για ποιο λόγο το χρησιμοποιώ:

  • Blog Post rendering (1 ώρα διακομιστή, 5 λεπτά client/CDN)
  • Blog list/category pages
  • Στοιχεία ημερολογίου

Πλεονεκτήματα στην πράξη:

  • 25x επιτάχυνση (50ms → 2ms)
  • Κλιμάκια στην υψηλή κυκλοφορία χωρίς να σπάσει ο ιδρώτας
  • Ξεχωριστά TTLs για server vs client
  • Λειτουργεί απρόσκοπτα με HTMX (VaryByHeader)
  • Μετρητικός αντίκτυπος μέσω των μετρήσεων του Προμηθέα

Κονς Έχω χτυπήσει:

  • Αποσφαλμάτωση σύγχυσης (ξέχασε την κρύπτη)
  • Έκρηξη Cache με πολλούς συνδυασμούς παραμέτρων ερωτημάτων
  • Παράθυρο αναστολής (5 λεπτά για τους πελάτες)
  • Ανάγκη για χειροκίνητη ακύρωση των ενημερώσεων περιεχομένου

Καλύτερα για: Read-heavy εφαρμογές με κυρίως στατικό περιεχόμενο. Δεν είναι καλό για εξατομικευμένα ή σε πραγματικό χρόνο δεδομένα.

ViewBag BAR

Για ποιο λόγο το χρησιμοποιώ:

  • Δεδομένα παγκόσμιας διάταξης (analytics, κατηγορίες)
  • Τίτλος σελίδας

Πλεονεκτήματα στην πράξη:

  • Απλό και γρήγορο για δεδομένα επιπέδου διάταξης
  • Ορίστε μία φορά στο BaseController, διαθέσιμο παντού
  • Λειτουργεί μια χαρά με τη λίστα κατηγοριών cached

Κονς Έχω χτυπήσει:

  • Καμία ασφάλεια τύπου (typos αποτυχία κατά τη διάρκεια του τρεξίματος)
  • Πειραματισμός για υπερβολική χρήση περίπλοκων δεδομένων
  • Δύσκολο να δοκιμαστεί

Κανόνας που ακολουθώ: ViewBag για απλά scalars μόνο. Complex αντικείμενα πηγαίνουν στο ViewModels.

Απαιτήσεις Auth

Για ποιο λόγο το χρησιμοποιώ:

  • Ταυτότητα χρήστη, όνομα, email, URL avatar
  • Σημαία διαχειριστή (έλεγχος sub απαίτηση κατά της ρύθμισης)

Πλεονεκτήματα στην πράξη:

  • Ασφαλής (υπογεγραμμένο cookie, Προστασία Δεδομένων)
  • Αυτόματη με ταυτότητα πυρήνα ASP.NET/OAuth
  • Διαθέσιμο μέσω User.Claims παντού
  • Συρόμενη λήξη κρατά τους χρήστες συνδεδεμένοι

Κονς Έχω χτυπήσει:

  • Όριο μεγέθους cookie (μην υπερβάλλετε)
  • Οι ισχυρισμοί είναι στατικές μέχρι την επανεγγραφή
  • Αν προσθέσω έναν ισχυρισμό (όπως το "IsEditor"), πρέπει να ξαναεκδώσω cookie auth

Βέλτιστη πρακτική: Κρατήστε τις απαιτήσεις ελάχιστες και σταθερές. Μην τοποθετείτε συχνά μεταβαλλόμενα δεδομένα σε αξιώσεις.

Πράγματα που δεν χρησιμοποιώ (και γιατί)

Συνεδρία (ποτέ δεν χρησιμοποιείται):

  • Θα απαιτούσε κατανεμημένο κατάστημα (Redis)
  • Προσθέτει πολυπλοκότητα για το ελάχιστο όφελος
  • Οι περιπτώσεις χρήσης μου εξυπηρετούνται καλύτερα από:
    • Αμοιβαίες απαιτήσεις (ταυτότητα)
    • IMemoryCache (σύντομη κατάσταση ζωής)
    • Βάση δεδομένων (διαρκής κατάσταση)

TempData (ποτέ δεν χρησιμοποιήθηκε):

  • HTMX αποβάλλεται μετά- Redirect- Get μοτίβο
  • Έντυπα επιστρέφουν μερικές απόψεις με μηνύματα inline
  • Δεν χρειάζεται να επιβιώσετε ανακατευθύνσεις

HttpContext.Items (ποτέ δεν χρησιμοποιείται):

  • Δεν είχαμε μια υπόθεση χρήσης για την κατάσταση ανά αίτηση
  • Το μεσαίο μου λογισμικό δεν υπολογίζει τις τιμές για τους ελεγκτές
  • Αν χρειαζόμουν ανίχνευση ενοικιαστών, θα το χρησιμοποιούσα.

IDistributedCache (ποτέ δεν χρησιμοποιείται):

  • Ανάπτυξη ενός διακομιστή
  • IMemoryCache καλύπτει όλες τις ανάγκες
  • Θα χρησιμοποιούσε Redis αν εγώ κλίμακα σε πολλούς διακομιστές

Εξέλιξη της προσέγγισής μου

Φάση 1 (αρχική): Κάθε αίτημα χτύπησε τη βάση δεδομένων και έβγαλε τον Μάρκνταουν, δούλεψε μια χαρά για χαμηλή κίνηση.

Φάση 2 (πρώτη βελτιστοποίηση): Προστέθηκε IMemoryCache για κατηγορίες. Είδα άμεση μείωση φορτίου DB. Κράτησε απλό: 30 λεπτά TTL, καμία φανταχτερή λογική.

Φάση 3 (βαθμονόμηση): Προστέθηκε OutputCache για δημοσιεύσεις blog όταν αυξήθηκε η κυκλοφορία. Μαζική βελτίωση των επιδόσεων. αρχικό λάθος: cacheed για 5 λεπτά μόνο. Αυξήθηκε σε 1 ώρα μετά την παρακολούθηση έδειξε περιεχόμενο σπάνια αλλάζει.

Φάση 4 (παρατήρηση): Προστέθηκε SerilogTracing σε μνήμη μέτρησης. Ανακαλύφθηκε αστοχίες cache ήταν υψηλή λόγω της διαμόρφωσης ημερομηνία στα κλειδιά. Σταθερή μορφή κλειδί για να yyyyMMdd Αντί για πλήρεις χρονοσφραγίσεις, ο ρυθμός εκτέλεσης πήγε από 60% σε 95%.

Φάση 5 (ενσωμάτωση HTMX): Προστέθηκε VaryByHeader αντί hx-request. Αρχικά το ξέχασα και υπηρέτησε πλήρεις σελίδες σε αιτήματα HTMX. Αποσφαλματώνοντας εφιάλτη μέχρι να το καταλάβω.

Τρέχουσα κατάσταση: Happy with the stack. IMemoryCache + OutputCache + ResponseCache handle 98% of my state management needs. Βάση δεδομένων για σταθερή κατάσταση.

Συμβουλές για την εφαρμογή σας

Ξεκίνα από εδώ:

  1. Διαδρομή/έδαφος για πλοήγηση/φιλτράρισμα (πάντα απάτριδες πρώτα)
  2. Ισχυρισμοί ταυτότητας
  3. Βάση δεδομένων για οτιδήποτε πρέπει να επιμείνει
  4. IMemoryCache για αναγνωσμένα-βαρύ υπολογισμένα δεδομένα
  5. OutputCache για ακριβές σελίδες

Προσθέστε αν χρειάζεται: 6. Συνεδρία (μόνο αν πρέπει να έχετε server-side συνομιλία κατάσταση) 7. IDistributedCache (μόνο όταν κλιμακώνετε σε πολλούς διακομιστές) 8. Cookies (για τις προτιμήσεις του πελάτη, συγκατάθεση)

Αποφύγετε:

  • Συγκρότημα αντικειμένων στο ViewBag/TempData
  • Συνεδρία χωρίς κατανεμημένο κατάστημα υποστήριξης
  • Αποθήκευση δεδομένων που αφορούν το χρήστη ή συχνά αλλάζουν
  • Πρόωρη βελτιστοποίηση (μέτρο πρώτα!)

Μαθήματα κλειδιά:

  • Ξεκινήστε απλά, προσθέστε πολυπλοκότητα μόνο όταν οι μετρήσεις δείχνουν ότι το χρειάζεστε
  • Η παρατηρητικότητα είναι κρίσιμη (να ξέρετε τα ποσοστά χτύπημα κρύπτη σας!)
  • Σκεφτείτε τα όρια κλίμακας νωρίς (ανά-χρήστης caches χρειάζονται όρια)
  • Δοκιμαστική κρύπτη ακύρωση σχολαστικά (η μπαγιάτια είναι ένα πραγματικό πρόβλημα)
  • Καταγράψτε τις επιλογές σας TTL (προς το παρόν θα ρωτήσετε "γιατί 30 λεπτά;")

Τύλιγμα

Επιλέξτε την πιο ελαφριά επιλογή που καλύπτει τις ανάγκες σας, προτιμάτε τα μοτίβα των ανιθαγενών όταν μπορείτε, και να είναι σαφής σχετικά με την ασφάλεια και τον κύκλο ζωής.

Αν θέλετε να πάτε βαθύτερα για το πώς αυτά τα κομμάτια ρέουν μέσα από τον αγωγό, δείτε τη σειρά μου ξεκινώντας με Μέρος 1: Επισκόπηση και Ίδρυμα και ειδικά τα μεσαία λογισμικά και τα μέρη δρομολόγησης.

Χαρούμενο κτίριο.


Προσάρτημα: Περισσότερα αντιγραφέα παραδείγματα (επανορισμός)

Αυτά τα παραδείγματα εμβαθύνουν τις προηγούμενες ενότητες με λεπτομέρειες παραγωγής μπορείτε να επικολλήσετε σε net9 minimal πρότυπα, MVC, ή Razor Pages.

Cookies: Προστατέψτε τις τιμές με την προστασία δεδομένων

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

Συμβουλή: Σε εφαρμογές πολλαπλών κόμβων, επιμένουν τα κλειδιά προστασίας δεδομένων (π.χ., σε ένα κοινό σύστημα αρχείων, Redis, ή Azure Key Vault) ώστε τα cookies να μπορούν να διαβάζονται σε όλες τις περιπτώσεις.

Αντιαξέχαστη σε MVC, Razor Pages, και 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: διακοσμήστε δράσεις με [ValidateAntiForgeryToken] και χρήση @Html.AntiForgeryToken() σε μορφές.
  • Razor Pages: ενεργοποιημένη από προεπιλογή στις θέσεις φόρμας; χρήση asp-antiforgery="true" Αν χρειαστεί.
  • Ελάχιστη: επικύρωση μέσω IAntiforgery όπως φαίνεται.

Συνεδρία με Redis και συρόμενη vs απόλυτη λήξη

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

Αποθηκεύστε μικρά, συμπιεστικά δεδομένα μόνο.

IMemoryCache με επιλογές εισόδου και κλήση έξωσης

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 with jitter to doe battles

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

Για στιβαρό κλείδωμα, προτιμάτε Redis πρωτόγονα (SET NX EX) μέσω StackExchange.Redis.

Έκδοση και επικύρωση JWT τοπικά (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");

Ενημερώσεις υπό αίρεση με 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: σύνθετα αντικείμενα μέσω 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

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

Αυτό θα πρέπει να καλύπτει τα κενά: ισχυρότερες αθετήσεις ασφαλείας, ετοιμότητα πολλαπλών κόμβων, και πρότυπα πραγματικού κόσμου για κρύπτες, μάρκες και αιτήματα υπό όρους.


Βαθιά κατάδυση: HttpContext.Items (Πρακτικά πρότυπα και βοηθοί)

HttpContext.Items είναι μια τσάντα ανά αίτημα (IDictionary<αντικείμενο, αντικείμενο; >) που ζει μόνο για τη διάρκεια ζωής ενός μόνο αιτήματος. Είναι ιδανικό για τη μεταφορά υπολογισμένες τιμές από το μέσο λογισμικό / φίλτρα στα τελικά σημεία σας, ελεγκτές, και Razor Pages χειριστές χωρίς να αγγίζει παγκόσμια κατάσταση ή μακράς διάρκειας καταστήματα.

  • Κύκλος ζωής: δημιουργείται κατόπιν αιτήματος, απορρίπτεται όταν ολοκληρωθεί η απόκριση.
  • Πεδίο εφαρμογής: η τρέχουσα αίτηση μόνο □ ποτέ δεν διασχίζει τις επανακατευθύνσεις ή τις εργασίες υποβάθρου.
  • Performance: O(1) lookups; ιδανικό για κάθε αίτηση caching.
  • Ασφάλεια: μόνο στο server-side· δεν είναι ορατό στον πελάτη.

Γιατί αντικείμενα αντί

  • Session/TempData: Αυτά τα cross requests και να εισαγάγει ανησυχίες διανομής. Τα αντικείμενα είναι εφήμερο και κλίμακας-φιλικό.
  • DI Scoped services: Χρησιμοποιήστε αυτά για τη συμπεριφορά και τις κοινές εξαρτήσεις. Τα αντικείμενα είναι καλύτερα για ad-hoc, υπολογισμένες τιμές (tenant, user locale, feature flags) και ανά αίτημα caches.
  • HttpContext.Χαρακτηριστικά: Για χαρακτηριστικά επιπέδου πλαισίου/μεταφοράς (IEndpointFeature, IHttpUpgradeFeature).

Αποφύγετε τις βασικές συγκρούσεις: ισχυρά δακτυλογραφημένα πλήκτρα

Επειδή τα αντικείμενα χρησιμοποιούν τα κλειδιά αντικειμένων, προτιμούν τα ιδιωτικά στατικά κλειδιά αντικειμένων ή έναν ειδικό τύπο κλειδιού για να αποφύγουν τις συγκρούσεις ονόματος.

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

Ή δημιουργήστε ένα δακτυλογραφημένο περιτύλιγμα με επεκτάσεις:

public static class HttpContextItemsExtensions
{
    public static void Set<T>(this HttpContext ctx, object key, T value)
        => ctx.Items[key] = value!;

    public static T? Get<T>(this HttpContext ctx, object key)
        => ctx.Items.TryGetValue(key, out var v) ? (T?)v : default;

    public static T GetOrCreate<T>(this HttpContext ctx, object key, Func<T> factory)
    {
        if (ctx.Items.TryGetValue(key, out var existing) && existing is T typed)
            return typed;
        var created = factory();
        ctx.Items[key] = created!;
        return created;
    }
}

Μοτίβο: Υπολογίστε στο μεσαίο λογισμικό, καταναλώστε σε τελικά σημεία / ελεγκτές / σελίδες

// Program.cs
app.Use(async (ctx, next) =>
{
    var tenant = ctx.Request.Headers["X-TenantId"].FirstOrDefault() ?? "public";
    ctx.Set(ItemKeys.TenantId, tenant); // using extension above

    // Per-request cache holder (optional)
    ctx.Set(ItemKeys.PerRequestCache, new Dictionary<string, object?>());

    await next(ctx);
});

// Minimal API
app.MapGet("/whoami", (HttpContext ctx) => new
{
    Tenant = ctx.Get<string>(ItemKeys.TenantId),
});

// MVC Controller
public IActionResult WhoAmI()
    => Json(new { Tenant = HttpContext.Get<string>(ItemKeys.TenantId) });

// Razor Page handler
public IActionResult OnGet()
    => new JsonResult(new { Tenant = HttpContext.Get<string>(ItemKeys.TenantId) });

Μοτίβο: Ανά απαίτηση μνήμης για την αποφυγή επαναλαμβανόμενων εργασιών

Χρησιμοποιήστε τα αντικείμενα ως μια μικροσκοπική κρύπτη έτσι επαναλαμβανόμενες διαβάζει μέσα στο ίδιο αίτημα dont re-hit βάσεις δεδομένων / υπηρεσίες.

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

Σημειώσεις:

  • Threading: Ένα μόνο αίτημα εκτελεί συνήθως σε ένα λογικό μονοπάτι; Τα στοιχεία δεν είναι threa-safe για παράλληλες γράφει. Αν ξεκινήσετε παράλληλες εργασίες που μοιράζονται αντικείμενα, προσθέστε το δικό σας συγχρονισμό.
  • Μέγεθος: Κρατήστε τις τιμές μικρές και φθηνές για να υπολογίσετε / serialize.

Μοτίβο: Φίλτρα που κοσμούν αντικείμενα (MVC/Razor Pages)

public class TenantFilter : IAsyncResourceFilter
{
    public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
    {
        var tenant = context.HttpContext.Request.Headers["X-TenantId"].FirstOrDefault() ?? "public";
        context.HttpContext.Set(ItemKeys.TenantId, tenant);
        await next();
    }
}

// Register filter globally
services.AddControllersWithViews(o => o.Filters.Add<TenantFilter>());

Μοτίβο: Πλούσιος κούτσουρα χωρίς κατανομή παντού

Υπολογίστε μία φορά, στη συνέχεια, διαβάστε στο πεδίο καταγραφής ή το μεσαίο λογισμικό.

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

Πότε να μην χρησιμοποιήσετε τα αντικείμενα

  • Τα δεδομένα που απαιτούνται μετά την ανακατεύθυνσή τους ή σε όλες τις αιτήσεις (χρησιμοποιήστε το TempData/Session/DB αντ' αυτού).
  • IMemoryCache/IDistributedCache).
  • Αξίες που ανήκουν στην ταυτότητα/συγγραφή (χρησιμοποιήστε αξιώσεις/πολιτικές).

Γρήγορος κανόνας: Εάν υπολογίσει κατά τη διάρκεια αυτού του αιτήματος και διαβάσει μέσα σε αυτό το αίτημα από το δικό σας κώδικα, τα αντικείμενα είναι ιδανικά.

logo

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