Back to "在 ASP .NET CoreMSSK1一个实用的,No‑nessense 指南\ MS K4MVC},Razor Pages, Minimal APIS)"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

AI-Article ASP.NET ASP.NET Core State Web Development

在 ASP .NET CoreMSSK1一个实用的,No‑nessense 指南\ MS K4MVC},Razor Pages, Minimal APIS)

Sunday, 09 November 2025

**更新@(2025-11-10) @%**从我的实际代码库中添加了更多实际例子, 显示真实的-@world stands @,#Trade_-}posts,}(从简单到精密的国家管理方法的演化). 包括详细的 IMemoryCache type={,ViewBag use}MSKO8}好的和坏的MS K9反应卡奇}/OutputCache 战略>, 以及从制作中汲取的教训〞.}

一. 导言导言 导言 导言 导言

HTTP 是著名的无国籍@. @ 您的APPMSMK1} 不是一个在 @ ,} 在 pages@ ,明天返回,之间添加项目到篮子@ ,跳跃中, 期待您记住 @ MPK7 在 ASPQK8NET Core {(MVCK10Razor Pages,MSK11_ MSSKZQMS K12在请求中有许多保存和传输状态的方法 *%.}有些只满足了 <-> 要求} 一些住在客户端 一些人被分配并幸存服务器重开关 每个选项都有交易}-% 跨安全区=,} 性能 MSKO21Sandreaster er ergronomic}

此日志列表中的选项@, @ 显示混凝土 ,} 复制 k2 postable 示例, 在全部三个堆叠中都提供决定指导, 这样您就可以为工作选择正确的工具@ MS K4

这是我自己编辑的A )Q MS K3Q 同一声音 同样的实用性 ,Q 一样

风景一看

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
  • 客戶端 - @carried@:}查詢,}路由MS K3}标题},}表單_,cookie{,}JWT.}按水平排列,但客户端可看到 <%(}%必须檢驗MSKO9signate安字
  • 伺服器@ - @ carried}:TemalData,会话,缓存{,DB.需要近似或分布策略}MS K6
  • Per-request: HttpContextMS K2 項目{-} 有用於在一次請求中傳遞內部資料}.

在我们潜入APIS=)}之前 金色规则 @ ( @

  1. 国家渗漏是一个真正的威胁 伺服器@- @side state @( @saynology_,}在-{memory 暗藏}{)可以通过編碼錯誤(,}) 種族環境, 或不正確的生命周期管理來在使用者之間漏出 *MSK6}(Nationalistic Statusion) <'}♪没有更多可伸缩性(MS K9)♫sire'.}
  2. 如果您需要水平缩放“ (” push state 状态到客户标语@ MS K3} 耐久商店 @ , 或分布式缓存=).
  3. 永远不要信任客戶端@- @ 控制状态 @ .} 驗證@ MS K2} 簽署 *,} 與{/or encrypt}.}
  4. 将大数据保存在饼干和信头中@; @ they bloat every request .}他们每次请求都会浮出水面
  5. 仅对 Post- @ recent@ -}获取 < MS K2 PRG )\ one\ -\ shots like flash message\ MASK5> 只使用 TempData 仅用于 postMSSK0_\ redirect\ MPK1 _ Get *(\
  6. 只当您必须维护服务器@ - @ side contactalal 状态且计划发布\ —}并且对安全有偏执性时使用会话@ MPK3
  7. 索赔要求是身份和粗体-+Grained Agency App state=.}不是普通应用程序
  8. 如果数据重要的话, Cache 不是真理的来源@ . @ 以耐久的存储方式返回它

请求状态@: @HttpContext_.}项目

  • 范围@:}当前请求仅@MS K1 @ teardown 在管道末端 @)
  • 用于 @ : 传递中间软件和端点之间的计算值@ MS K1\ 控制器
  • 缩放:无影响
  • 安全@:伺服器 @- @仅%
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"]

中继器例 @ (}所有堆叠@ MS K1

app.Use(async (context, next) =>
{
    var tenantId = context.Request.Headers["X-TenantId"].FirstOrDefault() ?? "public";
    context.Items["TenantId"] = tenantId;
    await next(context);
});
  • 最小 API 端点@: @%
app.MapGet("/whoami", (HttpContext ctx) => new { Tenant = ctx.Items["TenantId"] });
  • MVC 主计长
public IActionResult WhoAmI() => Json(new { Tenant = HttpContext.Items["TenantId"] });
  • Razor 页面管理器 @:
public IActionResult OnGet() => new JsonResult(new { Tenant = HttpContext.Items["TenantId"] });

透明客户端@- @Carried state *:}路线价值和查询字符串

  • 范围@:当前请求 @;客户端明确携带它.
  • 用于 @ : @ 导航背景@ MS K1 @ 过滤 @ MPK2 @ pagenation_ , 资源身份
  • 安全性:必须验证 /授权{;}不隐藏秘密

实例示例例子

  • 最小APIs:
app.MapGet("/orders/{id:int}", (int id, int? page) => Results.Ok(new { id, page }));
// GET /orders/5?page=2
  • MVC:
[HttpGet("/orders/{id:int}")]
public IActionResult Details(int id, int? page)
  => View(new { id, page });
  • Razor Pages @(_Orders}/#Deltats .MS K3Cs%):}(Razor pages MSK 0 Orders}MSk1}DealtiesMSK 2}(MSCShtml}MSShtml>.}(MSK)}[MSK]
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)

页眉: 相關的代碼MS K1 指定密钥, 特效旗

  • 范围:当前请求,在答复中可选择性回覆
  • 用于 @ :追踪@ MS K1重新 try-# safety _,A/B 旗
  • security : 被当作不信任的输入@,}验证 */whitelist
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);
});

应急能力: @(_patten}):#

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

  • 范围@: 下次只请求 (}客户端日志返回值 @)
  • 用于 @ : 向导阶梯@ MS K1\ again @ I-_ 伪造标牌@ MPK3 在整个 PRG 中保留小部分状态
  • 安全性=:}总能验证 ; 结合抗伪

MVC MSMK0Razor 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)
  • MVC:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Save(SettingsModel model)
{
    // validate & persist
    return RedirectToAction(nameof(Summary), new { tab = model.SelectedTab });
}
  • Razor Pages @:
public IActionResult OnPost(SettingsModel model)
{
    return RedirectToPage("/Settings/Summary", new { tab = model.SelectedTab });
}

Kookies: SmallMSMK1 签名, 有时加密

  • 范围@:浏览器在到期前的每次请求
  • 使用“:”首选项@, @ non-}敏感旗子 @,}(同意) *; auth cookies {(}%spartate section_)}
  • 贸易量=%:}大小限制(4 @KB 每块饼干),}性能影响=,}必须遵守同意法

最小 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核心数据保护系统 来保护自己装在饼干里的有效载荷


Temta=: One - _ redirect message Bus

  • 范围=: 维持一个方向
  • 正在备份仓库@ : @ cookie @ MS K1 默认@ mafault @ )} 或会话
  • 用于 pRG 之后的“:” 闪电消息@,}校验摘要

