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
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
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);
});
app.MapGet("/whoami", (HttpContext ctx) => new { Tenant = ctx.Items["TenantId"] });
public IActionResult WhoAmI() => Json(new { Tenant = HttpContext.Items["TenantId"] });
public IActionResult OnGet() => new JsonResult(new { Tenant = HttpContext.Items["TenantId"] });
Παραδείγματα
app.MapGet("/orders/{id:int}", (int id, int? page) => Results.Ok(new { id, page }));
// GET /orders/5?page=2
[HttpGet("/orders/{id:int}")]
public IActionResult Details(int id, int? page)
=> View(new { id, page });
public IActionResult OnGet(int id, int? page)
=> Page();
Δημιουργία συνδέσμων που διατηρούν την κατάσταση:
// Razor Pages
<a asp-page="/Orders/Details" asp-route-id="@Model.Id" asp-route-page="@Model.Page">Next</a>
// MVC
@Html.ActionLink("Next", "Details", "Orders", new { id = Model.Id, page = Model.Page }, null)
app.Use(async (ctx, next) =>
{
var correlationId = ctx.Request.Headers["X-Correlation-Id"].FirstOrDefault()
?? Guid.NewGuid().ToString("n");
ctx.Response.Headers["X-Correlation-Id"] = correlationId;
await next(ctx);
});
Αδιευκρίνιστος χαρακτήρας (σύνδεσμος):
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 μοτίβο σε σελίδες 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)
[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 });
}
Ελάχιστο παράδειγμα 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.
Ρύθμιση (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]
Ρύθμιση:
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 Είχαμε δημιουργήσει κατά λάθος ένα μηχανισμό παραβίασης της προστασίας δεδομένων που θα μπορούσε να εκθέσει ευαίσθητες ιατρικές πληροφορίες σε διάφορες συνεδρίες επαγγελματιών υγείας.
Τι θα έπρεπε να είχε συμβεί:
Το μάθημα: Η κατάσταση συνεδρίας δεν κλιμακώνεται κάθετα και ίσα που ζυγίζει οριζόντια (ακόμα και με κολλώδεις συνεδρίες ή κατανεμημένα καταστήματα, εξακολουθείτε να ταξινομείτε/επιβράβευση σε κάθε αίτημα).Το πείραμα Superdome απέδειξε ότι ρίχνοντας υλικό σε αρχιτεκτονικά προβλήματα είναι ακριβό και συχνά μάταιο.
Αλλά το πιο σημαντικό: bugs κατάστασης συνεδρίας γίνονται ευάλωτα σημεία ασφαλείας. Thread-safety θέματα, φυλετικές συνθήκες, εσφαλμένος χειρισμός ID συνεδρίας αυτά δεν προκαλούν μόνο προβλήματα απόδοσης, μπορούν να διαρρεύσουν ευαίσθητα δεδομένα μεταξύ των χρηστών. Σε ένα πλαίσιο υγειονομικής περίθαλψης (ή τραπεζικό, ή οποιαδήποτε ρυθμιζόμενη βιομηχανία), αυτό είναι ένας εφιάλτης συμμόρφωσης και πιθανή ποινική ευθύνη.
Σύγχρονη συμβουλή:
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");
});
Προειδοποίηση κατά του περιγράμματος: εάν πρέπει να είναι ανθεκτικό ή έγκυρο, να το αποθηκεύσετε σε μια βάση δεδομένων και να το κρύψετε προαιρετικά.
Κρηπιδώματα (πιο συχνές):
Read-through (μέσω βιβλιοθήκης/προμηθευτή): cache χειρίζεται τη φόρτωση σε αστοχίες.
Write-through: γράφει πηγαίνει σε κρύπτη και backing κατάστημα συγχρονικά.
Εγγραφή-πίσω: γράψτε στην κρύπτη, ξεπλύνετε για να αποθηκεύσετε ασύγχρονα (κίνδυνος: απώλεια/ασυνέπεια).
Ανανέωση: αναζωογονήστε τα ζεστά κλειδιά πριν λήξει για να αποφύγετε τις κρύες αστοχίες.
Όταν χρειάζεται να ακυρώσετε πολλές σχετικές καταχωρήσεις:
Εκδοθέντα προθέματα (μαλακή ακύρωση): χτυπήστε μια παγκόσμια έκδοση σε ένα μικρό κλειδί και συνθέστε τα πλήκτρα με αυτό.
// 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 μονοπάτια; εντάξει για τη διαχείριση εργαλείων σε μικρούς χώρους κλειδιά.
T GetOrAdd<T>(IMemoryCache cache, string key, Func<ICacheEntry, T> factory)
=> cache.GetOrCreate(key, e =>
{
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
e.SlidingExpiration = TimeSpan.FromMinutes(2);
e.Priority = CacheItemPriority.Normal;
e.Size = 1;
return factory(e);
});
static async Task<T?> GetOrSetJsonAsync<T>(IDistributedCache cache, string key, Func<Task<T>> factory, TimeSpan ttl)
{
var json = await cache.GetStringAsync(key);
if (json is not null)
return System.Text.Json.JsonSerializer.Deserialize<T>(json);
var value = await factory();
var opts = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = ttl };
await cache.SetStringAsync(key,
System.Text.Json.JsonSerializer.Serialize(value),
opts);
return value;
}
Εδώ είναι το πώς χρησιμοποιώ πραγματικά IMemoryCache στην πλατφόρμα blog μου, εξελίχθηκε μέσω της δοκιμής και του λάθους. Θα σας δείξω τρία πραγματικά πρότυπα από απλή έως εξελιγμένη.
Αυτό ήταν το πρώτο μου 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;
}
Γιατί αυτό λειτουργεί:
Σε χτύπησα. Αρχικά χρησιμοποίησα ένα κλειδί συμβολοσειράς "Categories" . Λειτουργεί μια χαρά μέχρι να έχετε πολλούς ελεγκτές και ένα κατά λάθος ξαναχρησιμοποιεί το ίδιο κλειδί . Τώρα χρησιμοποιώ σταθερές ή έντονα δακτυλογραφημένα πλήκτρα (δείτε την προηγούμενη ενότητα για την αποφυγή συγκρούσεων).
Είναι πιο περίπλοκο επειδή χρειάζεται όρια και θα πρέπει να παραμείνει ζωντανός όσο ο χρήστης είναι ενεργός:
// 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)
});
}
}
Γιατί αυτό είναι διαφορετικό:
Λάθος που έκανα: Αρχικά δεν περιόρισα τον αριθμό των εργασιών. Ένας χρήστης ενέργειας πυροδότησε τις μεταφράσεις 50+ και είχα διαρροή μνήμης. Τώρα κρατάω το μέγιστο 5 ανά χρήστη.
Ανταλλαγές: Αν αυτό γίνει πρόβλημα, θα μετακομίσω στο IDistributedCache (Redis) ή θα αποθηκεύσω στη βάση δεδομένων με έναν δείκτη στο userId + startTime.
Αυτό 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;
}
}
Τι κάνει αυτή την παραγωγή έτοιμη:
yyyyMMdd μορφή σε πλήκτρο τόσο διαφορετικές φορές την ίδια ημέρα μοιραστείτε την κρύπτηΕξέλιξη: Αρχικά κόλλησα για 10 λεπτά. Αλλά Umami μετρήσεις είναι βαριά για να φέρει και να μην αλλάξει πολλά, έτσι 1 ώρα είναι μια χαρά. Ανακάλυψα αυτό με την εξέταση των δεδομένων SerilogΑκολουθώντας και βλέποντας υπερβολική κρύπτη αστοχίες.
Παρακολούθηση της δράσης: Στο Seq (το αρχείο καταγραφής μου), μπορώ να ρωτήσω:
ActivityName = "GetMetricsWithPrefix" and CacheHit = false
Αν είναι ψηλά, προσαρμόζω το TTL ή τη βασική στρατηγική.
Το μοτίβο χρήσης της υπόθεσης λήξης του μεγέθους του ελέγχου |---------|----------|------------|--------------|---------------| | Κατηγορίες Σε παγκόσμιο επίπεδο, σπάνια αλλάζει 30 λεπτά απόλυτο ~ Δεν χρειάζεται (μικρό) ~ Basic loging ~ | Μεταφραστικά καθήκοντα Ανά χρήστη, ορίζεται 6h απόλυτο + 1h συρόμενο εγχειρίδιο (5 items max) Κανένα (θα πρέπει να προσθέσετε!) | Μετρικός Ακριβές εξωτερικές κλήσεις 1h απόλυτη ~ Φυσική (χρόνος-παράκαμψη) ~ Πλήρης ιχνηλάτηση ~
Μαθήματα κλειδιά:
Όταν δεν χρησιμοποιώ IMemoryCache:
Ρυθμίστε το 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) }));
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
Σχεδίαση πυρήνα 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);
});
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");
});
Χρησιμοποιώ τόσο ResponseCache και OutputCache μαζί στο blog μου για διαφορετικούς σκοπούς.
Το ASP.NET Core έχει δύο παρόμοιας όψης συστήματα caching:
Cache-Control, Vary) λέγοντας browsers και CDNs πώς να κρυφτείτε
εΣυμπληρώνονται μεταξύ τους. ResponseCache χειρίζεται client/CDN caching; OutputCache hands server-side caching.
// 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:
Έλεγχος εξόδουCache πρώτα: Έχω μια cached απάντηση για my-post + en Τη γλώσσα;
Η απάντησηCache θέτει κεφαλίδες: Μετά το OutputCache παράγει την απάντηση, η απάντησηCache προσθέτει:
Cache-Control: public, max-age=300
Vary: hx-request
Caching browser: Browser caches την απάντηση για 300 δευτερόλεπτα (5 λεπτά). Επακόλουθε αιτήματα από τον ίδιο χρήστη δεν χτυπήσει καν το διακομιστή.
Caching CDN (αν χρησιμοποιείτε Cloudflare/Fastly): CDN caches για 5 λεπτά. Χρήστες σε όλο τον κόσμο χτύπησε το CDN, όχι τον διακομιστή μου.
[ResponseCache(Duration = 300)] // 5 minutes client/CDN cache
[OutputCache(Duration = 3600)] // 1 hour server cache
Λογισμός:
Εξέλιξη: Αρχικά είχα και τα δύο στα 5 λεπτά... αλλά αυτό σήμαινε ότι ο διακομιστής μου ξανάρχιζε κάθε 5 λεπτά... ακόμα κι αν το περιεχόμενο σπάνια αλλάζει.
VaryByHeader = "hx-request" // ResponseCache
VaryByHeaderNames = new[] { "hx-request" } // OutputCache
Γιατί αυτό μετράει: Οι αιτήσεις HTMX περιλαμβάνουν hx-request: true Επιστρέφω διαφορετικές απαντήσεις:
Χωρίς 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
[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 → Μια άλλη καταχώρησηΜιτιξοποίηση:
Απάντηση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 φεύγει για:
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 (το μεγαλύτερο μέρος της κυκλοφορίας χτυπά τις ίδιες δημοφιλείς θέσεις επανειλημμένα).
Αντίκτυπος:
Μερικές φορές αποσφαλματώνω και χρειάζομαι νέες απαντήσεις κάθε φορά:
// 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:
Κατά:
Όταν έκανα κοπάνα
Η ετυμηγορία μου: Για ένα blog με κυρίως στατικό περιεχόμενο και υψηλή αναλογία ανάγνωσης/γραφής, διπλή caching είναι μια τεράστια νίκη.
Αυτοί οι τρεις συχνά είναι μπερδεμένοι. 'κου πώς διαφέρουν και τι χρησιμοποιώ στην παραγωγή.
// 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οβάλετε. | |||
| Ανακατευθύνσεις επιβίωσης Όχι, όχι, όχι, όχι, ναι, ναι |
Στο 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>
}
Γιατί αυτό το μοτίβο λειτουργεί:
Λάθος που έκανα νωρίς: Ήμουν έτοιμος. 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>
Αυτό είναι εντάξει επειδή:
Title και Category ιδιότητες σε κάθε μοντέλο προβολήςΑντικολλητικός δίσκος:
// 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 };
Προβλήματα:
ViewBag.Usr αποτυγχάνει κατά τη διάρκεια του τρεξίματος)Καλύτερα: Ισχυρά δακτυλογραφημένα μοντέλα προβολής:
// 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:
[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 μου:
[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!"
});
}
Όταν το TempData βγάζει νόημα:
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]
Οι κανόνες μου:
Ποιος κάτοχος κράτους να χρησιμοποιήσει;
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
Παράδειγμα (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");
});
TempData["Flash"] = "Profile saved";
return RedirectToAction("Index");
Θέα ξυραφιού:
@if (TempData["Flash"] is string flash) {
<div class="alert alert-info">@flash</div>
}
flowchart LR U[User] -- cart-id cookie --> S[Server] S --> DB[(Cart Table)] S <--> Cache[Distributed Cache]
Secure, HttpOnly, SameSite, IsEssential (εάν απαιτείται από τη συγκατάθεση/λειτουργική ανάγκη).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
}
Γρήγορες επιλογές:
Και οι τρεις στοίβες κάθονται στα ίδια πρωτόγονα (HttpContext, σύνδεση μοντέλου, auth, προστασία δεδομένων). Τα παραπάνω παραδείγματα δείχνουν ότι οι API διαφέρουν κυρίως στην εργονομία:
Results.*.Όλοι μοιράζονται τους ίδιους κρατικούς μηχανισμούς που συζητούνται εδώ.
Αφού σας δείξω όλες αυτές τις επιλογές, ορίστε η ειλικρινής εκτίμησή μου για το τι λειτουργεί στην παραγωγή για την πλατφόρμα blog μου.
~ Μηχανισμός ~ Συχνότητα ~ Χρησιμοποίησε υποθέσεις ~ Ικανοποίηση ~ |-----------|-----------|-----------|--------------| | IMemoryCache Πολύ υψηλές κατηγορίες, μετρικές, μεταφραστικές εργασίες | OutputCache ~ Υψηλή ~ Rendered blog posts, lists ~ Τεράστιο perf win ~ | ΑπάντησηCache Η υψηλή κεφαλίδα HTTP λειτουργεί με το OutputCache | Προβολή ετικέταςName Ρυθμίσεις Μεσαίων Αναλυτών, τίτλοι σελίδων Εντάξει για απλά πράγματα | Ισχυρισμοί επωνυμίας Μεσαία ταυτότητα χρήστη, σημαία διαχειριστή | Διαδρομή/Κορυφή Μεσαίος κορμός, φιλτράρισμα, γυμνοσάλιαγκες | Βάση δεδομένων Μεσαίο ~ Blog αναρτήσεις, σχόλια, state ~ Πηγή της αλήθειας ~ | Cookies Οι χαμηλές προτιμήσεις των χρηστών (μελλοντικά) δεν χρειάζονται ακόμα. | Σύνοδος Δεν υπάρχει κατανεμημένο store setup | TempData Ποτέ δεν είναι έτσι η HTMX εξαλείφει την ανάγκη | HttpContext.Items Ποτέ δεν είχες χρησιμοποιήσει την υπόθεση. | IDistributedCache Ποτέ δεν θα είναι ένας ενιαίος διακομιστής (για τώρα)
Για ποιο λόγο το χρησιμοποιώ:
Πλεονεκτήματα στην πράξη:
Κονς Έχω χτυπήσει:
Όριο κλιμάκωσης: Αν φτάσω σε πολλούς διακομιστές, θα χρειαστώ IDistributedCache (Redis) για κοινή κατάσταση. Για τώρα, single server + μνήμη κρύπτη είναι τέλεια.
Για ποιο λόγο το χρησιμοποιώ:
Πλεονεκτήματα στην πράξη:
Κονς Έχω χτυπήσει:
Καλύτερα για: Read-heavy εφαρμογές με κυρίως στατικό περιεχόμενο. Δεν είναι καλό για εξατομικευμένα ή σε πραγματικό χρόνο δεδομένα.
Για ποιο λόγο το χρησιμοποιώ:
Πλεονεκτήματα στην πράξη:
Κονς Έχω χτυπήσει:
Κανόνας που ακολουθώ: ViewBag για απλά scalars μόνο. Complex αντικείμενα πηγαίνουν στο ViewModels.
Για ποιο λόγο το χρησιμοποιώ:
sub απαίτηση κατά της ρύθμισης)Πλεονεκτήματα στην πράξη:
User.Claims παντούΚονς Έχω χτυπήσει:
Βέλτιστη πρακτική: Κρατήστε τις απαιτήσεις ελάχιστες και σταθερές. Μην τοποθετείτε συχνά μεταβαλλόμενα δεδομένα σε αξιώσεις.
Συνεδρία (ποτέ δεν χρησιμοποιείται):
TempData (ποτέ δεν χρησιμοποιήθηκε):
HttpContext.Items (ποτέ δεν χρησιμοποιείται):
IDistributedCache (ποτέ δεν χρησιμοποιείται):
Φάση 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. Βάση δεδομένων για σταθερή κατάσταση.
Ξεκίνα από εδώ:
Προσθέστε αν χρειάζεται: 6. Συνεδρία (μόνο αν πρέπει να έχετε server-side συνομιλία κατάσταση) 7. IDistributedCache (μόνο όταν κλιμακώνετε σε πολλούς διακομιστές) 8. Cookies (για τις προτιμήσεις του πελάτη, συγκατάθεση)
Αποφύγετε:
Μαθήματα κλειδιά:
Επιλέξτε την πιο ελαφριά επιλογή που καλύπτει τις ανάγκες σας, προτιμάτε τα μοτίβα των ανιθαγενών όταν μπορείτε, και να είναι σαφής σχετικά με την ασφάλεια και τον κύκλο ζωής.
Αν θέλετε να πάτε βαθύτερα για το πώς αυτά τα κομμάτια ρέουν μέσα από τον αγωγό, δείτε τη σειρά μου ξεκινώντας με Μέρος 1: Επισκόπηση και Ίδρυμα και ειδικά τα μεσαία λογισμικά και τα μέρη δρομολόγησης.
Χαρούμενο κτίριο.
Αυτά τα παραδείγματα εμβαθύνουν τις προηγούμενες ενότητες με λεπτομέρειες παραγωγής μπορείτε να επικολλήσετε σε net9 minimal πρότυπα, MVC, ή Razor Pages.
using Microsoft.AspNetCore.DataProtection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDataProtection();
var app = builder.Build();
app.MapPost("/prefs/secure/{value}", (HttpContext ctx, string value, IDataProtectionProvider dp) =>
{
var protector = dp.CreateProtector("prefs.theme");
var protectedValue = protector.Protect(value);
ctx.Response.Cookies.Append("pref.theme.p", protectedValue, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Lax,
Expires = DateTimeOffset.UtcNow.AddYears(1)
});
return Results.Ok();
});
app.MapGet("/prefs/secure", (HttpContext ctx, IDataProtectionProvider dp) =>
{
if (ctx.Request.Cookies.TryGetValue("pref.theme.p", out var v))
{
var protector = dp.CreateProtector("prefs.theme");
return Results.Text(protector.Unprotect(v));
}
return Results.NotFound();
});
Συμβουλή: Σε εφαρμογές πολλαπλών κόμβων, επιμένουν τα κλειδιά προστασίας δεδομένων (π.χ., σε ένα κοινό σύστημα αρχείων, Redis, ή Azure Key Vault) ώστε τα cookies να μπορούν να διαβάζονται σε όλες τις περιπτώσεις.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddAntiforgery(o => o.HeaderName = "X-CSRF-TOKEN");
var app = builder.Build();
app.MapGet("/antiforgery/token", (IAntiforgery af, HttpContext ctx) =>
{
var tokens = af.GetAndStoreTokens(ctx);
return Results.Json(new { token = tokens.RequestToken });
});
app.MapPost("/submit", (HttpContext ctx) => Results.Ok("posted"))
.AddEndpointFilter(async (efiContext, next) =>
{
var af = efiContext.HttpContext.RequestServices.GetRequiredService<IAntiforgery>();
await af.ValidateRequestAsync(efiContext.HttpContext);
return await next(efiContext);
});
app.MapControllers();
app.MapRazorPages();
[ValidateAntiForgeryToken] και χρήση @Html.AntiForgeryToken() σε μορφές.asp-antiforgery="true" Αν χρειαστεί.IAntiforgery όπως φαίνεται.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();
Αποθηκεύστε μικρά, συμπιεστικά δεδομένα μόνο.
builder.Services.AddMemoryCache();
app.MapGet("/fx/{pair}", (IMemoryCache cache, string pair) =>
{
var key = $"fx:{pair.ToLowerInvariant()}";
return Results.Ok(cache.GetOrCreate(key, entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
entry.SlidingExpiration = TimeSpan.FromMinutes(2);
entry.Size = 1; // enable size-based eviction if configured
entry.RegisterPostEvictionCallback((k, v, reason, state) =>
{
Console.WriteLine($"Evicted {k} because {reason}");
});
return 0.92m; // fetch from external service in real life
}));
});
builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");
app.MapGet("/feature/{name}", async (IDistributedCache cache, string name) =>
{
var key = $"feat:{name}";
var cached = await cache.GetStringAsync(key);
if (cached is not null) return Results.Text(cached);
// Lock key to prevent thundering herd (very simple approach)
var lockKey = key + ":lock";
var gotLock = await cache.SetStringAsync(lockKey, "1", new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5)
});
try
{
cached = await cache.GetStringAsync(key);
if (cached is null)
{
var computed = "on"; // expensive work
var rnd = Random.Shared.Next(0, 15); // jitter
await cache.SetStringAsync(key, computed, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5).Add(TimeSpan.FromSeconds(rnd))
});
cached = computed;
}
}
finally
{
await cache.RemoveAsync(lockKey);
}
return Results.Text(cached);
});
Για στιβαρό κλείδωμα, προτιμάτε Redis πρωτόγονα (SET NX EX) μέσω StackExchange.Redis.
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("super-secret-key-please-rotate"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
IssuerSigningKey = key,
ValidateIssuerSigningKey = true,
ValidateLifetime = true
};
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapPost("/token", () =>
{
var claims = new[] { new Claim(ClaimTypes.Name, "alice") };
var jwt = new JwtSecurityToken(claims: claims, expires: DateTime.UtcNow.AddMinutes(30), signingCredentials: creds);
var token = new JwtSecurityTokenHandler().WriteToken(jwt);
return Results.Json(new { access_token = token });
});
app.MapGet("/who", [Microsoft.AspNetCore.Authorization.Authorize] () => "ok");
record Todo(int Id, string Title, string Version);
var store = new Dictionary<int, Todo> { [1] = new(1, "Ship", "v1") };
app.MapGet("/todo/{id:int}", (int id, HttpContext ctx) =>
{
if (!store.TryGetValue(id, out var t)) return Results.NotFound();
ctx.Response.Headers.ETag = t.Version;
return Results.Json(t);
});
app.MapPut("/todo/{id:int}", (int id, HttpContext ctx, Todo input) =>
{
if (!store.TryGetValue(id, out var current)) return Results.NotFound();
var ifMatch = ctx.Request.Headers["If-Match"].ToString();
if (string.IsNullOrEmpty(ifMatch) || ifMatch != current.Version)
return Results.StatusCode(StatusCodes.Status412PreconditionFailed);
var next = current with { Title = input.Title, Version = $"v{DateTime.UtcNow.Ticks}" };
store[id] = next;
ctx.Response.Headers.ETag = next.Version;
return Results.Ok(next);
});
public static class TempDataJsonExtensions
{
public static void Put<T>(this ITempDataDictionary tempData, string key, T value)
=> tempData[key] = System.Text.Json.JsonSerializer.Serialize(value);
public static T? Get<T>(this ITempDataDictionary tempData, string key)
=> tempData.TryGetValue(key, out var o) && o is string s
? System.Text.Json.JsonSerializer.Deserialize<T>(s)
: default;
}
// Usage in MVC action
TempData.Put("WizardState", new { Step = 2, Name = "Alice" });
var state = TempData.Get<dynamic>("WizardState");
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
[Timestamp] public byte[] RowVersion { get; set; } = default!;
}
// On update
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
return Results.StatusCode(StatusCodes.Status412PreconditionFailed);
}
app.MapPost("/promote", async (HttpContext ctx) =>
{
var u = ctx.User;
var claims = u.Claims.ToList();
claims.Add(new Claim(ClaimTypes.Role, "Editor"));
var id = new ClaimsIdentity(claims, "Cookies");
await ctx.SignInAsync("Cookies", new ClaimsPrincipal(id));
return Results.Ok();
});
Αυτό θα πρέπει να καλύπτει τα κενά: ισχυρότερες αθετήσεις ασφαλείας, ετοιμότητα πολλαπλών κόμβων, και πρότυπα πραγματικού κόσμου για κρύπτες, μάρκες και αιτήματα υπό όρους.
HttpContext.Items είναι μια τσάντα ανά αίτημα (IDictionary<αντικείμενο, αντικείμενο; >) που ζει μόνο για τη διάρκεια ζωής ενός μόνο αιτήματος. Είναι ιδανικό για τη μεταφορά υπολογισμένες τιμές από το μέσο λογισμικό / φίλτρα στα τελικά σημεία σας, ελεγκτές, και Razor Pages χειριστές χωρίς να αγγίζει παγκόσμια κατάσταση ή μακράς διάρκειας καταστήματα.
Επειδή τα αντικείμενα χρησιμοποιούν τα κλειδιά αντικειμένων, προτιμούν τα ιδιωτικά στατικά κλειδιά αντικειμένων ή έναν ειδικό τύπο κλειδιού για να αποφύγουν τις συγκρούσεις ονόματος.
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);
});
Σημειώσεις:
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);
}
});
Γρήγορος κανόνας: Εάν υπολογίσει κατά τη διάρκεια αυτού του αιτήματος και διαβάσει μέσα σε αυτό το αίτημα από το δικό σας κώδικα, τα αντικείμενα είναι ιδανικά.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.