尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

.NET 9 ASP.NET Core实战:远程验证、HybridCache与静态资源优化

.NET 9 ASP.NET Core实战:远程验证、HybridCache与静态资源优化 这次我们来看 .NET 9 里 ASP.NET Core 的实战深度内容。.NET 9 是 2024 年 11 月发布的 STS短期支持版本虽然不是 LTS但它的 ASP.NET Core 部分一口气补上了不少生产环境需要用到的能力远程验证、服务端输出缓存、静态资源指纹、OpenAPI 文档生成等。这篇文章会分成两条线展开一条是很多人搜索过但没找到完整方案的“如何启用远程验证 remote”另一条是 .NET 9 里新增的 HybridCache 与 MapStaticAssets。全程会给出可复制命令、代码、配置和排查思路。先说结论如果你正在用 .NET 8 或者 .NET 6 的项目并且被“用户名重复校验”“邮箱重复校验”“表单提交不算频繁但每次都要校验”这类需求困扰过远程验证值得直接看如果你关心站点性能、缓存击穿、静态资源带指纹发布.NET 9 的 HybridCache 和 MapStaticAssets 值得纳入技术选型评估。1. .NET 9 ASP.NET Core 核心能力速览能力项说明运行时版本.NET 9.0STS支持 18 个月框架组件ASP.NET Core 9.0包含 MVC、Razor Pages、Blazor、Minimal API、SignalR新增缓存抽象HybridCache位于 Microsoft.Extensions.Caching.Hybrid静态资源处理MapStaticAssets对静态文件做指纹、压缩和条件请求OpenAPI 支持Microsoft.AspNetCore.OpenApi新增 MapOpenApi 端点远程验证MVC/Razor Pages 的 [Remote] 属性需要 jQuery Unobtrusive Validation最低开发环境.NET 9 SDKVisual Studio 2022 17.12 / VS Code / Rider是否支持跨平台是Windows、Linux、macOS 均可运行API 接口能力原生支持 REST API、SignalR、gRPC、WebSocket批量任务能力需要自行实现 HostedService 或队列框架本身不含任务队列适合场景Web 站点、API 服务、微服务、Blazor 应用、混合缓存场景从表格能看出来.NET 9 这一版真正值得关注的是基础设施层面的改进而不是花哨语法。它对现有 ASP.NET Core 项目的迁移成本相对可控依赖注入、中间件、控制器这些核心编程模型没有颠覆性变化。2. 适用场景与使用边界这一类技术内容适合谁适合正在维护 .NET 6/8 项目的后端开发也适合准备启动 .NET 9 新项目的团队做技术预研。远程验证适合需要“一边输入一边提示”的交互场景最典型的就是注册页判断用户名是否重复。这个问题如果用传统 Post 全表单提交体验差如果用前端写死名单又不安全。远程验证的价值是让服务器在输入框失焦后立即校验效率高且结果可信。HybridCache 适合读多写少、数据量不大但访问集中的数据比如商品详情、用户配置、配置项。它和 IMemoryCache、IDistributedCache 的核心差异是封装了“内存缓存 分布式缓存”两层结构并且内置了防缓存击穿机制。使用边界需要注意远程验证只适用于服务端渲染的 MVC 或 Razor Pages 场景如果前端是 Vue/React API 完全分离更推荐直接调用校验接口而不是引入 jQuery Unobtrusive。HybridCache 的防击穿机制不等于数据库无限并发保护它保护的是同一个 key 的并发请求设计缓存 key 时仍然要考虑业务维度。MapStaticAssets 在 .NET 9 中用于符合 Web SDK 条件的项目不是无脑替换 UseStaticFiles迁移前需要检查项目结构。如果涉及用户数据、身份信息、未公开的业务数据缓存和分布式缓存必须考虑隐私边界敏感数据不建议长缓存时间更不能写入没有访问控制的公共缓存。3. 环境准备与项目初始化开始之前确认环境里已经安装 .NET 9 SDK。在终端里执行dotnet --list-sdks如果输出里看不到类似 9.0.100 的版本需要先安装 .NET 9 SDK。安装完成后创建项目。这里演示两种项目模板一种是普通 Web API一种带 MVC 界面执行# Web API 项目 dotnet new webapi -n Net9.Demo.Api # MVC 项目 dotnet new mvc -n Net9.Demo.Mvc创建完项目后先确认目标框架cd Net9.Demo.Api cat Net9.Demo.Api.csproj正常情况 csproj 里会有这一行TargetFrameworknet9.0/TargetFramework如果项目文件是 net9.0说明环境正确。为了方便测试还要添加两个包dotnet add package Microsoft.AspNetCore.OpenApi dotnet add package Microsoft.Extensions.Caching.Hybrid第一个包用于 OpenAPI 文档生成第二个包是 HybridCache。如果你更想用 Visual Studio 操作可以直接用 VS 2022 17.12 以上的项目模板创建时选择 .NET 9.0其他逻辑不变。4. 远程验证启用与实现细节这一节是全文重点。先解释清楚远程验证不是 .NET 9 的新功能它是 ASP.NET Core MVC 里长期存在的[Remote]验证特性但在 .NET 9 项目中仍然好用而且它很好地解决了实际业务里的重复校验问题。4.1 远程验证解决的问题假设注册页面有“用户名”输入框。传统做法是用户填完整个表单后点提交服务端再判断用户名是否重复重复则返回错误。这个方案的问题很明显用户要等填完所有字段才发现用户名不行。另一个做法是前端准备一个“所有已注册用户名集合”在页面加载时一次性拿回来前端本地判断。这个方案在小数据量时能用用户量大了就是灾难而且存在数据泄露风险。远程验证的机制是输入框失焦后前端自动向服务端发送 Ajax 请求只携带部分字段服务端处理后返回 JSON 格式的 true/false前端据此显示校验信息。它不需要提交完整表单也不会把用户名单全部暴露到前端。4.2 后端校验 Action 设计在 MVC 项目里新建一个 AccountController并添加一个可用于校验的 Action。注意这个 Action 返回类型是JsonResult不是ViewResult。下面是一个最基础的定义using Microsoft.AspNetCore.Mvc; namespace Net9.Demo.Mvc.Controllers; public class AccountController : Controller { // 模拟一个已经存在的用户列表 private static readonly string[] ExistingUsers { admin, root, test }; [AcceptVerbs(GET, POST)] public IActionResult CheckUsername(string username) { // 远程验证期望拿到 bool见 4.4 bool isAvailable !ExistingUsers.Contains(username, StringComparer.OrdinalIgnoreCase); return Json(isAvailable); } }这里有两个细节[AcceptVerbs(GET, POST)]是为了兼容 jQuery Unobtrusive 默认发起的 GET 请求也允许客户端改用 POST避免某些浏览器或代理对 GET 请求的缓存。返回Json(bool)是远程验证默认约定的格式。前端 validation 插件会把这个 bool 当作校验结果true 表示验证通过false 表示验证失败。4.3 模型上添加 [Remote] 特性接下来创建或修改一个 ViewModel在需要校验的属性上添加[Remote]特性。这个特性能告诉 MVC 框架某个属性需要走远程验证并且要请求哪个 Controller 下的哪个 Actionusing System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc; namespace Net9.Demo.Mvc.Models; public class RegisterViewModel { [Required] [Remote(action: CheckUsername, controller: Account, ErrorMessage 用户名已被占用)] public string UserName { get; set; } string.Empty; [Required] [DataType(DataType.Password)] public string Password { get; set; } string.Empty; }ErrorMessage是校验失败时显示的提示文字可以按业务需要修改。4.4 前端引入 Unobtrusive Validation远程验证能不能跑通前端环境是关键。jQuery、jQuery Validation、jQuery Unobtrusive Validation 这三个脚本缺一不可。在 Razor 视图里可以这样引入div classcontainer form asp-actionRegister asp-controllerAccount methodpost div classform-group label asp-forUserName/label input asp-forUserName classform-control / span asp-validation-forUserName classtext-danger/span /div div classform-group label asp-forPassword/label input asp-forPassword classform-control / span asp-validation-forPassword classtext-danger/span /div button typesubmit classbtn btn-primary注册/button /form /div section Scripts { script src~/lib/jquery/dist/jquery.min.js/script script src~/lib/jquery-validation/dist/jquery.validate.min.js/script script src~/lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js/script }这里直接使用asp-for标签生成输入框。Razor 会看到RegisterViewModel.UserName上的[Remote]特性并在渲染 HTML 时自动输出>[Remote( action: CheckUserCombination, controller: Account, AdditionalFields Email, ErrorMessage 用户名和邮箱组合不合法)] public string UserName { get; set; } string.Empty;服务端 Action 通过第二个参数接收 Email[AcceptVerbs(GET, POST)] public IActionResult CheckUserCombination(string username, string email) { bool result IsCombinationValid(username, email); return Json(result); }4.6 与 ASP.NET Core Identity 结合的真实场景如果项目使用了 ASP.NET Core Identity可以把用户判断改为读数据库。下面的写法适合注册页判断用户名是否重复using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; public class AccountController : Controller { private readonly UserManagerIdentityUser _userManager; public AccountController(UserManagerIdentityUser userManager) { _userManager userManager; } [AcceptVerbs(GET, POST)] public async TaskIActionResult CheckUsername(string username) { if (string.IsNullOrWhiteSpace(username)) { return Json(false); } var user await _userManager.FindByNameAsync(username.Trim()); return Json(user null); } }从性能角度看FindByNameAsync会对用户表做一次查询这个频率一般不高因为只有输入框失焦时才会触发不会每个键盘事件都发请求。4.7 远程验证的局限性远程验证很适合做“快速判断”但它不能替代服务端最终校验。原因很简单攻击者完全可以跳过前端脚本直接提交合法或非法数据。所以在任何需要高安全性的场景里Controller 的 Post Action 里仍要重复执行一次用户名是否存在的检查。远程验证只是提升用户体验不是安全边界。也有一个值得注意的细节远程验证在多人并发注册时存在竞态窗口A 用户和 B 用户同时提交同一个用户名远程验证阶段都可能通过只有数据库唯一索引能兜住最后一层。生产环境里用户名、邮箱这类字段一定要加数据库唯一约束。5. HybridCache 缓存深入体验.NET 9 的另一个重点是 HybridCache。它的定位很直接同时使用进程内内存缓存和分布式缓存把读取高频、更新低频的数据放到更快的一层同时通过机制避免两个缓存之间的数据不一致。5.1 为什么需要 HybridCache在 .NET 9 之前很多项目面临两难选择IMemoryCache很快但它只存在于当前进程多实例部署时数据不一致而且无法跨容器共享。IDistributedCache能跨实例共享但和 Redis、SQL Server 通信有网络开销读一次数据慢不少。HybridCache 的思路是先查内存缓存如果命中了就直接返回未命中再去查分布式缓存两层都没有再去执行数据源查询并把结果同时写入两级缓存。而且它默认实现了一套防缓存击穿机制同一个 key 的并发请求只会有一个去执行真实数据源读取其他请求等待这个结果。5.2 注册 HybridCache注册方式非常简单。在 Program.cs 中加入using Microsoft.Extensions.Caching.Hybrid; var builder WebApplication.CreateBuilder(args); builder.Services.AddHybridCache(); var app builder.Build(); // 这里配置路由 app.MapGet(/weather/{city}, async (string city, HybridCache cache, CancellationToken ct) { // 实际处理见下方 }); app.Run();如果只使用内存缓存只需要AddHybridCache()。如果要用 Redis 作为二级缓存同时添加dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis配置builder.Services.AddStackExchangeRedisCache(options { options.Configuration localhost:6379; }); builder.Services.AddHybridCache();5.3 使用 GetOrCreateAsync核心用法是GetOrCreateAsync。下面是一个商品详情的示例public class ProductService { private readonly HybridCache _cache; public ProductService(HybridCache cache) { _cache cache; } public async TaskProductBrief GetProductAsync(int productId, CancellationToken ct) { return await _cache.GetOrCreateAsync( $product-{productId}, async token await LoadProductFromDatabaseAsync(productId, token), new HybridCacheEntryOptions { Expiration TimeSpan.FromMinutes(10), LocalCacheExpiration TimeSpan.FromMinutes(5) }, ct ); } }参数解释第一个参数是缓存 key必须包含业务维度。第二个参数是回调只有缓存未命中时才会执行。Expiration是整体过期时间。LocalCacheExpiration是本地内存缓存的过期时间可以比分布式缓存短保证多实例场景下本地数据不会太陈旧。如果没有指定默认跟随Expiration。5.4 标签失效HybridCache 支持按标签批量移除缓存项。比如一组数据被同一个操作更新可以打上同一个 tagawait _cache.GetOrCreateAsync( $product-list-page-{page}, async token await QueryProductListAsync(page, token), new HybridCacheEntryOptions { Expiration TimeSpan.FromMinutes(5) }, ct );配合RemoveByTagAsyncawait _cache.RemoveByTagAsync(product-updated, CancellationToken.None);这里需要说明RemoveByTagAsync在 .NET 9 的某些实现里对标签的操作能力和底层缓存实现相关生产环境使用前要确认后端缓存是否支持标签。更稳妥的做法是维护业务层的版本号 key数据变化时切换 key 前缀而不是完全依赖直接删除。5.5 使用注意点HybridCache 的缓存 key 中不能包含太多敏感信息。虽然 key 是字符串但普通情况下会记录到日志、追踪系统里如果 key 里有用户手机号、邮箱则会带来不必要的信息暴露风险。另外如果数据更新频率很高就不适合长时间缓存。可以把Expiration设得很短比如 10 秒用于抗住瞬时峰值。缓存不是数据库的替代它只是数据库前面的保护层。6. MapStaticAssets 静态资源交付.NET 9 里另一个值得关注的变化是MapStaticAssets。它的目标是替代部分UseStaticFiles场景把静态文件做成带指纹的构建产物并提供更高效的压缩和条件请求。6.1 基本用法在 Blazor Web App 或 Web SDK 项目中把原来写UseStaticFiles的位置改成var app builder.Build(); app.MapStaticAssets(); app.MapRazorPages(); app.Run();需要注意MapStaticAssets在 .NET 9 中并不是直接替换所有UseStaticFiles场景。它依赖构建时的静态 Web Asset 清单信息所以更适合使用了 Web SDK、且静态文件参与构建管线的项目。如果你只是需要简单地对外提供wwwroot目录里的文件不涉及指纹、缓存优化继续使用UseStaticFiles也没有问题。6.2 指纹与缓存刷新使用MapStaticAssets之后项目发布时会给静态资源生成基于内容哈希的指纹 URL。例如原来的site.css在页面上可能渲染为site.css?vabc123。这样 CSS、JS 文件内容变化时浏览器会请求新地址从而解决“改完样式但用户浏览器缓存不更新”的经典问题。6.3 性能收益体现在哪传统UseStaticFiles在请求时判断文件是否存在、读磁盘、设置响应头。MapStaticAssets在启动阶段就构建了资源元数据请求进来直接按索引匹配省掉一部分文件 IO 和重复判断。从官方给的预期看静态资源的请求延迟和启动时间都有优化空间但具体数值要结合文件数量和体积测试不要直接照搬别人的基准结果。如果已有站点对静态文件的要求不高比如只有几个 JS 文件这个优化带来的收益不明显。如果项目里有大量样式、脚本、图片值得做一次对比测试。7. Minimal API 与 OpenAPI 文档.NET 9 对 OpenAPI 支持做了不少改进。这里要区分ASP.NET Core 8 自带的 OpenAPI 还有不少实验性质.NET 9 里已经可以在Microsoft.AspNetCore.OpenApi包里直接使用。7.1 添加 OpenAPI 服务var builder WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); var app builder.Build(); app.MapOpenApi(); app.Run();启动后访问/openapi/v1.json就能看到自动生成的 OpenAPI 文档。7.2 给接口补充摘要信息Minimal API 默认生成的文档比较简陋用WithSummary、WithDescription可以让文档更可读app.MapGet(/weather/{city}, async (string city, WeatherService service) { var data await service.GetWeatherAsync(city); return Results.Ok(data); }) .WithName(GetWeather) .WithSummary(获取指定城市天气) .WithDescription(返回指定城市当前天气数据支持城市名拼音。) .WithTags(weather);7.3 从 OpenAPI 到客户端代码OpenAPI 文档生成后可以用dotnet-openapi工具或NSwag生成 TypeScript、C# 客户端。这相当于提供一个标准的 API 契约前端和后端可以并行开发。不过MapOpenApi默认的文档端点是无鉴权的。如果接口信息属于内部实现细节部署到生产环境时应该放在内网或者用 Authentication 限制访问。不要把内部 API 文档直接暴露到公网。8. 性能观察与资源占用很多刚接触 .NET 9 的人会关心这版 ASP.NET Core 启动快不快内存占用多少这个问题没法给出统一答案因为它取决于应用类型、依赖数量和运行环境。但可以通过一套通用的方式观察。8.1 启动时间创建默认的 Web API 项目后在终端里运行dotnet run观察从执行命令到监听到端口的时长。更准确的方式是使用dotnet-countersdotnet-counters monitor --process-id pid或者启动时开启诊断DOTNET_EnableDiagnostics0 dotnet run如果启动时间明显偏长通常是依赖注入容器初始化慢、反射扫描程序集多、初始化数据库连接等原因。8.2 内存与 GCdotnet-counters里最值得关注的是GC Heap Size、Working Set、Time in GC。如果内存上涨明显可以先检查是否开启了太多缓存尤其是IMemoryCache没有设置过期时间。HybridCache 默认内存缓存也会占用内存需要为每个 key 设置合理的过期时间。8.3 压测工具接口压测推荐用hey或wrk。比如对刚才的/weather接口压测hey -n 5000 -c 100 http://localhost:5000/weather/shanghai压测时观察QPS 是否稳定。平均响应时间是否随并发上涨异常。CPU 使用率。GC 频率。如果接口命中 HybridCache理想情况下 QPS 应该远高于未命中缓存的状态。通过对比有没有缓存两种场景的压测结果可以直观判断缓存是否生效。8.4 降低资源占用的通用手段发布时使用 Release 配置dotnet publish -c Release适当裁剪运行时使用.NET 9的应用程序裁剪但不适用于所有场景尤其是反射较多、动态生成代码的项目。服务器 GC 模式与工作站 GC 模式根据负载调整。静态资源交给 CDN避免 ASP.NET Core 进程承担不必要的文件传输开销。9. 接口调用与批量任务触发ASP.NET Core 本身不含现成的任务队列但我们可以用 HostedService 实现简单的后台批量任务处理。如果你的场景是“定时批量处理一批数据”配合缓存和后台服务能做到不错的效果。9.1 后台批量任务服务using System.Collections.Concurrent; public class BatchTaskService : BackgroundService { public static ConcurrentQueuestring TaskQueue { get; } new(); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { while (TaskQueue.TryDequeue(out var taskId)) { // 处理任务 await ProcessTaskAsync(taskId); } await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); } } private Task ProcessTaskAsync(string taskId) { // 具体业务逻辑 return Task.CompletedTask; } }注册builder.Services.AddHostedServiceBatchTaskService();9.2 向队列投递任务app.MapPost(/batch/enqueue, (string taskId) { BatchTaskService.TaskQueue.Enqueue(taskId); return Results.Accepted(new { taskId }); });这种实现适合简单的内存队列场景缺点是进程重启后队列会丢失。如果要求可靠投递还是要引入 Redis Stream、RabbitMQ 或 Azure Service Bus。文章不展开消息队列细节但设计批量任务时一定要考虑失败重试、幂等性和任务日志。10. 常见问题与排查方法问题现象可能原因排查方式解决方案远程验证不触发前端缺少 jquery.validate.unobtrusive 脚本F12 查看 Network确认输入框失焦后是否发请求引入完整脚本检查 Layout 中是否加载 jQuery远程验证返回 500后端 Action 里抛出异常查看服务端日志检查依赖注入是否注册检查数据库连接远程验证一直显示失败后端返回结果不是 bool或未返回 JSON浏览器直接打开校验 URL确保 Action 返回 Json(true/false).NET 9 项目找不到 OpenAPI 包未添加 Microsoft.AspNetCore.OpenApi检查 csproj PackageReference执行 dotnet add package Microsoft.AspNetCore.OpenApiHybridCache 缓存不生效每次请求 key 不同或未复用服务打印缓存命中日志检查业务 key 设计确认服务生命周期是 Singleton/Scoped静态资源 URL 没有指纹仍在使用 UseStaticFiles检查 Program.cs改为 app.MapStaticAssets()确认项目使用 Web SDK端口启动冲突其他进程占用端口netstat -ano查找端口占用用 app.Run(http://localhost:5001) 显式指定端口页面编译慢调试模式下反射 JIT 多发布 Releasedotnet publish -c Release这个表是通用排查起点。实际项目里如果日志更详细建议把每个中间件的异常写清楚方便定位是哪一层出的问题。11. 最佳实践与使用建议先给一个完整的技术选型清单适合拿到新项目或者准备迁移时快速对照交互表单中有唯一性校验需求优先考虑远程验证但必须保留服务端二次校验。读多写少、可容忍短时间陈旧的数据优先考虑 HybridCache严格要求实时性的数据不要缓存。大量静态资源的站点迁移到 .NET 9 后可以评估 MapStaticAssets简单站点维持 UseStaticFiles 即可。对外提供 API务必配置 OpenAPI 文档并给关键接口标注 Summary提高前后端协作效率。后台批量任务至少包含队列、执行器、失败重试、日志四部分。生产环境默认不开启 OpenAPI 文档或在网关层做访问控制。远程验证的安全实现要注意一个问题通过[Remote]暴露的校验接口会让未登录用户也能触发一定量的数据库查询。虽然单次开销不大但如果被恶意脚本高频轮询仍然可能构成一个消耗数据库连接的手段。建议对这个接口增加简单的频率限制或者校验来源甚至引入验证码避免被批量调用。HybridCache 的 key 设计也是一个容易踩坑的点。key 应该包含业务名和业务标识不要裸用userId这种不具辨识度的字符串。另外如果缓存的数据包含租户信息key 里必须包含租户 ID否则可能发生跨租户数据串读。MapStaticAssets 的迁移要检查项目中是否有动态生成的静态文件。如果有些文件是运行时创建的MapStaticAssets 就不会自动处理需要额外配置或者继续使用 UseStaticFiles 处理这些动态文件。12. 总结这次我们重点解决了两个问题第一asp.net core 如何启用远程验证 remote从后端 Action、Model 特性到前端 Unobtrusive Validation 脚本完整跑通了整个链路第二梳理了 .NET 9 在缓存和静态资源上的新工具HybridCache 解决了两级缓存协调问题MapStaticAssets 解决了静态资源指纹和请求效率问题。另外Minimal API 和 OpenAPI 的组合也基本能在 .NET 9 里直接落地。如果你正在评估 .NET 9 值不值得升级我最建议先做一个最小验证创建 .NET 9 的 MVC 或 Web API 项目把现有项目里最核心的页面或接口迁移过来然后压测对比数据。只有你自己项目里的真实流量、真实数据量、真实依赖关系才能判断升级的价值。远程验证和 HybridCache 都是上手门槛低、见效快的功能可以作为迁移早期验证项。本文里的命令和代码都可以直接复制到本地测试但生产环境务必根据实际业务做二次审查。
返回列表