设置 “(” 方案“.” “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");

在“/page:”的视图中

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

会话:伺服器{-} <Side conversational 状态>

  • Servior :浏览器会话@(cookie 密钥 @+服务器存储})
  • 用于 @ : @ mounti@ MS K1 @ step 向导 @ MPK2 @ 小马车数据@ I, @ roottling 计数器
  • Trade-M: 需要粘性会话或分布式支持存储@; 可以限制规模

配置@: @

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

使用会话 @ ( @ any stack@ MS K1 @

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项目上工作过, 在这个项目中,会场国家滥用 (ammong许多其他建筑 ) 成了一个表演\MS K2k killing clockneckm. 一切一切 正在进入会话@: @ 用户偏好 @ , @ motion@ MS K2_ step form data @ MPK3} 搜尋結果@ I, 临时计算@ MOSK5} 甚至缓存的查勘, 这些查勘本应在适当的快取或数据库中@ MASK6}

问题是什么? 会话状态已存储在 @ -_ process {(} _MSK1ASP.NET会话状态中, 在 Web} <%K3 config},这是预留的<-Coledays).> 每份请求都必须解密大型会话对象@.随着负载增加,#,}(会话)将气球升至每家用户数万兆字节=.}有数千个同时使用者*,}...

绝望的解决方案 我们飞到斯图加特的HP's设施 测试他们的超人荷载 欧洲最强大的Windows机器这是一只野兽 : 几十个Itanium处理器 MS K2 数百千兆字节的RAM. 想法是证明 如果有足够的硬件

结果 #:\ 即使在超多点@,中, 我们也能在超级会话主机上击败所需的同时用户目标 @.。 会话状态架构被彻底破坏\ .垂直缩放可以省下错误的设计_'t

安全噩梦 我们发现一个编码错误 导致会话状态 用户间漏出@. @% 用户 A'} 会话数据偶尔会出现在使用者 BMS K2_s 会话@ . @ 这是 @ ' @ t只是让人尴尬\ — @ it was disaused NHS 工作人员 我们无意间创建了一个数据保护违约机制 它可以暴露不同医疗专业人士的敏感医学信息

本该发生什么了?

  1. 默认无国籍此会话的大部分数据本不该存在
  2. 耐久状态数据库: multi-pregy from proform 本该在数据库中输入一个工作流程编号
  3. 查找缓存快取: 共享搜索属于 IMemoryCache 或者分布的缓存
  4. 客户端“-”附加首选项@:_ 用户首选项可能是在 cookie 或者本地存储中
  5. 必要时分发会议@ : @ {如果真的需要会话} @ I,_ redisi- 支持的会话将会共享负载@ action

课程#: 工作階段的状态是“'t 垂直和几乎不水平 ” @(}(即使有粘贴会话或分布式商店)@,你're 仍然按每个请求序列="/ZZZAREATIONA}). 超市实验证明,在建筑问题上投掷硬件费用昂贵而且往往徒劳无益_.

但更重要的是, 状态错误成为安全弱点@,#@.}在医疗保健方面, 他们可以泄露敏感数据给用户@MS K8_or bankSK9或任何受监管行业@),}这是个遵纪守法的噩梦和潜在的刑事责任吗?

现代建议@: @%

  • 如果您发现自己需要超过数个会话数据KB@ , @ you MS K1}您可能遇到设计问题
  • 如果您在会话中存储敏感数据 , 您正在创建安全攻击表面
  • 僵尸建筑本身更安全, 因为那里没有服务器@-#side state
  • 在你需要飞到斯图加特之前 重新思考你的州管理策略 @(@or

Caching:I Memory缓存和分布缓存

  • 范围@:服务器进程 @ (IMemoryCache_ MS K2}或已分发的 @(IDistradtecacheQ)
  • 用于 < : produced\ MS K1 computed data _ , > lookups ,\ short\ - live status >
  • 批发的批量序列化

记忆卡切 :

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

分发快递 @( @e. @g}.,_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");
});

如果它必须是耐久的或权威的 ,将它存储在数据库中,并且可以选择将其缓存

I MemoryCache 和I 分布缓存之间的选择

  • 记忆卡切 :
    • 在 - process , @ 对象停留为天体@ MS K3_no 序列化@ MPK4}
    • 使用内存压力进行切除@,大小限制 @,绝对Q}/sliding 到期 *,和优先级 @MS K4
    • 在程序回收中清除了未共享的节点@ ; @
    • 计算查询@, @ 简短的 TTLs.}
  • 分布式缓存 @(_Ridis/}SQL#/.):}
    • 农场对面共享@; 继续使用应用程序重新启动 ; 要求序列化 @(strings}{/Bittes).
    • 稍高一点的延迟度@ ; @ 输送量取决于网络和后端@ MPK1
    • 支持绝对Q/sliding 到期@(preater @-依赖};Redis 提供方更新滑动访问的 TTL
    • 跨-node 一致性的首选项@ ,}大粉丝 @ MS K2 朗读@ MPK3 特写旗, 和会话.

共同暗藏战略

  • 最常见的 ):

    1. 尝试缓存@ ; @ MS K1 @ 如果从源代码 <% k3 }\ 3)\ 写到缓存{ ; * _ MSC6} 返回.}
    • 真相来源仍是数据库@.
    • 过期后的第一个请求是慢@;}可能的印章 @.
  • @(_via a library\ /prover@):}缓存会处理 missQ.的装载事宜

  • 写作:-nout: 写到缓存和备份商店同步@.

  • 写作:- @ behind@:}写到缓存@MS K2# flace 以不同步的方式存储 ( @risk_: @ loss @/{前后不一致}).}

  • 刷新-ahead:在过期前刷新热键以避免感冒@.

驱逐=,和缩放

  • 绝对到期@: @% 总是在固定期限过后过期 @ (# @ good for 外部数据更新@ MS K2} @
  • 滑动过期@: @% 在会话访问端扩展 TTL @ (# good
  • 大小@ - @ 基于基调的驱逐 @ MS K1 @ IMemoryCache @ MPK2} 设置了条目\ . @ size and 配置大小缩放到绑定内存@ .
  • 优先关注的“(IMEMORYCACH):缓存项目重点” @.High/NormalMS K4Low}/永远不要在压力下影响驱逐_.
  • Jitter: 添加小随机偏移到 TTL 中以避免同步过期@ ( stampedes @ MS K2}

防止缓存盖章 @(BARBURING)#

  • 使用 GetOrCreate/Get OrCryateAsync (IMemorychache})以确保每个节点的单数{->全线=%.}
  • 分发:使用一个短-活锁按键@ (SET NX EXMS K3或库支持@;添加 TTL jitter};考虑背景刷新 @.
  • 服务 stale@ - @ while @ - @ revalilidate @ MS K2} 在计算新值时, 保留一个带有粘贴值和短延的次要密钥\ .}

按键设计和命名间隔

  • 更低的大小写@ , @ 冒号 @ MS K1} 限制键 @ I: @ app:_ entity @ MPK4} 或租户@ MOSK5 @ usQZK6 @ uses @ MASK7}
  • 包含版本段以取消整个密钥分类, 不删除@ @ : @ v2:} products@ MS K2}
  • 租户或组织id 前缀密钥,以避免碰撞和缓解清洗
  • 保留小键但描述性@ ; 避免用户 @ MS K1 控制的原始输入, 没有正则@ MPK2

到期的密钥集 @ ( @ taggs@ MS K1}组=)#

