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
更新(2025-11-10): 从我的实际代码库中添加了更多实际例子,显示真实世界模式、权衡取舍、平衡取舍以及从简单国家管理方法向复杂国家管理方法的演变。 包括详细的IMemoryCache模式、ViewBag(好坏)使用情况、反应Cache/OutputCache战略以及从生产中汲取的教训。
HTTP 是著名的无国籍。 您的应用程序... 并不是 。 用户签名, 将项目添加到篮子里, 在页面间跳动, 明天返回, 并期望您记住。 在 ASP. NET Core ( MVC, Razor Pages, Minimal 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"]
Midroware 示例( 所有堆叠) :
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
MVC/Razor Pages中的PRG模式:
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 Pages使用方式相同 HttpContext.
关于完整性/保密性,请使用 ASP.NET 核心数据保护系统 来保护自己放置在饼干中的有效载荷。
设置( 方案. cs) :
builder.Services.AddControllersWithViews().AddSessionStateTempDataProvider(); // optional
builder.Services.AddSession();
var app = builder.Build();
app.UseSession();
MVC 控制器中:
TempData["StatusMessage"] = "Saved!";
return RedirectToAction("Index");
在 Razor 页面处理器中 :
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;
}
我曾经在英国政府的一个大型IT项目上工作过, 在这个项目中, 国家滥用(以及其他许多建筑罪)成了一个能杀死性能的瓶颈。 一切一切 用户偏好、多步骤格式数据、搜索结果、临时计算,甚至本应该放在适当缓存或数据库中的缓存查询。
问题: 会话状态被存储在处理中( ASP. NET 会话状态在 Web. config 中, 这是前科日 ) 。 每一项请求都必须解密大型会话对象 。 随着负荷的增加, 会话状态会向每个用户的数十兆字节膨胀。 使用数以千计同时使用的用户, 服务器将失去记忆 。
绝望的解决方案: 我们飞到位于斯图加特的惠普公司设施, 欧洲最强大的Windows机器。 它是一个野兽: 几十个处理器, 数百千兆字节的内存。 其想法是证明如果有足够的硬件, 系统可以满足要求 。
结果: 即使在超顶上,我们也无法击中要求的同步用户目标。 会话状态结构被从根本上破坏了。 垂直缩放无法拯救糟糕的设计。 会话的序列化/降空管理,加上来自大型会话物体的记忆压力,意味着系统根本无法规模化 — — 没有任何合理的成本。
安全恶梦: 比性能问题更糟糕的是,我们发现了一个编码错误,导致届会状态 用户间漏出用户 A 会话数据会偶尔出现在用户 B 的会话中。 这不仅仅是尴尬-这是灾难性的。 NHS 工作人员 我们无意中创建了一个数据保护违反机制,可以将敏感医疗信息暴露于不同的保健专业人员会议之中。
本来应该发生什么:
教训: 会话状态不是垂直的,也不是横向的(即使有粘合会话或分布式商店,你仍然在按每个请求进行序列化/分层化 ) 。 超市实验证明,向建筑问题投掷硬件成本昂贵,而且往往徒劳无益。
但更重要的是: 状态错误成为安全弱点在医疗(或银行,或任何受监管的行业)中,这是一种守法恶梦和潜在的刑事责任。
现代咨询:
记忆缓存 :
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");
});
Cache-as-state 反模式警告:如果必须是耐久的或权威的,应将其储存在数据库中,并随意将其储存起来。
缓存( 最常见的) :
读取(通过图书馆/提供者读取):缓存处理失事装载。
写入: 写入缓存并同步支持商店 。
书写后: 写入缓存, 冲去不同步存储( 风险: 损失/ 不一致 ) 。
刷新前:在热键过期前刷新热键,以避免冷落。
当您需要废除许多相关条目时 :
版本的前缀( 软无效) : 在小键中折叠一个全局版本, 并以此组成密钥 。
// 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/Sub 无效: 发布“ 无效: key” 消息; 每个节点从本地 IMemoryCache 中删除密钥 。
使用模式扫描:在预测热路中应当避免SCAN/KEYS;小密钥空间的管理工具可以使用。
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 , 经过试验和错误的演化。我会给你们展示三种从简单到复杂的模式。
博客分类不会经常改变, 所以在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;
}
为何如此成功:
抓住了,我打中了: 最初我使用了字符串密钥“ 目录 ” 。 直到您有多个控制器, 一个不小心再使用相同的密钥。 现在我使用常数或强型密钥( 见先前关于避免碰撞的部分 ) 。
它会缓存每个用户的翻译状态。 它更复杂, 因为它需要界限, 只要用户在活动, 它就应该活下来 :
// 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个 。
权衡: 这不会波及数百万用户。 如果这成为一个问题, 我会移到 IdispultedCache (Redis) , 或者存储在数据库中, 在 userId + startTime 上输入索引 。
使用 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的量度非常重,没有太多变化,所以一个小时是好的。我通过查看SerlogTtravel数据发现这一点,并且看到过量的缓存误差。
监测行动: 在 Seq (我的对数聚合器) 中, 我可以查询 :
ActivityName = "GetMetricsWithPrefix" and CacheHit = false
这说明我的缓存误差率,如果高的话,我调整TTL或关键策略
|---------|----------|------------|--------------|---------------| | 类别类别类别类别类别类别 Global, 鲜有变化 30分钟绝对 不需要(小) 基本伐木 | 翻译任务 《手册》(5项最大) *无(应该加上的) * * * * * * * * * 无 * (应该加上的) * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *、 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | 计量数 * 昂贵的外部电话 * * 1 h 绝对 * 自然(时间窗口) * * 全面追踪 *
关键经验教训:
当我不使用 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/最小数):
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");
});
我在我的博客上同时使用应答缓存和输出缓存,
ASP.NET核心 二 类似外观的缓存系统:
Cache-Control, Vary告诉浏览器和 CDN 如何缓存
它们相互补充。 反应缓存处理客户端/ CDN 缓存; 产出缓存处理服务器端缓存。
// 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分钟)。同一用户随后提出的请求甚至不会击中服务器。
CDN 缓存 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 提供英文缓存版本。
使用 VaryBy 查询密钥 : 独立的缓存条目 :
/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
输出缓存被跳过, 原因是 :
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% 博客文章(大多数交通流量多次点击相同的流行文章),
影响:
有时候我调试了 每次都需要新的反应
// 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 正在生产。
专业:
关节 :
当我跳过焦头烂额时:
我的裁决是: 对于一个大部分是静态内容和高读/写比率的博客来说,双叠加是一个巨大的胜利。 我不会在数据迅速变化的行政面板或仪表板上使用它。
这三个问题经常被混淆起来。这里是它们的不同之处, 以及我在生产中实际使用的东西。
// 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");
| --------- | ---------- | --------- | ---------- |
|---|---|---|---|
| 使用寿命 * 现请求 * * 现请求 * 改头换面 * | |||
| 密钥访问 字符串键 * 属性语法 * 字符串键 * * 字符串键 * | |||
| 类型安全 * 无(需要播报) * 无(动态) * 无(需要播报) * * 无(需要播报) * | |||
| 汇编时间检查 不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不。 | |||
| 幸存者调整 不,不,不,不,是,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不,不。 |
将数据从控制器传送到共享布局(分析、分类等):
// 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);
}
标准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:
[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)得到: 默认情况下由 cookie 支持( 自 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");
Razor 视图 :
@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.*.他们都拥有在这里讨论过的相同的州机制。
在展示了所有这些选择之后, 以下是我对我的博客平台的作品的诚实评估。
使用案件 满足 满足 满足 满足 满足 |-----------|-----------|-----------|--------------| | 记忆缓存 高度的类别、标准、翻译任务 | 产出缓存 高调的博客文章, 列出 | 反应缓存 高端 HTTP 缓冲信头 与 Outtacache 合作 | 视图包 中度分析设置, 页面标题 * OK 对于简单的东西 * * * OK for simple stuff * | 授权请求书 中度 用户身份 行政旗帜 正确工具 | 路线/查询 中度 | 数据库数据库数据库数据库 中度部落格文章、评论、状态、真相来源 | 曲曲曲曲曲曲曲曲曲曲曲曲曲 低用户偏好(未来) 尚未需要 | 届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议届会议 绝不 - - - - - - - -没有分销店的布置 | 临时数据 永不 - 'HTMX消除需求' | Httpcontext. 项目 永远不要 - - 还没有使用 案例 | 分布缓存 永远不要 - - \ \ \ \ \ \ \ \ \ \ 单个服务器 (目前) \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \
我用它来做什么:
实践证明:
我击中了控制杆:
缩放限制 : 如果我能找到多个服务器, 我需要 I dispreditedCache (Redi) 共享状态 。 目前, 单个服务器 + 内存缓存是完美的 。
我用它来做什么:
实践证明:
我击中了控制杆:
最佳用于: 大多为静态内容的重读应用程序。 不适合个人化或实时数据 。
我用它来做什么:
实践证明:
我击中了控制杆:
规则一如下: 仅用于简单标量的 ViewBag 。 复杂的对象在 ViewModels 中进行 。
我用它来做什么:
sub (a) 针对 config的索赔)实践证明:
User.Claims 各地我击中了控制杆:
最佳做法: 索赔要求保持最低程度和稳定。不要在索赔中提出经常变化的数据。
会话( 从未使用过 ) :
TempData( 从未使用过) :
HttpContext. 项目( 从未使用过 ) :
Idisp分配缓存( 从未使用过) :
第1阶段(初始): 完全没有堵塞,每个请求都击中数据库 并设定了马克唐,因为交通流量低所以罚款。
第2阶段(第一次优化): 添加了分类的 I MemoryCache 。 立即看到 DB 减载。 简单说来: 30 分钟 TTL, 没有花哨逻辑 。
第3阶段(扩大规模): 交通激增时添加了博客文章的 输出缓存 。 大规模性能改进 。 初始错误 : 只缓存5 分钟 。 监控显示内容后增加到 1 小时 很少变化 。
第4阶段(可观察性): 将 Serilog 跟踪添加到度量缓存中 。 由于密钥格式化日期, 发现缓存缺失率很高 。 固定密钥格式 yyyyMMdd 点击率从60%上升到95%。
第5阶段(HTMX一体化): 已添加 VaryByHeader 用于2004年和 hx-requestHTMX 请求时, 最初忘记了这个内容, 并提供了完整的页面。 直到我发现为止, 调试恶梦 。
当前状态 : 满意的堆叠 。 I memoryCache + extraCache + reactionCache 处理98%的我的国家管理需求。 数据库用于耐久状态。 验证身份。 仅此而已 。
从这里开始 :
必要时添加: 6. 会话( 只有当您必须拥有服务器- 端对话状态时) 7. 分配缓存(仅在您向多个服务器缩放时) 8. 饼干(用于客户方偏好、同意)
避免:
关键经验教训:
状态在网络应用程序中并非一刀切。 选择满足您需要的最轻的选择, 选择可以选择的无国籍模式, 并明确安全性和生命周期。
如果你想深入了解这些碎片如何通过管道 看我的系列从 第1部分:概述和基础 特别是中间器械和路由部件。
快乐的建筑。
这些例子加深了前几节内容,可以粘贴到净9最低模板、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 密钥 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();
仅存储小的、可压缩的数据。 固态真实的手推车/ 订单到 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);
});
使用StackExchange. Redis(SET NX EX),
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();
});
这应该弥补差距:更强大的安全违约、多节准备状态以及缓存、象征性物品和有条件请求的现实世界模式。
HtpContext 。 项目是每个请求的包( IDctionary <object, object? > ) , 仅存于一个请求的一生。 它完美地将计算值从中器/过滤器传递到您的端点、 控制器和 Razor Page 处理器, 而不触及全球状态或长寿命仓库 。
因为项目使用对象键, 更喜欢私有静态对象键或专用密钥类型, 以避免名称碰撞 。
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) });
使用物品作为小缓存, 重复阅读相同请求中的内容, 不要重新点燃数据库/服务 。
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.