自制 .NET Core 路由调试中间件
导语
本文教大家如何在 .NET Core 应用中使用中间件输出路由信息以便调试程序。
背景
在 .NET Framework 的上古时代,有个叫做 RouteDebugger 的神器,可以在 MVC 或 Web API 应用中输出当前页面的路由信息,也可查看应用中注册的所有路由信息。它的 NuGet 包(routedebugger)最新版是 2.1.5,更新于 2016年,源于 Phil Haack 大神12年前的文章 https://haacked.com/archive/2008/03/13/url-routing-debugger.aspx
这个包可以非常直观的在浏览器访问应用的时候,直接在页面最下方输出当前的路由信息以及全部的路由表。以便于在复杂的应用中帮助程序员摆脱 996。
.NET Core 怎么办
12年后,已经是 .NET Core 的天下了,显然由于运行机制的不同,.NET Core 无法使用 RouteDebugger 或者改造它的代码,只能重写。
虽然社区已经有人写了一份 AspNetCoreRouteDebugger 但是这个项目有几个明显的问题:
https://github.com/ardalis/AspNetCoreRouteDebugger
使用不方便
项目需要用户手工拷贝它的两个文件 Routes.cshtml,Routes.cshtml.cs 到自己的工程。并且要自己修改命名空间、做访问限制等。你以为是 996 的结束,其实是 007 的开始。
另外,项目默认提供的是 Razor Page 方案,在不使用 Razor Page 的项目里,还需要继续手工拷它的 Routes2Controller 去使用。
只能输出全部路由
原版 RouteDebugger 解决的最重要的问题之一就是输出当前页面的路由,因为不是每个公司都按照 MVC 的默认 convention 做项目,很可能URL和 Controller / View 对应不起来,程序员接到需求要改代码很可能找不到改哪个 Action,所以才用 RouteDebugger。而该 .NET Core 项目只能输出全部路由表而不是当前页面的路由,使用场景很有限。
没有 NuGet 包
一旦项目有更新,用户必须时刻关注作者 GitHub 才行,并需要手工更新代码,非常不方便。
.NET Corelish
既然用了 .NET Core,就要用出精髓。.NET Core 的精髓之一在于中间件(Middleware),而获取路由信息并输出,显然最适合用中间件去做,以尽可能的对业务代码实现 0 侵入。
不要输出到页面末尾
在用户的页面末尾输出debug信息,看上去很方便,但实际项目中在极端场景下,可能会破坏页面的功能和显示样式,尤其是页面加载了三方统计、样式修改插件等。更别说遇到SPA项目了,页面呈现的原理都不一样,所以输出到页面这套方法已经过时。
目前我觉得,输出到 HTTP Response Header 是更好的做法,不仅不会影响页面功能及显示效果,对工具(CURL、浏览器F12)等支持也更加友好。
JSON over HTML Table
Json 是当今世界的政治正确,它比起 HTML Table,更面向程序员,有更好的工具适配。因此不管是输出当前路由还是全部路由表,我都选择了 JSON 格式。
自主研发成功
如果你并不想了解怎么写,只想拿来就用的话,我已将工程打包成 NuGet 包,并开源:
https://github.com/EdiWang/AspNetCore-RouteDebuggerMiddleware
dotnet add package Edi.RouteDebugger
只要添加到自己应用的请求管线即可,推荐仅用于开发环境:
if (env.IsDevelopment())
{
app.UseRouteDebugger();
}
这里要注意顺序,ASP.NET Core 的中间件顺序有讲究,得写在 app.UseEndpoints() 及 app.UseRouting() 的上面。具体原因可参考微软官方文档对中间件的介绍:
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/
接下来,打开应用中任意一个页面,只要它有路由信息,就能在 Header 里看见:
若要查看全部路由表,访问 "/route-debugger" 即可:
代码解析
想要获取当前请求的路由信息,只要调用 HttpContext 对象的 GetRouteData() 方法即可。然后序列化为 Json 输出到 Response Header。
由于我们得先执行 _next(context) 才能得到非空的路由信息,而这部操作会导致 HTTP Reponse已经开始输出从而导致 Header 只读,所以需要一些 workaround 来设置 Header。最终代码如下:
private async Task SetCurrentRouteInfo(HttpContext context)
{
var originalBodyStream = context.Response.Body;
await using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
await _next(context);
context.Response.Body.Seek(0, SeekOrigin.Begin);
await new StreamReader(context.Response.Body).ReadToEndAsync();
context.Response.Body.Seek(0, SeekOrigin.Begin);
var rd = context.GetRouteData();
if (null != rd && rd.Values.Any())
{
var rdJson = JsonSerializer.Serialize(rd.Values);
context.Response.Headers["current-route"] = rdJson;
}
await responseBody.CopyToAsync(originalBodyStream);
}
获取全部路由信息得注入一个 IActionDescriptorCollectionProvider 对象,好在 Middleware class 的构造函数可以直接无脑注入。
public async Task Invoke(HttpContext context, IActionDescriptorCollectionProvider provider = null)
{
if (context.Request.Path == "/route-debugger")
{
if (null != provider)
{
var routes = provider.ActionDescriptors.Items.Select(x => new {
Action = x.RouteValues["Action"],
Controller = x.RouteValues["Controller"],
Name = x.AttributeRouteInfo?.Name,
Template = x.AttributeRouteInfo?.Template,
Contraint = x.ActionConstraints
}).ToList();
var routesJson = JsonSerializer.Serialize(routes);
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(routesJson, Encoding.UTF8);
}
else
{
await context.Response.WriteAsync("IActionDescriptorCollectionProvider is null", Encoding.UTF8);
}
}
else
{
await SetCurrentRouteInfo(context);
}
}
目前这个 .NET Core 版的 RouteDebugger 功能还非常基础,非常欢迎大家提建议或PR。
汪宇杰博客
.NET | Azure | 微软MVP