当您需要取消许多相关条目时@ : @

  • 版本的前缀@( @ 软无效@ ):} 在小密钥中跳过一个全局版本, 用它来拼写密钥@ I.}

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

    取消所有产品@: @ 递增 v:} products {(} 客户将自然丢失旧的前缀密钥@ ).}

  • 每个组的标签设置 #% ( redis):} 保留每个标记的一组密钥@ ; 无效@ MS K3 fack members and delete @ .}

    // 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取消@: 发布一个“ "invalidate” key MS K4 message};每个節點從本地的 IMemoryCache}.}移除密钥

  • 用图案扫描@ : SCAN/KEYS, 在标注热路中应该避免@;可以使用小密钥空间的管理员工具 @.

实践帮助者

  • ImemoryCache 获得“ - @ or @ MS K1_ set” 选项@ : @ {
    T GetOrAdd<T>(IMemoryCache cache, string key, Func<ICacheEntry, T> factory)
      => cache.GetOrCreate(key, e =>
      {
          e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
          e.SlidingExpiration = TimeSpan.FromMinutes(2);
          e.Priority = CacheItemPriority.Normal;
          e.Size = 1;
          return factory(e);
      });
    
  • 与 JSON 和过期分配缓存@: @%
    static async Task<T?> GetOrSetJsonAsync<T>(IDistributedCache cache, string key, Func<Task<T>> factory, TimeSpan ttl)
    {
        var json = await cache.GetStringAsync(key);
        if (json is not null)
            return System.Text.Json.JsonSerializer.Deserialize<T>(json);
    
        var value = await factory();
        var opts = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = ttl };
        await cache.SetStringAsync(key,
            System.Text.Json.JsonSerializer.Serialize(value),
            opts);
        return value;
    }
    

监测和可见度

  • 音轨赛跑速率和平均负载时间@;QQ 暴露了每关键组的度量 {(} prometheus counts @)Q}
  • 加上在暗藏地人口周围的伐木和为IMemoryCache@.的驱逐回召
  • 对于 Redis, 观看密钥空格点击@ / misses @ MS K2 lantency @ MPK3 和内存破碎@ I; 酌情设定最大缩略政策@ OMK5

世界生产中的记忆缓存模式

@I'Q}将展示三种真实模式, 从简单到精密的#. 。

“1:”模式简单缓存“-” 选项

这是我第一次在博客分类中进行切换@'%t 经常更改 @,%s, 所以它们会缓存到 & 30\ minutes @MS K4}

// From BaseController.cs
private const string CacheKey = "Categories";

private async Task<List<string>> GetCategories()
{
    baseControllerService.MemoryCache.TryGetValue(CacheKey, out var value);

    if (value is List<string> categories) return categories;

    logger.LogInformation("Fetching categories from BlogService");
    categories = (await BlogViewService.GetCategories(true)).OrderBy(x => x).ToList();
    baseControllerService.MemoryCache.Set(CacheKey, categories, TimeSpan.FromMinutes(30));
    return categories;
}

为何如此成功?

  • 在导航中 ) {每页都读取分类}(_shown
  • 他们很少会更改 @(@ 只有当我加入新的博客文章,
  • @30-_ minute TTL is fine@;}如果出现新的分类,#,}用户在 @MS K3\ 分钟内看到它们
  • 只有% ( @ no plipple@ MS K1 @ 因为我们不关心它访问的次数

抓到你了 我打中了: 最初我使用了一个字符串密钥“" @Categories".}工作顺利,直到您有多个控制器并有一个不小心重新使用相同的密钥‘.’为止。 现在我使用常数或坚固的“MS K3”键@(* 见先前关于避免碰撞的章节<).>

@2:_Per-}用户状态,有大小限制和滑动失效 {(\Q}翻译任务=)}

此缓存状态为每个用户@ . @ it'} 更复杂, 因为它需要边框并且只要用户是活跃的% MS K2 就会活下来

// From TranslateCacheService.cs - tracks translation tasks per user
public void AddTask(string userId, TranslateTask task)
{
    CachedTasks CachedTasks() => new()
    {
        Tasks = new List<TranslateTask> { task },
        AbsoluteExpiration = DateTime.Now.AddHours(6)
    };

    if (memoryCache.TryGetValue(userId, out CachedTasks? tasks))
    {
        tasks ??= CachedTasks();
        var currentTasks = tasks.Tasks;

        // Keep only the 5 most recent tasks
        currentTasks = currentTasks.OrderByDescending(x => x.StartTime).ToList();
        if (currentTasks.Count >= 5)
        {
            var lastTask = currentTasks.Last();
            currentTasks.Remove(lastTask);
        }

        currentTasks.Add(task);
        currentTasks = currentTasks.OrderByDescending(x => x.StartTime).ToList();
        tasks.Tasks = currentTasks;

        memoryCache.Set(userId, tasks, new MemoryCacheEntryOptions
        {
            AbsoluteExpiration = tasks.AbsoluteExpiration,
            SlidingExpiration = TimeSpan.FromHours(1) // Extends on access
        });
    }
    else
    {
        var absoluteExpiration = DateTime.Now.AddHours(6);
        var cachedTasks = CachedTasks();
        memoryCache.Set(userId, cachedTasks, new MemoryCacheEntryOptions
        {
            AbsoluteExpiration = absoluteExpiration,
            SlidingExpiration = TimeSpan.FromHours(1)
        });
    }
}

为什么这不一样?

  • 流滑过期如果用户不断检查翻译状态 @,}让缓存活到 6 最多小时
  • 大小限制: 仅保留 @5 每个用户的最新任务, 以防止内存浮肿
  • 手工管理@ : @ I 明确限制大小, 因为内存缓冲( doesn't memoryCache) '} t强制执行条目 MS K2}# 级别项目计数限制 {(} 仅限制总体快取大小@ )

我犯错了,我犯了错 起初,我并没有限制任务数量@.}一个电源用户触发了 @50+}翻译 我有一个内存漏...

**贸易 ** 若出现问题, 则将其移动到 IdispreadCache @ ( @ Redis)}或存储在数据库中, 在用户Idd@+_ startTime#.}上保存索引

@3: 缓存模式 : @ ( 仪表正在缓存@ MSC2

使用 Serilog 追踪 < :\ }来缓存分析度量和跟踪缓存效果@ msk0

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

为何如此制作此作品?

  • 可观察性:SerrilogTtract活动跟踪暗藏缓存 hit*/}失踪率和记录量计
  • 复合密钥: 包括日期范围和前缀以避免关键碰撞
  • 日期格式化:用途 yyyyMMdd 共享缓存
  • 纸质处理若外部服务失败, 则返回无效@ ; @ does ' @ t缓存失败
  • 过滤的缓存: 缓存过滤结果@, 而非原始响应

进化: 最初我为 @10 @ minutes @ . @ 但是Umami 度量仪太重了, 要取并换很多#'_ t change most@,}所以“ MS K4}小时是好的 我通过查看SerilogTtract数据 和看到过多的缓存误差.> 发现了这一点

监测行动 在 Seq 中 @ ( @ my log agregatator@),}我可以查询\ MS K2}

ActivityName = "GetMetricsWithPrefix" and CacheHit = false

