**)٢٢٥-١١-١٠(**يضاف المزيد من الأمثلة العملية من قاعدة شفرتي الفعلية التي تبين أنماط العالم الحقيقي، والمقايضات، والوصايا، والتطور من نُهُج بسيطة إلى نُهُج متطوِّرة لإدارة الدولة. بما في ذلك الأنماط المفصَّلة ImoryCache، واستخدام ViewBag (الجيد والسيء)، واستراتيجيات الاستجابة لـCache/exputtsCache، والدروس المستفادة من الإنتاج.
HTTP هو مشهور عديم الجنسية. التطبيق الخاص بك... ليس. المستخدمون يوقعون في، يضيفون عناصر إلى السلال، يقفزون بين الصفحات، يعودون غدا، ويتوقعون منك أن تتذكر. في ASP.Net Corre (MVC, Razor pages, APIs) هناك الكثير من الطرق للحفاظ على حالة النقل بين الطلبات. بعضها هو لكل طلب فقط. البعض آخر جلسة. البعض يعيش في العميل. البعض منهم موزعون وينجو من عمليات إعادة تشغيل الخوادم. كل خيار له مقايضة بين الأمن، الأداء، الحجم، والمطور.
هذه الوظيفة تصنف الخيارات، تظهر الخرسانة، نسخ الأمثلة القابلة للشفاء في كل الأكوام الثلاثة، وتعطيك توجيه القرار حتى تتمكن من اختيار الأداة الصحيحة للوظيفة.
ملاحظة: هذا جزء من تجاربي مع 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 });
}
المثال:
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 لحماية الحمولات التي تضعها في الكعك بنفسك.
إعداد (البرامج:
builder.Services.AddControllersWithViews().AddSessionStateTempDataProvider(); // optional
builder.Services.AddSession();
var app = builder.Build();
app.UseSession();
في متحكم MVC:
TempData["StatusMessage"] = "Saved!";
return RedirectToAction("Index");
في مُعالج مُحَسِّز الصفحة:
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;
}
ذات مرة عملت على مشروع ضخم لتكنولوجيا المعلومات لحكومة المملكة المتحدة حيث أصبح سوء استخدام الدولة في الجلسة (بين العديد من خطايا معمارية أخرى) " كل شيء " (أ) أفضليات المستعملين، وبيانات الشكل المتعدد الخطوات، ونتائج البحث، والحسابات المؤقتة، وحتى النظريات المختزنة التي كان ينبغي أن تكون في مخبأ مناسب أو قاعدة بيانات مناسبة.
المشكلة: تم تخزين حالة الجلسة أثناء العملية (ASP.Net session State in Web. config, كان هذا قبل - يوم). كان على كل طلب أن يزيل التهجئة عن كائنات الجلسات الضخمة. كلما زاد الحمولة، كانت الجلسة تُنقل إلى عشرات ميغابايت لكل مستخدم. مع آلاف المستخدمين المتزامنين، نفدت الذاكرة من الخوادم.
:الحل المُسْتَحِل: طارنا إلى منشأة HP في شتوتغارت لإجراء اختبارات تحميل على سوبردوم - في ذلك الوقت، اقوى اورو اورو اورو اورو اورو اورو اورو اقوى ماكينة ويندوزكان وحشاً: العشرات من معالجات الإيتانيوم، ومئات الجيجا بايتات من إدارة السجلات والمحفوظات، وكانت الفكرة هي إثبات أنه مع ما يكفي من الأجهزة، يمكن للنظام أن يفي بالمتطلبات.
النتيجة: حتى على السوبرDome، لم نتمكن من ضرب أهداف المستخدم المتزامنة المطلوبة. بنية الحالة الجلسة كانت معطلة بشكل أساسي. التدرج الرأسي لا يستطيع حفظ التصميم السيء. الدورة التسلسلية / التضخيم العام، بالاقتران مع ضغط الذاكرة من كائنات الجلسات الضخمة، عنى النظام ببساطة لا يستطيع التدرج ليس بأي تكلفة معقولة.
الكابوس الأمني: أكثر من مشاكل الأداء، اكتشفنا خطأ في الترميز التسرب بين المستخدمينبيانات جلسات المستخدم A تظهر في وقت ما في جلسة المستخدم B. هذا لم يكن مجرد إحراج لقد كان كارثي. المستخدمون كانوا. موظفو الخدمات الصحية الوطنية قمنا عن طريق الخطأ بإنشاء آلية لخرق حماية البيانات التي يمكن أن تكشف المعلومات الطبية الحساسة عبر مختلف دورات المهنيين في مجال الرعاية الصحية.
ما كان ينبغي أن يحدث:
الدرس: ولاية الجلسة لا تتدرج عمودياً و بالكاد تتدرج أفقياً (حتى مع الجلسات اللاصقة أو المتاجر الموزعة، لا تزال تتسلسلة/تجسيد على كل طلب). أثبتت تجربة سوبردوم أن إلقاء العتاد على المشاكل المعمارية مكلف وغالباً ما يكون عقيماً.
والأهم من ذلك: حالة من الحالات التي تم فيها تحويل حشرات إلى نقاط ضعف أمنيةقضايا السلامة المحيطة، ظروف العرق، التعامل غير الصحيح مع هوية الجلسة - هذه لا تسبب مشاكل في الأداء فحسب، بل يمكنها تسريب بيانات حساسة بين المستخدمين. وفي سياق الرعاية الصحية (أو المصارف، أو أي صناعة خاضعة للتنظيم)، هذا هو كابوس الامتثال والمسؤولية الجنائية المحتملة.
المشورة الحديثة:
أنا المُقْصِل:
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);
});
(على سبيل المثال، 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");
});
إنذار مضاد من طراز Cach-as-as-as-state: إذا كان يجب أن يكون مستداماً أو موثوقاً، يخزنه في قاعدة بيانات ويخبئه اختيارياً.
المخبأ - الجانب (الأكثر شيوعاً):
قراءتها (عن طريق مكتبة/مزود): مقابض المخبأة تُحمّل على الأخطاء.
الكتابة من خلال: الكتابة تذهب إلى المخبأ و دعم المخزن متزامناً.
الكتابة من بعد: الكتابة إلى المخبأ، والتدفق إلى التخزين بشكل غير متزامن (الخطر: الخسارة/عدم الاتساق).
تحديث المفاتيح الساخنة قبل أن تنتهي لتجنّب الأخطاء الباردة.
عندما تحتاج إلى إبطال العديد من المدخلات ذات الصلة:
البادئات المطبعية: صدم نسخة عالمية في مفتاح صغير ومفاتيح تأليف معه.
// version key: "v:products"; keys like $"{version}:product:{id}"
var version = await cache.GetStringAsync("v:products") ?? "1";
var key = $"{version}:product:{id}";
لإبطال جميع المنتجات: زيادة في المنتجات مقابل المنتجات (الزبائن سيفتقدون بطبيعة الحال المفاتيح البادئة القديمة).
مجموعة وسم لكل مجموعة (Redes): حافظ على مجموعة مفاتيح لكل وسمة؛ عند الإبطال وجلب العناصر وحذف.
// 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);
البوب/الإلغاء الفرعي: نشر رسالة "البطلان: key" ؛ كل عقدة تزيل المفتاح من مكانها المحلي IMMoryCache.
(ج) ينبغي تحاشي SCAN/KYES في مسارات ساخنة مبسطة؛
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;
}
هذه هي الطريقة التي استخدمها في الواقع I memoryCache في منصة مدونتي، تطورت من خلال التجربة والخطأ. سأريكم ثلاثة أنماط حقيقية من بسيط إلى متطور.
كانت هذه أول عملية تنفيذ. تصنيفات المدونات لا تتغير كثيراً، لذا أخفيها لمدة 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 لكل مستخدم.
المقايضة: هذا لن يتسع لملايين المستخدمين. إذا أصبحت هذه مشكلة، سأنتقل إلى الـ Digistroped Cache (Redis) أو تخزين في قاعدة البيانات مع فهرس على المستخدم Id + بدء الوقت.
يقوم هذا المخابئ بتحليل القياسات ويتتبع فعالية المخابئ باستخدام تعقب 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 دقائق. ولكن قياسات أمامي ثقيلة لا يمكن جلبها ولا تتغير كثيراً، لذا لا بأس بساعة واحدة. اكتشفت هذا من خلال النظر في بيانات SerilogTtraves
رصد التنفيذ: في Seq (سجلّ سجلّي مُجَمَّع)، يمكنني الإستفسار:
ActivityName = "GetMetricsWithPrefix" and CacheHit = false
هذا يُخبرني أن معدل فاتني إذا كان مرتفعاً، سأعدّل تي تي إل أو الاستراتيجية الرئيسية.
□ النمط □ استخدام القضية □ انتهاء الصلاحية □ مراقبة الحجم □ المراقبة □ المراقبة □ |---------|----------|------------|--------------|---------------| | **** □ على الصعيد العالمي، ونادر التغير، ونادرا ما يتغير، و 30 دقيقة مطلقة مطلقة 30 دقيقة لا حاجة إليها (صغيرة) □ قطع الأشجار الأساسي □ | مهام الترجمة * لكل مستخدم، مقيد برقم 6h المطلق + 1h انزلاق دليل (5 بنود كحد أقصى) □ لا يوجد (ينبغي أن يضاف!) □ | **** ● المكالمات الخارجية الباهظة التكلفة o 1 ساعة مطلقة مطلقة o طبيعية (الزمن - النافذ) □ التعقب الكامل □
(أ) الدروس المستفادة:
عندما لا أستخدم أنا Memory Cache:
إنشاء كِك المعيث:
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();
مثال eTTAg:
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");
});
أستعمل كلاً من المستجيبين لـ CraffectCache و extractcache معاً على مدونتي لأغراض مختلفة. لهذا السبب قد ترغب في كليهما وكيف يختلفان.
SPSP. SP. ENS ثانياً - نُظُم كشك متشابهة:
Cache-Control, Vary(يخبر المتصفحات و CD CDNs كيف يمكن الاختباء
(هـ)يكمّلا بعضهما البعض. (أ) مقابض الاستجابة التي يقوم بها العميل/مقابض الاحتياجات المحلية المحلية؛
// 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:
أولاً: هل لدي رد مختزل على my-post + en لغته؟
المراس المراس:بعد أن يولّد برنامج الناتج المحلي الإجمالي الاستجابة، يضيف التقرير:
Cache-Control: public, max-age=300
Vary: hx-request
المُسْف المُسْجيخبئ المتصفح الرد لـ 300 ثانية (5 دقائق). الطلبات اللاحقة من نفس المستخدم لا تضرب الخادم حتى.
DCD (إذا كان باستخدام coodflare/fastly): CDN مخابئ لمدة 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 من شأنه أن يخدم النسخة الإنجليزية المخبأة.
مع VaryByQQureKeyseys: مُنتفِد:
/blog/my-post?language=en □ مُدرج في خانة مُجلِز يشمل "en"/blog/my-post?language=fr □ مُدرجات مُكْمِم المُجل في "fr"الاستخدام الحقيقي من قائمة مدونتي:
[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, /* ... */)
تحذير: كل مجموعة فريدة من البارامترات = مدخل مستقل للمخبأ:
page=1&pageSize=20&language=en مدخلة واحدةpage=2&pageSize=20&language=en مدخلة أخرىpage=1&pageSize=10&language=en مدخلة أخرى& & & & & & & & & & & &
المُلْخِل المُرْجِل العمل من الصندوق، ولكن من أجل مخرجات المجلج تحتاج إلى تثبيت:
// 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
%s: %s
Set-CookieCache-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 في المائة للمدوّنات (أغلب حركة المرور تضرب نفس المدوّنات الشعبية مراراً وتكراراً).
التأثير:
في بعض الأحيان أنا أقوم بالتنقيح و أحتاج إلى إجابات جديدة في كل مرة:
// 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:
كون:
عندما كنت اقفز
قراري: بالنسبة لمدوّنة ذات محتوى ثابت في الغالب ونسبة قراءة/كتابة عالية، فإن النقش المزدوج هو فوز ضخم. لن أستخدم هذا على لوحات الإدارة أو أجهزة المتابعة مع بيانات سريعة التغير.
هذه الثلاثة غالباً ما تكون مشوشة. وهنا كيف يختلفون وما الذي استخدمه فعلياً في الإنتاج.
// 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");
□ المعالم □ عرض عرض بيانات □ عرض □ عرض □ عرض □ عرض □ عرض □ عرض □ □ □ TemData o
| --------- | ---------- | --------- | ---------- |
|---|---|---|---|
| **** الطلب الحالي الطلب الحالي الطلب الحالي الطلب الأول إعادة توجيه | |||
| الوصول إلى المفتاح مفاتيح سلسلة سلسلة مفاتيح TM TRANSLAT TRANSLAT TRANSLAT TRANSLAT TRANSLAT TRANSLAT TRANSLAT | |||
| نوع السلامة لا يوجد (احتاجت التوقعات) لا يوجد (ديناميكية) لا يوجد (احتاجت النُهُج) | |||
| التحقق من صحة التحقق من الوقت رقم رقم رقم رقم رقم | |||
| نُجُو إعادة توجيه □ لا □ لا □ نعم □ |
في مدونتي، أستخدم فيو باج فقط لنقل البيانات من المتحكمين إلى التصميم المشترك (التحليلات، الفئات، الخ):
// 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 في كل طريقة من طرق العمل، وانتهكها وسهل نسيانها، وانقلها إلى 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);
}
حالة استخدام TMPDAT:
[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();
}
لماذا لا أستخدم TimPData في مدونتي:
[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!"
});
}
عندما تكون TimDatata منطقية:
TimDdata حصلت على ما يلي: بإفتراضي مدعوم ببسكويت (منذ ASP.NET or 2.0). إذا وضعت كائنات كبيرة في TmpData، فأنت تنفخ الكعكة المرسلة مع كل طلب. بالنسبة للدولة الكبيرة، استخدم الجلسة مع مخزن دعم أو 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
}
اختيارات سريعة:
جميع الرزمات الثلاثة تقع على نفس البدائيات (HttpConstex, نموذج ملزم, auth, protection).
Results.*.وجميعهم يشتركون في نفس آليات الولاية التي نوقشت هنا.
بعد عرض كل هذه الخيارات، هذا هو تقييمي الصادق لما يعمل في الإنتاج من أجل منصة مدونتي.
• الآلية / التواتر / استخدام القضايا / الترضية |-----------|-----------|-----------|--------------| | المُقَرَّخ تصنيفات، ومقاييس، ومهام الترجمة التحريرية العالية جداً | مخرجات المجلج "مدوّنات عالية الجودة" "مدوّنات مقوّمة" "قوائم" "فائز كبير جداً" | المُلْخِل المُرْجِل TTTP عالي الـ HTTP cascing headers o يعمل مع مخرجات Cache o | **** □ متوسط □ إعدادات التحليل، عناوين الصفحات □ □ على ما يرام للاشياء البسيطة □ | ألف- المطالبات □ هوية مستعمل متوسطة، علم إداري □ أداة سليمة للصدى □ | الطريق/الربع □ متوسط □ منخفضات ومرشحات ورصاصات □ عديمة الجنسية وقابلة للوصل □ | قاعدة بيانات • الوظائف، والتعليقات، ومصدر الحقيقة | **** أفضليات مستعملين منخفضة (في المستقبل) لم تكن هناك حاجة لها بعد | ثانياً - الدورة * لم يسبق أبداً أبداً - لا يوجد تجهيزات تخزين موزعة* | لا شيء * أبداً أبداً - يزيل HTMX الحاجة * | HTTTpConCon conconcing. Ims لم يسبق أبداً لم أستفيد من قضية | الاسم المُعَلَف كِصِص * أبداً - خادوم وحيد (في الوقت الحاضر) *
ما أستخدمه من أجله:
في الممارسة العملية:
:أنا أَضْربُ:
حدِّ الترويسة: إذا وصلت إلى خوادم متعددة، فسأحتاج لـ حالة متشاركة لـ الآن خادم مفرد مخبئ ذاكرة مثالي.
ما أستخدمه من أجله:
في الممارسة العملية:
:أنا أَضْربُ:
أفضل من أجل: تطبيقات مثقلة بالقراءة مع المحتوى الثابت في الغالب. ليست جيدة للبيانات الشخصية أو في الوقت الحقيقي.
ما أستخدمه من أجله:
في الممارسة العملية:
:أنا أَضْربُ:
المادة الأولى تنص على ما يلي: عرض باج لـ بسيط قياسي فقط كائنات معقّد بوصة عرض موجّهات.
ما أستخدمه من أجله:
sub مـن المطالبـةفي الممارسة العملية:
User.Claims في كل مكان:أنا أَضْربُ:
الممارسات الفضلى: لا تضع بيانات متواترة التغير في المطالبات
(لم يُستخدم بعد):
15/م/م أ/م/م/م/م أ/م/م/م أ/م/م أ/م/م أ/م/م/م أ/م/م أ/م/م/م أ/م/ك/م أ/م/م/ك/م/م/ك/م/م/ك/م/م/مم أم/م/ع/م/م/م أم/م/م/ع/م/م/ ع/م/م ع/ ممم/ ع/ م مم/ ع/م/ ع:
HttpContext. Ims (لم يُستخدم بعد):
المحلات المُعَيّل كِشِش (لم يُستخدَم قط):
المرحلة الأولى (الرحلة الأولى): كل طلب إصطدم بقاعدة البيانات وأعطى علامة، عمل بشكل جيد لقلة حركة المرور.
المرحلة 2 (الترتيب الأمثل الأول): مضاف IMMMoryCaches لـ فئات. شاهد الانتقاص الفوري من حمولة DB. أبقى الأمر بسيطا: 30 دقيقة TTL، لا منطق فاخر.
المرحلة 3 (التصعيد): إضافة مُضاف مخرجات لـ مدوّنات لـ مدوّنات لـ مُستخدَم أداء خطأ الأوّلي مخبئ لـ 5 دقائق فقط زيادة إلى ساعة بعد رصد المحتوى يُظهِر تغيراً نادراً.
المرحلة 4 (الملاحظات): مُضِف SeriilogTrap إلى المخبأ المترّسِر المخبأ المُخبِّر المُسْتَلَسِر المُخْرِفُ المُخْرَفَات كَانَتْ مُثْرِلًا لـ التاريخ المُنتفِرَفَة في المُمَثِّلَات. yyyyMMdd معدل الاصطدام ارتفع من 60% إلى 95%
المرحلة 5 (التكامل HTMX): VaryByHeader - - - - - - - - - - - - - hx-requestنسيت هذا في البداية وقدمت صفحات كاملة إلى طلبات HTMX.
الحالة الحالية: سعيد مع رزمة I memoryCacke + productCacke + responced Cache التعامل مع 98٪ من احتياجاتي من إدارة الولاية. قاعدة بيانات الحالة الدائمة. Athth يَدّعي للهوية. هذا هو.
بدء التشغيل هنا::
ما إذا كان هناك حاجة إلى ما يلي: الجلسة 6 (فقط إذا كان يجب أن يكون لديك حالة محادثية في جانب الخادم) كاش (فقط عندما تتدرج إلى خوادم متعددة) 8 - الكعكات (للأفضلات، والرضا، والرضا، جانب العميل)
المُسَجَّل:
(أ) الدروس المستفادة:
الدولة في تطبيقات الويب ليست واحدة تناسب الجميع. اختر الخيار الأضيق الذي يلبي احتياجاتك، تفضل أنماط عديمة الجنسية عندما تستطيع، وكن صريحاً حول الأمن ودورة الحياة.
إذا أردت أن تتعمق أكثر في كيفية تدفق هذه القطع عبر خط الأنابيب، انظر سلسلتي تبدأ مع الجزء 1: لمحة عامة ومؤسسة و خصوصاً المنتصف و الأجزاء الموجّهة
بناء بناء سعيد.
هذه الأمثلة تُعمّق الأجزاء السابقة مع تفاصيل الإنتاج Quester production production checks يمكنك لصقها في قوالب الحد الأدنى الصافي 9، MVC، أو صفحات الرازور.
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 Voot) بحيث يمكن قراءة الكعك عبر الحالات.
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();
اخزن البيانات الصغيرة والضاغطة فقط واثبت العربات/الطلبات الحقيقية لـ DB
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) عن طريق takexchange. 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();
});
وهذا من شأنه أن يغطي الثغرات: حالات قصور أمني أقوى، والتأهب المتعدد الأنسانود، وأنماط عالمية حقيقية للمخابئ، والعلامات، والطلبات المشروطة.
Http construct. Items عبارة عن حقيبة لكل طلب (IDictionary<object, object?>) تعيش فقط مدى عمر طلب واحد واحد. إنها مثالية لنقل القيم المحسوبة من البرمجيات المتوسطة/المرشحات إلى نقاط النهاية الخاصة بك، وأجهزة التحكم، ومعاملات صفحات المزالق بدون لمس حالة عالمية أو مخازن طويلة العمر.
لأن البنود تستخدم مفاتيح كائنات أو تفضل مفاتيح كائنات ثابتة خاصة أو نوع مفتاح مخصص لتجنب الاصطدامات بالاسم.
public static class ItemKeys
{
public static readonly object TenantId = new();
public static readonly object UserLocale = new();
public static readonly object PerRequestCache = new();
}
أو إنشاء a مع:
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) });
استخدام الأصناف كمخابئ صغير جداً يُقرأ في نفس الطلب أنها لا تعيد تشغيل قواعد البيانات/الخدمات.
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.