- 示例工程
【免费下载链接】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 仓库中的
projects/endpoint-routing/new-routing-10示例,讲解如何在自定义中间件中通过HttpContext.GetEndpoint()扩展方法获取当前正在执行的 Endpoint 对象,并据此实现"按端点定制行为"的中间件逻辑。读完本文,你将掌握 Endpoint 与中间件的关系、GetEndpoint()的调用方式与空值防护、以及如何借助Endpoint.Metadata和IEndpointFeature做更深入的路由内省。
背景:Endpoint Routing 时代,中间件如何"看见"路由
从 ASP.NET Core 3.0 引入端点路由(Endpoint Routing)开始,应用的请求处理被统一抽象为"匹配 Endpoint → 执行 Endpoint"两个阶段。中间件(Middleware)位于请求管线之中,在UseRouting之后、UseEndpoints之前执行时,已经能够感知"这条请求最终会命中哪个 Endpoint"。
在旧模型下,中间件想要知道请求对应哪个路由、哪个控制器或页面,往往需要自己解析路径、手动查路由表;而在端点路由模型下,框架已经替你完成了匹配,你只需要通过HttpContext.GetEndpoint()把匹配结果"取出来"即可。
new-routing-10 示例 的核心就是这一行 API:
Use the brand new
HttpContext.GetEndPointextension method to examine the current endpoint that is being executed.
(注:README 中写作GetEndPoint,实际源码中 API 名称为GetEndpoint(),使用时应以代码为准。)
示例项目全景:一个"打招呼"的中间件
示例项目位于 projects/endpoint-routing/new-routing-10,结构非常精简:
new-routing-10/ ├── Pages/ │ ├── about.cshtml # 展示中间件写入的问候语 │ └── index.cshtml # 首页,提供指向 /about 的链接 ├── Program.cs # 应用装配 + GreeterMiddleware 实现 ├── README.md └── new-routing-10.csproj项目目标:当请求命中/about这个 Razor Page 端点时,GreeterMiddleware在转发请求之前向HttpContext.Items写入一条问候语,about.cshtml再把这条问候语渲染到页面上。判断"是否命中/about"的手段,正是从HttpContext中取出当前 Endpoint 并检查其DisplayName。
目标框架与编译配置
new-routing-10.csproj 内容如下:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>true</ImplicitUsings> <LangVersion>preview</LangVersion> </PropertyGroup> </Project>需要说明的适用前提:
- 示例面向net10.0,并启用了
LangVersion=preview,这是该仓库实验性示例的常见配置; - 本项目使用了隐式
using(ImplicitUsings),因此Endpoint、RequestDelegate等类型无需显式引入命名空间; HttpContext.GetEndpoint()扩展方法自 ASP.NET Core 3.0 起便已可用,并非 .NET 10 独占,只是该示例在 net10.0 环境下演示。
核心 API:HttpContext.GetEndpoint() 是什么
HttpContext.GetEndpoint()是定义在Microsoft.AspNetCore.Http命名空间下的扩展方法,签名形如:
public static Endpoint? GetEndpoint(this HttpContext context);它从当前请求上下文的特性集合(Features)中读取端点路由写入的IEndpointFeature,返回与该请求匹配的Endpoint对象。返回值是可空的(Endpoint?):当请求没有匹配到任何端点(例如未经UseRouting匹配、或匹配失败进入后续未映射的处理)时,返回null。
Endpoint对象携带的关键信息包括:
| 成员 | 含义 | 本例中的值 |
|---|---|---|
DisplayName | 端点的显示名称 | Razor Page/about对应"/about" |
Metadata | 端点元数据集合(EndpointMetadataCollection) | 可存放MessageAttribute等自定义元数据 |
RequestDelegate | 端点最终执行的委托 | 由 Razor Pages / MVC 映射时自动设置 |
在端点路由模型中,Razor Page 通过@page指令声明路由模板,页面about.cshtml的@page指令没有指定模板,因此其路由默认为页面路径/about,DisplayName也即"/about"。
完整代码拆解:GreeterMiddleware 如何按端点分流
Program.cs 完整代码如下:
var builder = WebApplication.CreateBuilder(); builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(); var app = builder.Build(); app.UseMiddleware<GreeterMiddleware>(); app.MapRazorPages(); app.Run(); public class GreeterMiddleware { RequestDelegate _next; readonly LinkGenerator _linkGenerator; public GreeterMiddleware(RequestDelegate next, LinkGenerator linkGenerator) { _next = next; _linkGenerator = linkGenerator; } public async Task InvokeAsync(HttpContext httpContext) { Endpoint endPoint = httpContext.GetEndpoint(); if (endPoint.DisplayName == "/about") { httpContext.Items.Add("GreetingFromMiddleWare", "Hello world from GreetingMiddleware"); } await _next.Invoke(httpContext); } }逐段说明其工作原理:
- 服务注册:
AddControllersWithViews()与AddRazorPages()同时注册,说明本示例是"Razor Pages + MVC(带视图)"的混合装配,与仓库中 new-routing-18(仅 MVC 不带 Razor Pages) 等示例形成对照; - 中间件注册:
app.UseMiddleware<GreeterMiddleware>()将自定义中间件插入管线。由于.NET 10的WebApplication会自动在管线前段加入路由中间件,因此中间件内调用GetEndpoint()时,路由匹配已完成,可以拿到当前端点; - 端点映射:
app.MapRazorPages()把Pages目录下的 Razor Page 映射为端点。中间件注册在端点映射之前,这并不影响GetEndpoint()的使用——端点路由中间件已经提前完成了匹配; - 构造函数注入:中间件通过构造函数注入了
LinkGenerator(用于在端点路由中生成链接的服务)。本例的InvokeAsync中并未实际使用它,属于示例保留字段;真正起作用的是_next委托与GetEndpoint()的调用; - 端点分流:
endPoint.DisplayName == "/about"判断当前端点是否为 about 页面。命中时向HttpContext.Items写入键"GreetingFromMiddleWare",值为"Hello world from GreetingMiddleware"; - 继续转发:
await _next.Invoke(httpContext)将请求交给管线后续环节(此处即 Razor Pages 端点执行器)。
⚠️ 空值防护提醒:示例代码直接访问endPoint.DisplayName而未判空。在生产代码中,GetEndpoint()在请求未匹配任何端点时返回null,推荐写成:
var endpoint = httpContext.GetEndpoint(); if (endpoint?.DisplayName == "/about") { // 仅当命中 /about 端点时执行的逻辑 }页面层:问候语如何被渲染出来
首页 index.cshtml
index.cshtml 只做一件事:给出指向/about的入口链接,便于人工验证中间件行为:
@page <html> <body> <h1>An example of accessing Endpoint class from a middleware</h1> <ul> <li><a href="/about">/about</a></li> </ul> </body> </html>访问首页根路径/时,路由命中的是 index 页面端点,其DisplayName不是"/about",因此中间件不会写入问候语——这正是"按端点分流"的直观体现:同一中间件对不同端点表现出不同行为。
about 页面 about.cshtml
about.cshtml 从HttpContext.Items读取中间件写入的值并渲染:
@page <html> <body> <h2>@HttpContext.Items["GreetingFromMiddleWare"]</h2> </body> </html>HttpContext.Items是一个IDictionary<object, object?>,存活于单次请求生命周期内,非常适合中间件与页面/控制器之间传递轻量数据。
运行验证
在项目目录下执行:
dotnet run然后依次访问:
http://localhost:5000/→ 首页无问候语;http://localhost:5000/about→ 页面渲染出Hello world from GreetingMiddleware,证明中间件成功通过GetEndpoint()识别出了/about端点并写入了数据。
(具体端口以dotnet run实际输出为准。)
另一种等价实现:通过 IEndpointFeature 获取 Endpoint
GetEndpoint()本质上是特性的"语法糖"。仓库中的 new-routing-14 示例 展示了等价的底层写法:
using Microsoft.AspNetCore.Http.Features; var builder = WebApplication.CreateBuilder(); builder.Services.AddControllersWithViews(); var app = builder.Build(); app.MapGet("/", async context => { var feature = context.Features.Get<IEndpointFeature>(); await context.Response.WriteAsync($"Endpoint Name {feature.Endpoint.DisplayName}"); }); app.Run();两种方式的关系:
context.Features.Get<IEndpointFeature>()直接读取特性集合中的IEndpointFeature,随后访问其Endpoint属性;httpContext.GetEndpoint()是封装了上述读取过程的扩展方法,代码更简洁;- 两者获取到的是同一个
Endpoint实例。当没有匹配端点时,GetEndpoint()返回null,而IEndpointFeature同样可能为null或其Endpoint为null,使用前都需要判空。
从源码结构看,GetEndpoint()的实现即是对Features.Get<IEndpointFeature>()?.Endpoint的简化包装——这也是它在 README 中被冠以 "brand new"(当时新引入)扩展方法的原因。
进阶:从 Endpoint 读取 Metadata,而不是硬编码 DisplayName
示例中通过比较DisplayName字符串来决定分支,这在端点较少时直观可用,但字符串比较脆弱(端点显示名可能因映射方式不同而变化)。更稳健的做法是从Endpoint.Metadata读取自定义元数据。
仓库中的 new-routing-11 示例 展示了这一模式:定义一个MessageAttribute,在中间件中通过endPoint.Metadata.GetMetadata<MessageAttribute>()取出,若存在则写入问候语:
Endpoint endPoint = httpContext.GetEndpoint(); var message = endPoint.Metadata.GetMetadata<MessageAttribute>(); if (message != null) { httpContext.Items.Add("GreetingFromMiddleWare", message.Content); } await _next.Invoke(httpContext);两者对比:
| 方案 | 判定方式 | 优点 | 局限 |
|---|---|---|---|
| new-routing-10 | DisplayName == "/about" | 代码最少,直观看懂 | 字符串耦合,端点改名即失效 |
| new-routing-11 | Metadata.GetMetadata<T>() | 类型安全、可携带任意数据 | 需要先定义 Attribute 并注册到端点 |
将两者结合是最佳实践:先用GetEndpoint()拿到端点,再用Endpoint.Metadata做类型化的"按端点定制行为",这也是仓库中 new-routing-12(Razor Page 按请求方法取元数据) 与 new-routing-13(MVC Controller 端点元数据) 继续深化的方向。
使用建议与注意事项
- 判空是必须的:
GetEndpoint()可能返回null(未匹配任何端点时),示例代码为保持精简未判空,生产代码务必加上; - 调用时机:只有请求经过端点路由中间件完成匹配后,
GetEndpoint()才能取到值。在WebApplication(Minimal Hosting)模板下路由中间件自动装配,自定义中间件位于其后即可正常使用;若使用传统的IApplicationBuilder显式装配,需确保app.UseRouting()在自定义中间件之前执行; - DisplayName 的来源:Razor Page 的
DisplayName与其@page路由模板相关(@page无参时即页面路径,如/about);MVC 端点的DisplayName则形如ControllerName.ActionName。跨类型比较时要留意差异; - 中间件顺序影响行为:若把
UseMiddleware<GreeterMiddleware>()放在会短路请求的中间件之后,可能永远执行不到GetEndpoint()的分支逻辑; - 与端点执行器的关系:
GetEndpoint()只是"读取匹配结果",并不会改变端点路由的执行流程;真正执行业务逻辑仍需await _next.Invoke(httpContext)将控制权交给后续管线(本例中即 Razor Pages 端点)。
总结
HttpContext.GetEndpoint()是端点路由模型中连接"中间件世界"与"路由世界"的关键桥梁。通过 new-routing-10 这个最小示例,你可以看到一条清晰的实践路径:
- 注册中间件 → 2. 在
InvokeAsync中调用httpContext.GetEndpoint()→ 3. 依据DisplayName或Metadata判定当前端点 → 4. 写入HttpContext.Items等共享数据 → 5. 转发请求让端点执行。
结合仓库中的 new-routing-11(元数据方案)、new-routing-14(IEndpointFeature底层实现)、new-routing-12 与 new-routing-13(Razor Page / MVC 元数据进阶),你可以把这一技巧从"示例"扩展为"面向生产的分端点中间件设计模式"——无论是做按端点注入响应头、权限预处理,还是针对特定页面注入统计逻辑,GetEndpoint()都是那个正确的起点。
- 示例工程
【免费下载链接】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.
相关推荐
react-admin 中 useGetRecordId 用法详解:从 Context 与路由中获取当前记录 ID
react admin 中 useGetRecordId 用法详解:从 Context 与路由中获取当前记录 ID useGetRecordId 是 react
前端UI组件Meshery 中的 AWS Load Balancer Controller 集成模型:组件解析、功能特性与工作原理
Meshery 中的 AWS Load Balancer Controller 集成模型:组件解析、功能特性与工作原理 导读 本文围绕 Meshery 文档仓库
云原生微服务运维DevOpspractical-aspnetcore 端点路由实战:用 MapDefaultControllerRoute 只启用 MVC 与默认约定路由
practical aspnetcore 端点路由实战:用 MapDefaultControllerRoute 只启用 MVC 与默认约定路由 本文基于 pra
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考