这告诉我我的缓存错误速率@. @ 如果它高的话#'}我调整TTL或关键策略=.}

比较这三种办法

大小控制 * 观察性 MS K5
类别类别类别 @ _Global=,}(很少)-_Zanging { } <30} 最短的绝对=@ }不需要吗?
翻译任务 @ @per@- @%user#,捆绑的 @MS K3}{6_h 绝对的*+(1QH)
计量数 @ @ 昂贵的外部呼叫 @ @ @ I1 @ h绝对 @ MPK3 @ 自然: MS K4_time @ MASK5 @ windowed# ) @ * @ 完全追踪 # @ @

关键课程@: @%

  1. 仅在需要时才添加复杂性
  2. 总是想着缓存内存使用@ - @ 添加 per-}用户缓存的大小限制
  3. 对于昂贵的操作来说, 添加从第一天开始的可观察性
  4. 依据实际数据变化频率调整 TTL@, @% 没有猜测

当我不使用 ImemoryCache : 时

  • 对于用户认证状态 {(} <% 1 xB1\fn方正黑体简体\fs18\b1\bord1\shad1\3cH2F2F3F}*@)}BARBAR
  • 用于购物车“(”将使用 DB “+”在生产中分布的缓存@)
  • 对于博客文章内容, @( already in DBQ,}只要提出要求就装入一次 {)}
  • Cross-Server state {(需要我分布的卡切}/Redis)

Cooks和索赔

  • 范围@:}直到过期为止的所有请求@ / @ sign=-_out
  • 用于 @ : @ 身份@ MS K1 @ 粗糙角色 @ I/_ permissions @ MPK3} 少量的配置数据
  • 交易量=%@:_cookie size @;_don’_tovertuff要求应稳定(.})

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

签名:-in,索赔号: @(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("/");
});

读取索赔 * @(_any stack* _):

[Authorize]
app.MapGet("/me", (ClaimsPrincipal user)
  => Results.Ok(new { user.Identity!.Name, Roles = user.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value) }));

JWT - / - 贝尔托肯斯

  • Service : 客戶端包含數據@; 无国籍伺服器
  • 用于@: @SPASQ/}(移动) @/_APIsMS K3}横贯{-}%domain,微服务
  • 交易量=-offs:象征性大小 ; 旋转_/rerefirst};存储最低要求量,必要时使用内部调查
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

数据库和朋友

  • 直到你删除它为止 )
  • 使用任何不可丢失的命令@ : @ mass @ MS K2 @ 命令 @ MPK3} profile *, long_ - @ 运行中的工作流程
  • 标准CRUD与EF Core@;}CQRS;事件

EF 核心草图

builder.Services.AddDbContext<AppDb>(o => o.UseSqlServer(cs));

app.MapPost("/cart/items", async (AppDb db, AddItem cmd) =>
{
    var cart = await db.Carts.FindAsync(cmd.CartId) ?? new Cart(cmd.CartId);
    cart.Add(cmd.ProductId, cmd.Qty);
    await db.SaveChangesAsync();
    return Results.Created($"/cart/{cart.Id}", cart);
});

回应 Caching @,ETags,和有条件请求

  • 不是严格意义上的““state commission” ,而是通过让客户端@/proxies 重新使用先前的回应来减少重复性工作 @. 通常配对到查询{/route state_.}
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();

TATA 示例*:

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

反应缓存对产出的缓冲@: @ real @-}世界生产使用量

我在我的博客上同时使用反应缓存和输出缓存,

混杂的: 两个缓存属性 ? *

ASP=.NET核心 二 类似@- @ 外观缓冲系统 @ : @

  1. **反应缓存 @(#HTTP caching@)**设置 HTTP 信头@( @Cache-Control, Vary) 告诉浏览器和 CDN 如何缓存
  2. 输出缓存 ( @server#-}%side@): 将服务器的输出缩放为跳过完全执行动作

它们相互补充 互为补充 响应缓存处理客户端@ / @ CDN caching; @ 输出缓冲处理服务器 @ MS K2 @ spide chaching @ MPK3}

我的博客文章行动@(}两个缓存都应用了 @)

// From BlogController.cs
[Route("{slug}")]
[HttpGet]
[ResponseCache(Duration = 300, VaryByHeader = "hx-request",
    VaryByQueryKeys = new[] { nameof(slug), nameof(language) },
    Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 3600, VaryByHeaderNames = new[] { "hx-request" },
    VaryByQueryKeys = new[] { nameof(slug), nameof(language) })]
public async Task<IActionResult> Show(string slug, string language = "en")
{
    var post = await blogViewService.GetPost(slug, language);
    if (post == null) return NotFound();

    // ... populate user info, comments, etc ...

    if (Request.IsHtmx()) return PartialView("_PostPartial", post);
    return View("Post", post);
}

当有人提出要求时会发生什么情况 /blog/my-post:

  1. 先检查产出缓存: 是否我有一个缓存回复 my-post + en 语言@?

    • 命中@ :_ 返回缓存的 HTML@ MS K1} 操作方法从不运行 *\ (fast!} @ MPK4ms @ I)}
    • 中误出@ :_ 执行动作@ MS K1} 生成“ ,Q” 的缓存结果 : @ I3600Q 秒 * (1Q hourZMK5
  2. 反应缓存设置信头@:在输出缓存生成响应后@,回应缓冲添加#:

    Cache-Control: public, max-age=300
    Vary: hx-request
    
  3. 浏览器缓存:浏览器会缓存 @300秒@(5minutes @MS K3}来自同一用户的接连请求,

  4. CDN 缓存 .}世界范围内的用户攻击了 CDN=MPK5}不是我的服务器=.}

为何不同时间段不同?

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

原因#:

  • 服务器缓存时间较长 @ (1 @ hour@ MS K1}@ :I control my server @ MPK1如果我更新一个邮件, 我可以清除缓存@ action
  • 客户端缓存更短@(5 @ minutes @ ) @:I can @'t purge 用户浏览器或 CDNs 很容易@; @\ MS K3分钟是一个合理的淡化窗口
  • 贸易@:_如果我编辑一个文章 ,}新的内容显示@:}
    • 伺服器@-}%side:}即刻 @(}I 可以取消缓存@MS K3}
    • CDN/browser:在 @5分钟@(or I手动清除 CDNA})

进化: 虽然内容很少改变@._Now @:}(但这意味着我的服务器是按每#- minutes

  • Server 愉快地服务缓存 HTML (@1+# hour)
  • 客户每分钟都有新内容@- @enough confession

HTMX 的 VaryBy 列头

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

为什么这重要? HTMX要求包括 hx-request: true 我回覆不同的回复@: @%

  • 全额请求: 完整 HTML 页及布局
  • HTMX 请求 部分视图, 没有布局

不无 VaryBy& ,}缓存将返回错误的格式@.}使用 VaryBy@, @I 缓存每页的两版 #.#

实例::

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

用于语言和页码的VaryBy VaryQueryKeys

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

没有此% 1 的问题@ : @ /blog/my-post?language=fr 提供英文缓存版本@. @%

与 VaryBy QueryKeys @: 单独的快取条目@: @%

  • /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 “→”另一个条目

