- 示例工程
【免费下载链接】practical-aspnetcore
Practical samples of ASP.NET Core 11, 10, 9, 8.0, 7.0, 6.0, 5.0, 3.1, 2.2, and 2.1,projects you can use. Readme contains explanations on all projects.
导读
本篇文章基于 practical-aspnetcore 仓库中的new-routing-12示例,讲解在 ASP.NET Core 端点路由体系下,如何从 Razor Page 生成的Endpoint中按请求方法(GET / POST)读取自定义特性(Attribute)元数据。由于 Razor Page 无法像 MVC 那样把特性挂在 Handler 方法上,示例给出了一个"特性挂在 PageModel 类、中间件按 HTTP 动词挑选字段"的额外步骤方案。读完本文,你将掌握HttpContext.GetEndpoint()、Endpoint.Metadata.GetMetadata<T>()以及HttpContext.Items组合使用的方法,能够在自定义中间件中实现与请求动词相关的端点元数据传递。
背景:端点路由中的元数据(Metadata)
ASP.NET Core 3.0 引入端点路由后,每个可执行的请求目标(MVC 控制器动作、Razor Page、MapGet内联委托等)都会被抽象为一个Endpoint对象。Endpoint不仅包含路由匹配信息(如DisplayName),还携带一组元数据(Metadata)——也就是各类Attribute实例的集合。中间件可以通过HttpContext.GetEndpoint()拿到当前请求匹配到的端点,再通过Endpoint.Metadata.GetMetadata<T>()取出挂载在端点上的自定义特性。
本仓库中new-routing-10展示了最基础的一步——在中间件里获取当前Endpoint并检查DisplayName,new-routing-11则展示了用GetMetadata<T>()直接读取特性值。本示例(new-routing-12)在其基础上再进一步:让同一份特性在不同请求方法下返回不同的值。
问题:Razor Page 的 Handler 方法挂不上特性
Razor Page 的请求处理逻辑写在一组约定命名的 Handler 方法里,例如OnGet()、OnPost()。与 MVC 不同,你不能在 Razor Page 的 Handler 方法上直接贴自定义特性,特性只能标注在对应的PageModel类上:
// ❌ Razor Page 中不能这样做 public class AboutModel : PageModel { [Message(ContentGet = "...", ContentPost = "...")] // 不合法 public void OnGet() { } [Message(...)] // 不合法 public void OnPost() { } }对比之下,MVC 控制器可以把特性直接标注在 Action 方法上。仓库中的new-routing-13示例就展示了这一点——HomeController的About()与AboutPost()方法分别贴上了内容不同的[Message(...)]特性(见 Controllers/HomeController.cs),端点路由会为每个 Action 单独生成一个Endpoint,各自的元数据天然区分。
正因 Razor Page 无法走这条捷径,new-routing-12的文档明确指出:"Unlike in MVC, you can't use Attribute from the method of a Razor Page. You can only use it from the Model class. This makes obtaining the appropriate metadata for each request require an extra step."(与 MVC 不同,你不能在 Razor Page 的方法上使用特性,只能在 Model 类上使用,这使得为每个请求获取合适的元数据需要额外的步骤。)
解决方案:特性挂类、中间件按动词挑选
示例采用的思路分三步:
- 自定义特性内置 GET / POST 两个字段,而不是单一值;
- 把特性整体标注在 PageModel 类上(这是 Razor Page 唯一允许的位置);
- 中间件在拿到端点元数据后,根据当前请求的 HTTP 动词,从特性对象中挑选对应的字段放入
HttpContext.Items,页面 Handler 再取出来展示。
这样虽然多了一个"按方法分发"的步骤,但特性值依旧能精准区分 GET 与 POST 两种请求。
源码解读
1. 定义双值特性MessageAttribute
特性定义在 Program.cs 中,与中间件同文件放置:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class MessageAttribute : System.Attribute { public string ContentGet { get; set; } public string ContentPost { get; set; } }要点:
AttributeUsage同时允许标注在类(Class)和方法(Method)上。尽管本示例只用在类上,保留Method目标可以保证该特性在 MVC 场景(如new-routing-13的控制器动作)下同样可用;- 特性用属性(Property)而非构造函数参数承载数据,使用命名参数语法
[Message(ContentGet = "...", ContentPost = "...")]赋值。
2. 在 PageModel 上标注特性
Pages/about.cshtml.cs 是整个示例的核心:
[Message(ContentGet = "This message only shows up on GET", ContentPost = "This message only shows up on POST")] public class AboutModel : PageModel { public string Message { get; set; } public void OnGet() { Message = HttpContext.Items["GreetingFromMiddleWare"] as string; } public void OnPost() { Message = HttpContext.Items["GreetingFromMiddleWare"] as string; } }可以看到:
- 特性标注在
AboutModel类上,这是 Razor Page 唯一合法的挂载点; - GET 与 POST 两个动词对应的文案分别存于
ContentGet、ContentPost; OnGet与OnPost两个 Handler 都从HttpContext.Items["GreetingFromMiddleWare"]读取中间件写入的消息。因为中间件已按动词挑选过文案,这里不需要再区分方法,逻辑保持统一。
3. 中间件按请求方法挑选元数据
GreeterMiddleware(同样定义在 Program.cs)执行关键分发逻辑:
public async Task InvokeAsync(HttpContext httpContext) { Endpoint endPoint = httpContext.GetEndpoint(); var message = endPoint.Metadata.GetMetadata<MessageAttribute>(); if (message != null) { if (httpContext.Request.Method == HttpMethods.Get) httpContext.Items.Add("GreetingFromMiddleWare", message.ContentGet); else if (httpContext.Request.Method == HttpMethods.Post) httpContext.Items.Add("GreetingFromMiddleWare", message.ContentPost); } await _next.Invoke(httpContext); }流程拆解:
httpContext.GetEndpoint():取得当前请求匹配到的端点。由于MapRazorPages()会为每个页面(页面路由 + 各 Handler 组合)生成端点,/about页面的 GET 与 POST 对应的是同一个端点(它们共享同一个PageModel类型),因此拿到的是同一份元数据;endPoint.Metadata.GetMetadata<MessageAttribute>():从端点元数据集合中取出MessageAttribute实例;取不到时返回null(所以调用前要做空值判断);- 按动词分发:通过
httpContext.Request.Method与HttpMethods.Get/HttpMethods.Post常量比较,决定取ContentGet还是ContentPost; - 写入
HttpContext.Items:中间件与页面 Handler 之间通过请求级字典Items传递数据,页面的OnGet/OnPost再原样取出。
中间件构造函数中还注入了LinkGenerator字段(readonly LinkGenerator _linkGenerator),本示例并未实际使用它——这是为了与仓库中其他中间件示例保持结构一致,读者可以忽略或移除。
4. 页面视图与入口页
Pages/about.cshtml 只做一件事——渲染@Model.Message:
@page @model PracticalAspNetCore.AboutModel <html> <body> <h2>@Model.Message</h2> </body> </html>Pages/index.cshtml 提供了两种触发方式,便于验证:
@page <html> <body> <h1>An example of accessing Razor Page attribute values from a middleware using Endpoint class</h1> <ul> <li><a href="/about">/about</a></li> <li><form asp-page="./about"><input type="submit" value="POST /About"/></form></li> </ul> </body> </html>- 第一个链接是常规的
GET /about; - 第二个是
asp-page="./about"表单(该 Tag Helper 通过 Pages/_ViewImports.cshtml 中的@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers启用),点击按钮即向同一页面发起POST /about请求。
因此同一个 URL、同一次中间件逻辑,GET 显示"This message only shows up on GET",POST 显示"This message only shows up on POST",验证了"按请求方法取元数据"的效果。
5. 启动装配
Program.cs 的装配部分非常简洁:
var builder = WebApplication.CreateBuilder(); builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(); var app = builder.Build(); app.UseMiddleware<GreeterMiddleware>(); app.MapRazorPages(); app.Run();- 注册 Razor Pages 与 MVC(含视图)服务;
- 把
GreeterMiddleware挂到管道中(位于MapRazorPages()之前,确保请求先经过中间件); MapRazorPages()为所有页面生成端点。
项目文件 new-routing-12.csproj 的目标框架为net10.0,仓库根目录的 global.json 要求 .NET SDK10.0.0(rollForward: major,允许预发布版)。
运行与验证
在仓库根目录执行:
dotnet restore projects/endpoint-routing/new-routing-12/new-routing-12.csproj dotnet run --project projects/endpoint-routing/new-routing-12/new-routing-12.csproj启动后:
- 浏览器访问
/,点击/about链接(GET 请求),页面显示This message only shows up on GET; - 返回首页,点击
POST /About按钮(表单 POST 请求),页面显示This message only shows up on POST。
两次请求命中同一个/about端点,输出却随动词变化——这正是本文所述"额外步骤"带来的按请求方法分发的元数据能力。
与仓库其他端点元数据示例的对照
| 示例 | 主题 | 与本文的关系 |
|---|---|---|
| new-routing-10 | 中间件中用GetEndpoint()检查DisplayName | 前置基础:拿到端点对象 |
| new-routing-11 | 用GetMetadata<T>()读取单一特性值 | 本文的基础版:单一Content字段,不区分动词 |
| new-routing-13 | MVC 控制器在 Action 方法上贴特性 | 对照版:MVC 天然按方法区分,无需额外步骤 |
| new-routing-14 | 通过IEndpointFeature获取Endpoint | GetEndpoint()的等价实现路径 |
| new-routing-15 | 用WithMetadata给内联中间件附加元数据 | 另一种"生产"元数据的途径 |
其中new-routing-11与本文差异最值得对比:前者只定义一个Content属性,中间件取到后直接使用;后者将属性拆为ContentGet/ContentPost,并在中间件里根据Request.Method分发。这正是 Razor Page 单端点、多 Handler 结构下,弥补"特性不能挂 Handler 方法"限制的实用模式。
小结
new-routing-12完整演示了在 Razor Page 场景下按请求方法读取端点元数据的可行路径:
- 约束认知:Razor Page 的特性只能标注在
PageModel类上,无法像 MVC 那样细化到 Handler 方法; - 解决手法:特性内用多个属性对应多个动词,中间件里用
GetEndpoint()+Metadata.GetMetadata<T>()取出特性,再以Request.Method决定取哪个字段; - 数据传递:通过
HttpContext.Items把结果从中间件安全地送到页面 Handler,保持页面逻辑与路由/元数据解耦。
这一模式同样适用于鉴权信息、A/B 文案、功能开关等按动词区分、又希望集中在管道层统一处理的元数据场景,是理解 ASP.NET Core 端点路由与元数据机制的优质实战范例。
- 示例工程
【免费下载链接】practical-aspnetcore
Practical samples of ASP.NET Core 11, 10, 9, 8.0, 7.0, 6.0, 5.0, 3.1, 2.2, and 2.1,projects you can use. Readme contains explanations on all projects.
相关推荐
使用 IEndpointConventionBuilder.WithMetadata 为 ASP.NET Core 端点附加元数据:practical-aspnetcore 端点路由实战
使用 IEndpointConventionBuilder.WithMetadata 为 ASP.NET Core 端点附加元数据:practical aspn
示例工程practical-aspnetcore 路由实战:使用 Razor Pages Areas 组织多区域站点
practical aspnetcore 路由实战:使用 Razor Pages Areas 组织多区域站点 本篇以 practical aspnetcore
示例工程practical-aspnetcore 实战:在 ASP.NET Core 端点路由中直接用 `Map` 映射 RequestDelegate 处理无专用辅助方法的请求
practical aspnetcore 实战:在 ASP.NET Core 端点路由中直接用 Map 映射 RequestDelegate 处理无专用辅助方法
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考