缓解@:

  • 合理的最大缓存大小 @ ( @ ExputCache 自动@ MS K1 @ evicts least\ - @ recently @ MPK3_ usedQ)
  • 共同参数缓存@(}页面 @1,}默认页面Size)}
  • 不常见的组合可能会丢失缓存@ ( @ 接受 @ MS K1 @

需要设置

反应缓存 框中的工程“,”但用于 产出缓存 您需要设置@: @%

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

当输出缓存时@ ' @ t帮助

@ :} *% 1} 输出缓存被跳过

  • 已验证的请求@ ( @ 不同用户看到不同的数据\ )
  • POST/PUT/DELETE(只得到 /HEAD缓存})
  • 答复者的答复 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
}

衡量成效

我使用 Prometheus 度量@ ( @ explained 通过我的 App)}来追踪\ MS K2}

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

我的缓冲速率= :} @ ~98% @(_).}大多数交通堵塞同样流行的帖子,

影响#:

  • 不带 chaching: @ ~50ms 平均响应时间@ MS K2DB 查问 *+ 马克唐纳making\ )}
  • 使用输出缓存响应的 OuttCache @ : @ {~1-2}%s
  • 25x 加速

当我暂时禁用缓存时

有时 I'M调试 需要新的回应 每次@ :

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

或使用环境@- @ action

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

更好的方法#: 使用配置@: @%

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

何处 responseCacheDuration 正在开发中的“0” 在生产中 “.”

这项双双双战略的利弊

Pros:

  • 服务器效率: 输出缓存通过 @98%来减少 CPU@/DB 加载
  • 客户端@/CDN 福利:应答缓存会减少我的带宽,
  • 灵活性用于服务器和客户端的不同 TTL
  • 节省成本节约@: 更少 DB 查询@, 少带宽

:

  • 静态@:_#内容更新需要时间到@5_Q 分钟才能向客户传播。
  • 缓存无效复杂性: 更新一个邮政需要取消服务器和 CDN 缓存
  • 内存使用: 输出缓存持有服务器内存中的 HTML
  • 调试混淆@ : @ 有时忘了缓存, 想知道为什么更改@ MS K1 @ t

当 I'd 跳过 cachingMS K1

  • 现场运动=)
  • 个人化内容 @(user_-}具体建议@)
  • Low-@traffic pages @(_cagging oversubject@>BARBAR)}翻譯頁面
  • 经常改变的页面

我的判断是 @.@I would'}在数据迅速变化的行政面板或仪表板上使用此功能。


ViewBag,ViewDataMPK1和Tembata}:Ctor{-to -Vision State}(我为何大多避免其中两个})

这三种情况往往令人困惑: . @ Here'}它们有何不同,

三个朋友比较

// 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");
@ @ 地貌@ MS K1 @ viewData @ I 视图Bag @ MPK3 Temta @ MASK4
类型种类 ViewDataDictionary 动态的 ITempDataDictionary
使用寿命 @ _当前请求@ _目前请求 @# }一转方向 @MS K3{*
密钥访问 字符串键 @ 产权语法@ }字符串密钥{
类型安全 @ #没有 (casts需要\ MS K2 NSK3}(无)(_动力性@)}{ Q} (无)%(casons 不需要□)} }
汇编- @time check # @ 不,不, # 没有,MS K2# *没有,MSC3#
幸存者调整 # @ 不,不, # 没有,MS K2# 是 @ @

用于全局布局数据的 ViewBag

在我博客“,”中, 我使用ViewBag专用于将数据从控制器传送到共享布局@(Analytics @,}%K2 类{,}等.):*。

// From BaseController.cs - runs before every action
public override async Task OnActionExecutionAsync(ActionExecutingContext filterContext,
    ActionExecutionDelegate next)
{
    logger.LogInformation("OnActionExecutionAsync");

    if (!Request.IsHtmx())
    {
        // Analytics settings for layout
        ViewBag.UmamiPath = AnalyticsSettings.UmamiPath;
        ViewBag.UmamiWebsiteId = AnalyticsSettings.WebsiteId;
        ViewBag.UmamiScript = AnalyticsSettings.UmamiScript;
    }

    logger.LogInformation("Adding categories to viewbag");
    ViewBag.Categories = await GetCategories(); // Cached list

    await base.OnActionExecutionAsync(filterContext, next);
}

然后在我的布局中_Layout.cshtml):

@if (ViewBag.Categories is List<string> categories)
{
    <nav>
        @foreach (var cat in categories)
        {
            <a asp-controller="Blog" asp-action="Category" asp-route-category="@cat">@cat</a>
        }
    </nav>
}

@if (!string.IsNullOrEmpty(ViewBag.UmamiPath))
{
    <script async src="@ViewBag.UmamiScript"
            data-website-id="@ViewBag.UmamiWebsiteId"></script>
}

为何此模式有效 @ : @

  • 全球全球:}每页都需要分类 nav 和分析
  • 计算一次: 基地控制器在每次行动前运行
  • HTMX优化@ :_ 跳过部分请求的解析脚本@ MS K1_ HTMX does'} 不需要它 -\ jectedQ}
  • 缓存@ :_ 分类已缓存 *\ MS K1} 参见我的 IMemoryCache 模式@ ),, 所以不要按下 DB 每次请求

我在:上犯的错误 我当时在设置 ViewBag.Categories 在每一个动作方法中 .DRY违规,很容易忘记 OnActionExecutionAsync 在基底控制器中解析了此@ . @%

页面的视图Bag@-}特定数据 @(}可接受模式{)}

// From BlogController.cs
[Route("category/{category}")]
public async Task<IActionResult> Category(string category, int page = 1, int pageSize = 10)
{
    ViewBag.Category = category; // Used in view for heading
    ViewBag.Title = category + " - Blog"; // Used in layout <title>

    var posts = await blogViewService.GetPostsByCategory(category, page, pageSize);
    // ... populate posts model ...

    if (Request.IsHtmx()) return PartialView("_BlogSummaryList", posts);
    return View("Index", posts);
}

依您看 @: @ @

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

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

这没关系,因为":

  • 简单卡路里值 @ (_ string@ MS K1} int) {
  • 仅在视图中使用@,没有传过
  • 将添加备选案文 Title 和 Category 每个视图模型的属性

在 ViewBag 中我所避免的复数对象@:

Antió-patten:

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

问题 @:

  • 没有编译@- @ time safety @ (_ typo ViewBag.Usr 运行时失败@) @%
  • 难以跟踪现有数据
  • 需要检查 ViewBag 字典@)}使测试更难( ( @%)
  • 诺特利感

更好的: 强烈的@- 类型视图模型 @:

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

{\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}这里是'

标准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”中不使用TempData?

  1. 我用HTMX代替改方向: 我的表单通过 HTMX 提交并返回部分视图, 且在线上成功@/error message @. 没有重定向 *=不需要 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!"
    });
}
  1. 对于传统的 PRG {(} post-} < redirect -}\ Get_)“,”我会使用Tempdata. 但我更希望尽可能避免重定向, 以便更好 UX=.#

当Temta 说得通时 @: @

  • POST后有整页重定向的传统MVC应用软件
  • 在两个阶梯之间重定向的多Q-step 向导
  • 认证重定向后的闪电消息

坦普达塔得到恰 : 默认情况下由 cookie {(} 支持, 自 ASP\ .NET Core 2.0). 如果您在 TempData,您MS K4 以每份请求方式上传的 cookies =. 为大状态{,} 使用会话时使用一个后台或 DBMSC7

快速决策树

flowchart TD
    A[Need to pass data to view?] --> B{What kind?}
    B -->|Global layout data| C[ViewBag in BaseController]
    B -->|Simple page-specific| D[ViewBag in action]
    B -->|Complex model| E[Strongly-typed ViewModel]
    B -->|Survive redirect| F{Using HTMX?}
    F -->|Yes| G[Return partial with message]
    F -->|No| H[TempData for flash message]

我的规则 :

  1. 仅用于全局布局内容的 ViewBag -=YTET -伊甸园字幕组=- 翻译:
  2. 用于简单页面标题的视图Bag@/}标题 @ (_ optional@ MS K1}可以使用 ViewModel){
  3. 复杂对象从不查看Bag (use View Models)
  4. 从未查看数据 (+ViewBag有更好的语法
  5. 只有当您需要PRG时才会使用TempData 多亏了HTMX),

模式@ : 向导和多 MS K1 step flows

哪个州持有者使用 @? @%

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
  • Small ,單一-sessionMS K2 会话或TempData 兩步之間的 TempData_.}
  • Crossó-device/longMS K2 current}: 坚持到 DB, 在 URL_.中包含一个密钥

ưμ㼯A

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

带有 Temta 的 Flash 信件模式@: @%

  • 设置在重定向后只读过一次的 POST @ ;@
TempData["Flash"] = "Profile saved";
return RedirectToAction("Index");

Razor 视图@: @%

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

購物墨盒

  • 小马车@ : @ 会话 @ MS K1 @ 如果您的比例不小且有粘性的话, 您的分布会话@ MPK3}
  • 以 cookie 或 URL .卡通速度@.
flowchart LR
  U[User] -- cart-id cookie --> S[Server]
  S --> DB[(Cart Table)]
  S <--> Cache[Distributed Cache]

Security ,私密,及遵章检查列表

  • 校验所有客戶端@-提供 state @:}查詢,标题{,表格}MS K4 cookies_, JWT claims.
  • 保护敏感客户端@- @served state @:}使用数据保护来保存您发行的 cookie 的数据保护 * ; n从不在查询字符串中存储机密*.
  • 设置曲奇旗: Secure, HttpOnly, SameSite, IsEssential @(_ 需要同意时@/}功能需求#).
  • 恢复对特权更改的认证 cookie @ ; @ 保留最小要求@ MS K1 @ {
  • 在休息时加密服务器@- @side 仓库的加密@ ;}确保密钥旋转 *%(#Data 保护键,}JWT签署密钥{).}
  • GDPR/CCPA}:提供用户数据导出 <% /deletion path@; 最小保留=.

决定母体 (热表MS K1

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
  }

快速选取@: @

  • 需要 1-redent flash ?TemData}.
  • 在一次会话中需要多个请求的向导@ ? @ 会话 @ ( @ or DB *+_ 键, 如果长关语是-\ curringMSK4}/ @ molti-deviceMS K6}
  • 身份需要缩放和无国籍的APIs? JWT@,DB□/州分配快递}=.
  • 只需在管道内传递数据@? @HttpContext.}项目=.#%
  • 计算查询时需要快取@ ? @ IMemoryCache 本地端 @ MS K1 @ I分布式缓存@ I.}

MVC 与 Razor Pages 和 Miminimal APIS @:Same Fundes@,不同形状

所有三个堆叠都位于相同的原始区域 : @(_HtpContlext,}模式绑定@,auth,}数据保护=).}上述例子显示,API在人类工程学方面差异最大

  • 从路由 @ /Qquery@ MS K2} mbody /}$APIS=:参数绑定的最小 APIs@;}返回 Results.*.
  • MVC:Q 属性@,}过滤器 @,}模式绑定为行动参数的模型 */_view model=.}
  • Razor Pages:}页面操作器,具有绑定属性和标签辅助器以生成链接@/}%forms @.}

他们都拥有相同的州机制 讨论在这里


坑瀑布和Anti-塔

  • 在 cookie 或 TempData 中存储大型或敏感数据@ .
  • 取决于 @ - @ memory 缓存是否正确, @ I(_ it’} 会有一个缓存@ MS K3# 不是真相 @ MPK4 @
  • 基于客户端@-_sent route\ /}(没有服务器检查)的建筑授权
  • 超过- 含有挥发性索赔的曲奇饼干或JWTs@.
  • 没有分发策略的会话 @ ( @ works local@ MS K1} 在比例上休息\ ).

我实际上使用什么 (和唐・'

在展示了所有这些选项之后, ”,在这里“'我诚实地评估我的博客平台的制作效果。

我的州管理烟囱按频率顺序 )

满意度=
记忆缓存 “ ”非常高的“ ”类别“MS K2” 标准“,”翻译任务“ ”、“⭐⭐⭐⭐⭐”基本内容“MSC6”
产出缓存 @ high { }_,列出 }MS K4 巨/人贏
反应缓存 Q高端 @ QHTTP 缓冲信头@ QQ⭐⭐⭐⭐Q与产出卡奇(Outscache)一起工作 @MS K4QQ#
视图包 @ _Minal _Analytics situation {,}页面标题@ }⭐⭐⭐}OK 用简单的东西来表示 <# >
授权请求书 @ _Minential }用户身份@,_管理旗 { }⭐⭐⭐⭐⭐_右方工具
路由@/rury @ 中度 倾斜 , 过滤MS K3 鼻涕虫 * *#⭐⭐⭐⭐ 无国籍和可链接的 *
数据库数据库 @ }⭐⭐⭐⭐⭐}#真相來源
饼干 “ Low 用户首选项”“(Future) @ #⭐⭐⭐Have{'}需要@
届会届会会议届会届会届会届会议届会届会 会议届会届会 @ @ 永远不要 @ @ * - @ * # *没有分布式商店设置@ *
临时数据 @ #永远不要 - @ @# #HTMX消除需要= #
HttpContext .}项目 “ 永远不要”“ ”、“-” 「 」 《❌》《黑文》5"特例用过「 」
分布缓存 @ 永远不要\ \ \ NSK2\ { } @ ❌ 单个服务器 @MS K5}(现在) < * @MSC7\ *

源自生产经验的详细细节

ImemoryCache @ ⭐⭐⭐⭐⭐ @

我用它来做什么?

  • 分类列表 @(global,}30 min TTLQZ)
  • 用户翻译任务追踪@(6_h绝对 @+}#1QH滑动□)
  • 校对:Portnoy

实践中的实践证明

  • 快速闪烁 @ _ in @ I-} process * MS K2 *
  • 无连序间接费用
  • 减少 DB/API 载荷
  • 易于执行和理解
  • 通过 " Serilog追踪 " 观测

{\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}

  • 如果您不做 @ ' @ t限制 per-}% 用户缓存, 内存会泄漏
  • 程序重新启动后清除@( @% 允许我使用 case @ )} *
  • 服务器上没有共享@ ( @ fine, 仅举一个例子 @ MS K1
  • 缓存无效是手动@(}需要删除 @() 明确)

缩放限制: 如果我能到达多个服务器@, I'd 需要Idispreditracte Cache @(Ridis}) 共享状态=. 现在,单一个服务器{+存储缓存是完美的_.}

输出缓存 @ + @ 响应缓存@ MS K1 @

我用它来做什么?

  • @(1_hour service@,}5}小用戶端{/)
  • 博客列表@/ @类页面
  • 日历数据

实践中的实践证明

  • 25x 加速 @(50 @%ms @ →_ _2 @ ms@)} @
  • 以高交通流量为尺度,不破汗流
  • 服务器与客户端的分隔 TTL
  • 使用 HTMX @ (#VaryBy Header@)}无缝工作
  • 通过Prometheus指标衡量的可计量影响

{\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}

  • 调试混乱@ ( @ forget 缓存在)}
  • 含有许多查询参数组合的缓存爆炸
  • 客户端的静态窗口 @ (5 分钟@ MS K1 @
  • 内容更新需要手动失效

最好的为: 读取多为静态内容的-Heavy应用程序@.}对于个性化或真实来说不怎么好 @-}时间数据=.}

视图Bag @ ⭐⭐⭐

我用它来做什么?

  • 全球布局数据 @ ( @ analytics @ MPK1} 类别@ MS K2
  • 页面标题

实践中的实践证明

  • 用于布局的简单快简便数据@-}关卡
  • 设置一次在基地控制器@,}无处不在
  • 使用缓存分类列表精细工作

{\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}

  • 运行时无类型安全 \ ( typos 失败 )\ {
  • 诱惑过度使用复杂数据
  • 难以测试

我遵循规则 遵守规则 仅用于简单的 calars 的 ViewBag 视图框@ . @ complicate 对象在查看 Models @ MS K1} {

C. 索赔要求

我用它来做什么?

  • 用户 ID , @ name @ MS K1 @ email *,} avatar URL
  • 管理标记 # ( checking sub C. 关于ConfigQ)索赔的索赔

实践中的实践证明

  • 数据保护=)
  • 自动使用 ASP.NET 核心身份@/OAUT
  • 可通过 User.Claims 各地
  • Slip 到期日让用户登录

{\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}

  • Cookie 大小限制 @ (don'}%t 超额索赔=)
  • 索赔在重新-@login之前是静止的
  • 如果我加上一个要求, “(” 或“" ” 或者“ISEDIOR "), ” 需要重新添加“MS K3” 问题 cookie

最佳做法:* Don'}在索赔中经常提供 -}交换数据=.}

我用的是( 以及为什么?)

会话 @ (_ never used MS K1}%

  • 需要已分配的仓库@ ( @ redis)}
  • 为最低效益增加复杂性
  • 我使用的案例最好由 @:@
      1. 被裁定的索赔 *
    • ImemoryCache @ ( @ short@ MS K1 @ 原状态 @ I)
    • 数据库@(可持续状态 @)

Temta @ ( @ 从未使用过@ MS K1 @

  • HTMX 已删除 Post- 重新中转}-Get 模式
  • 窗体返回含有内嵌信件的部分视图
  • 无需维持改道

HttpContext. 項目{(沒有用過MS K2

  • Haven't 有用于 per-}请求状态的用例
  • 我的中间器械为控制器计算值@ ' @ t
  • 如果我需要房客检测 , I'

从未使用过“:” % 1

  • 单一服务器部署
  • I MemoryCache 满足所有需要
  • 如果我缩到多个服务器, 将会使用 Redis

我的方法的演变

阶段: 1 MS K1 初始=): 所有请求都按下数据库 并设定了Markdown @.}因为交通流量低而罚款=.}

阶段: 2 MS K1 第一次优化=): 已添加分类的 I Memory 缓存@ . @ 观测到即时 DB 卸载Q. @ 保留它简单\ MS K2} @ 30 min TTL,{ 没有花哨逻辑=.}

阶段“3”和 “(”扩大“MS K2” 当交通流量猛增 . @ massive professional greaction #. 初始错误@:}(仅缓存) @5\ 分钟 @MS K4Q* 在监控显示内容很少变化的小时后, 添加到 &1+# hour

阶段: 4 MS K1 观察性): 添加的 Serilog 跟踪到度量缓存@ . 发现的缓存误差很高, 因为以 keys @ MS K1 固定密钥格式进行日期格式化 yyyyMMdd 点击率从 @60%@到#95%.}。

阶段 5 MS K1HTMX整合): 已添加 VaryByHeader 用于2004年和 hx-request@. 最初忘记了这个功能, 并且向 HTMX 请求提供全页服务@. 调试噩梦 直到我发现这是在.

当前状态@ : @ 满意我的国家管理需要的 M98%+#.+#永久状态数据库=.#Auth Claims for identation@.BARBAR

您应用程序的咨询

从这里开始 @ : @ @

  1. 用于导航的路线/Qquery prams
  2. 身份证明索赔
  3. 任何必须持久的数据库
  4. 读取“-”时Imemory缓存
  5. 昂贵的输出缓存@- @to @-_render page

需要时添加@: @% 6. 会话 @(}只指您必须拥有服务器@-#side contactal state=)} 7.I 分配缓存 @(只有当您向多个服务器缩放时@) @8.cookies {(} 为客戶端 <-}_Side喜好{同意}{)

避免:

  • ViewBag/TempData 中的复杂对象
  • 没有分布式支持商店的会话
  • Caching user@-}特定或经常 @-}改变数据
  • 早熟优化@ ( @ 度量先\ !)}

关键课程@: @%

  • 仅在测量显示需要时才开始简单@, 添加复杂性
  • 可观察性是关键 ( 知道你的缓冲速率
  • 考虑一下比例限制的早期@( @per% -}用户缓存需要边框 @)}
  • 测试缓存完全无效@( @staleness is a real problem *\ )
  • 文档中您的 TTL 选项@ ( @ 未来您将询问 # MS K1# 为什么\ 30 @ minutes @ MSC3}

折叠-

在网络应用程序中状态为 @ ' @ title 1 大小适合 all\ .} 选择满足您需要的最轻的选项@ MS K2 @ 偏向无国籍模式, 当您能够 @ I, 并且清晰地描述安全和生命周期时 @ MPK4 @

如果你想更深入地了解这些碎片是如何通过管道的 ,,看看我的系列从开始 概述和基金会 特别是中间器械和路由部件

幸福的建筑


附录::+更多副本@‑可粘贴示例 @(_Refinements=)}

这些例子深化了前几节的制作内容: @‑_gage 细节您可以粘贴到网络@9}最小模板@MS K2 MVC=,}或Razor Pages+.}

Cookies: 带有数据保护的保护价值

using Microsoft.AspNetCore.DataProtection;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDataProtection();
var app = builder.Build();

app.MapPost("/prefs/secure/{value}", (HttpContext ctx, string value, IDataProtectionProvider dp) =>
{
    var protector = dp.CreateProtector("prefs.theme");
    var protectedValue = protector.Protect(value);
    ctx.Response.Cookies.Append("pref.theme.p", protectedValue, new CookieOptions
    {
        HttpOnly = true,
        Secure = true,
        SameSite = SameSiteMode.Lax,
        Expires = DateTimeOffset.UtcNow.AddYears(1)
    });
    return Results.Ok();
});

app.MapGet("/prefs/secure", (HttpContext ctx, IDataProtectionProvider dp) =>
{
    if (ctx.Request.Cookies.TryGetValue("pref.theme.p", out var v))
    {
        var protector = dp.CreateProtector("prefs.theme");
        return Results.Text(protector.Unprotect(v));
    }
    return Results.NotFound();
});

在多@ ‑ @ node dispends @ MS K2 持续的数据保护密钥 @ MPK3 @ e.g}., 一个共享的文件系统, Redis,或 Azure 键 Vault_ ), 这样可以跨例阅读 cookies@.}

MVC,Razor PagesMS K1和最小API中的防伪

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

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

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

app.MapControllers();
app.MapRazorPages();
  • MVC=: 装饰行动 [ValidateAntiForgeryToken] 以及使用 @Html.AntiForgeryToken() 窗体中
  • Razor Pages: 默认启用的窗体 asp-antiforgery="true" 需要时 @. @ @
  • 通过 Minimal\ :}验证 IAntiforgery 显示的@. @%

带有 Rediis 和滑动与绝对到期的会话

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

小型存储@ , @ 压缩数据( 只有) @ MS K1 @ persist real masses @ MPK2} 命令到 DBQ.}

使用输入选项和回召驱逐的记忆缓存

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

IdispretrateCache 获得@ ‑ @ or @ MS K1}设置有抖动以避开印章

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 @.},

本地的 JWTs @ ( @demo@)}发布并验证

using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;

var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("super-secret-key-please-rotate"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", o =>
    {
        o.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = false,
            ValidateAudience = false,
            IssuerSigningKey = key,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true
        };
    });
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

app.MapPost("/token", () =>
{
    var claims = new[] { new Claim(ClaimTypes.Name, "alice") };
    var jwt = new JwtSecurityToken(claims: claims, expires: DateTime.UtcNow.AddMinutes(30), signingCredentials: creds);
    var token = new JwtSecurityTokenHandler().WriteToken(jwt);
    return Results.Json(new { access_token = token });
});

app.MapGet("/who", [Microsoft.AspNetCore.Authorization.Authorize] () => "ok");

与 ETags 有条件更新@( @IFT#‑}(Match_))

record Todo(int Id, string Title, string Version);
var store = new Dictionary<int, Todo> { [1] = new(1, "Ship", "v1") };

app.MapGet("/todo/{id:int}", (int id, HttpContext ctx) =>
{
    if (!store.TryGetValue(id, out var t)) return Results.NotFound();
    ctx.Response.Headers.ETag = t.Version;
    return Results.Json(t);
});

app.MapPut("/todo/{id:int}", (int id, HttpContext ctx, Todo input) =>
{
    if (!store.TryGetValue(id, out var current)) return Results.NotFound();
    var ifMatch = ctx.Request.Headers["If-Match"].ToString();
    if (string.IsNullOrEmpty(ifMatch) || ifMatch != current.Version)
        return Results.StatusCode(StatusCodes.Status412PreconditionFailed);

    var next = current with { Title = input.Title, Version = $"v{DateTime.UtcNow.Ticks}" };
    store[id] = next;
    ctx.Response.Headers.ETag = next.Version;
    return Results.Ok(next);
});

通过 JSON 传输 TemtaData:复合物体

public static class TempDataJsonExtensions
{
    public static void Put<T>(this ITempDataDictionary tempData, string key, T value)
        => tempData[key] = System.Text.Json.JsonSerializer.Serialize(value);

    public static T? Get<T>(this ITempDataDictionary tempData, string key)
        => tempData.TryGetValue(key, out var o) && o is string s
           ? System.Text.Json.JsonSerializer.Deserialize<T>(s)
           : default;
}

// Usage in MVC action
TempData.Put("WizardState", new { Step = 2, Name = "Alice" });
var state = TempData.Get<dynamic>("WizardState");

EF 核心货币代汇

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    [Timestamp] public byte[] RowVersion { get; set; } = default!;
}

// On update
try
{
    await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
    return Results.StatusCode(StatusCodes.Status412PreconditionFailed);
}
app.MapPost("/promote", async (HttpContext ctx) =>
{
    var u = ctx.User;
    var claims = u.Claims.ToList();
    claims.Add(new Claim(ClaimTypes.Role, "Editor"));
    var id = new ClaimsIdentity(claims, "Cookies");
    await ctx.SignInAsync("Cookies", new ClaimsPrincipal(id));
    return Results.Ok();
});

这应该涵盖以下空白: :+更强的安全默认值@,+%多 @ mostK2_node ready,+和真实的‑世界缓存模式=,#marks@MS K6}以及有条件的请求.{


深底端@: @ HtpContext_.}項目 @ ( @ 實體樣式與助手@ MS K3 @

HtpContext.}项目是您在不触动全球状态或长时间情况下将计算值从中器端点传到您的终端端点@,控制器 @,}和 Razor Page 处理器的完美使用。

  • 响应完成时丢弃@.
  • 范围@: @% 当前请求只@ -} 从不交叉重定向或背景工作 @ MS K2 @ *
  • 动作:OO} O(1)}查询: ;Q 理想的 perMS K3}请求抓取}.}
  • 安全@ : @ server @ MS K1 @side only ;} 客户端无法看到@. @

为什么项目不是 @… @%

  • Session /TempData:这些交叉请求并介绍分配问题@.项目是短暂的和规模的\‑友好的 @.
  • 服务范围@: 使用这些功能作为行为和共有的依附性 @. 项目对 ad{-hoc},}计算值更好
  • Htpcontext@ . @ fatures:}对于框架 < MS K2 transport -\ 关卡功能 < I(\ iEndpointFature_ MSKO5\ IHTPUpgrationFatrial).}项目是用于 app-{ 级别数据=.}

避免键相撞@ : @ 强烈@ MPK1 @ typed keyword

因为项目使用对象密钥@, 偏好私有静态物件密钥或专用密钥类型以避免名称碰撞 @.

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

模式@ : @ complete in middware MS K1} 在端点中计算消耗的% k2\ 控制器@ MPK3_ page

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

请求缓存以避免重复工作@ action: button

使用项目作为如此重复的小缓存, 在同一请求中读取@ ’ @ tre-hit数据库@/_ services @ MS K3}

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

注::

  • Treading @ : 单一请求通常在一条逻辑路径上执行@ MS K1 项目是 ’\ t线索@ MISK3} 安全到平行写进@ I. 如果您开始并行任务, 共享项目@ MPK5 添加您自己的同步=.}
  • 大小@ :} 保留小值和低价值, 以计算 @ MS K1 @ serrigize @ MPK2 @ It’%s in\ -_ memory per request @ MASK5}

填充項目@( @MVC#/_Razor Pages @)}的樣式:过滤器Name

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

各地没有拨款的丰富原木

计算一次@ , @ 然后在日志范围或中瓦内读取@ MPK1 @

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

何时不使用项目

  • 重新定向后或请求中需要的数据@( @use TembalData/ @saymond @/}代之以#).}
  • App@- @% 宽度单吨或横跨-}请求缓存 @ (_ use I MemoryCache @ MPK3# IDdispriedCacheQ).}
  • 属于身份的值 */授权 %(}使用权主张/Politicies=).

Quick rules @: 如果在此请求中计算并读取您自己的代码@,}项目是理想的\ .}

logo

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