在 ASP.NET Core Middleware 蒐集 Razor Pages 與 MVC 的方法資訊
1 |
我想寫程式捕捉 AJAX 呼叫 Razor Pages OnPostXXXX() 的執行錯誤(延伸閱讀: Razor Pages 實作 Ajax 呼叫,出現時統一回傳 ApiError JSON (延伸閱讀:範例教學:使用 ASP.NET MVC 打造 WebAPI 服務)。查了一下,Razor Pages 不像 MVC 有 IExceptionFilter 可用,似乎只能從自訂 Middleware 下手。(延伸閱讀: ASP.NET Core 基礎 - Middleware)。
試寫了一個 RazorPagePostExceptionMiddleware,出錯時導入自訂流程,檢查若為 POST Request、有 handler 參數而且包含 XHR 送出的 Header 註記,就當成它是 Razor Page 頁面的 AJAX 呼叫,統一回傳 ApiError 物件的 JSON 內容,若不是則直接 throw 交給原有的錯誤機制處理:
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;
namespace GitSidekick.Models
{
public class RazorPagePostExceptionMiddleware
{
private readonly RequestDelegate _next;
public RazorPagePostExceptionMiddleware(RequestDelegate next)
{
this._next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
if (context.Request.Method == "POST" &&
//包含 handler 參數
!string.IsNullOrEmpty(context.Request.Query["handler"]) &&
//是由 XHR 發出的 AJAX 呼叫
context.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
context.Response.StatusCode = 200;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(
JsonConvert.SerializeObject(new ApiError("5001", ex.Message)));
return;
}
throw;
}
}
}
}
同一網站我也啟用了 MVC (延伸閱讀:ASP.NET Core 練習 - 在 Razor Pages 專案加入 MVC),透過 POST + handler 參數 + XHR 發送三個條件過濾,理論上不會有 Controller Action 亂入,可 99.99% 滿足規格,但我對「如何在 Middleware 中依據 HttpContext 判別是 MVC 還是 Razor Pages?有沒有可能挖出 MVC/Razor Page 的 Action 名稱、Attribute 等資訊?」這個議題十分感興趣,便再繼續挖掘。
我找到 HttpContext.GetEndPoint().Metadata 藏了許多資訊,依 Request 是 Razor Pages 或 MVC 而有明顯差異:(下圖黃色部分是 Razor Page、橘色為 MVC)
單由 Microsoft.AspNetCore.Mvc.Filters.ControllerActionFilter、Microsoft.AspNetCore.Mvc.PageHandlerFilter 就足以達成區別 MVC 或 Razor Pages 的目的,進一步我再看到有趣的東西,MVC 的第二條 Metadata 顯示為 "GitSidekick.Controllers.WebApiController.Index,其型別為 Microsoft.AspNetCore.Mvc.ControllerActionDescriptor,以前寫 MVC 時曾用過 ActionDescriptor 讀取 Action 上 Attribute,在 .NET Core 應能如法炮製。
我寫了一個簡單的 Middleware,在 GetEndPoint().Metadata 尋找 ControllerActionDescriptor,找到後由 MethodInfo.GetCustomAttribute 調閱 Actction [Description("...")] 的內容:
app.Use(async (ctx, next) =>
{
if (ctx.Request.Query["hook"] == "Y")
{
var metadata = ctx.GetEndpoint().Metadata;
var ctlActDesc =
(Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)
metadata.SingleOrDefault(o => o is Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor);
if (ctlActDesc != null)
{
var desc = ctlActDesc.MethodInfo.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault()
as DescriptionAttribute;
if (desc != null)
{
await ctx.Response.WriteAsync(desc.Description);
return;
}
}
}
await next.Invoke();
});
測試成功!
那... Razor Pages 呢?
我沒能找到相關文件,但 Razor Page 的 HttpContext.GetEndpoint().Metadata 裡有一個 CompiledPageActionDescriptor 型別,其中包含 HandlerMethods 集合,會對映到 PageModel 中的 OnGet、OnPostXXX。將以上的程式稍作調整,改由 Metadata 取出 Microsoft.AspNetCore.Mvc.RazorPages.CompiledPageActionDescriptor,將 HandlerMethods 轉成 Dictionary<string, MethodInfo> 方便與 PageModel 方法對照:
var pageActDesc =
(Microsoft.AspNetCore.Mvc.RazorPages.CompiledPageActionDescriptor)
metadata.SingleOrDefault(o => o is Microsoft.AspNetCore.Mvc.RazorPages.CompiledPageActionDescriptor);
if (pageActDesc != null)
{
var handlers = pageActDesc.HandlerMethods.ToDictionary(
o => $"HttpMethod:{o.HttpMethod} Name:{o.Name}",
o => o.MethodInfo);
}
執行結果如下:
HandlerMethods 共有一個 GET 方法,兩個 POST 方法,Name 分別為 SelGitRepo 與 ListBranches,於 PageModel 中的 OnGet、OnPostSelGitRepo、OnPostListBranches,取得其對映的 MethodInfo,即可像 MVC 一樣 GetCustomAttribute() 利用 Attribute 控制在 Middleware 層執行不同邏輯,或是蒐集輸入參數資訊等。至於當下的 Request 是套用哪一個 Handler?我沒找到資訊來源,想到的做法是由 Request 的 HttpMethod 與 handler 參數推測。
掌握這些資訊,在撰寫 ASP.NET Core Middleware 可取得 Razor Pages/MVC 當下的執行方法資訊,實現一些進階應用。
Study of how to get handler action of Razor Pages or action of MVC in ASP.NET Core middleware.
Comments
# by 凱大
關於 Razor Page 基本上是透過把 CompiledPageActionDescriptor 轉換成 Endpoint 而轉換成 endpoint 之後作法就會與一般的endpoint 相同 至於 route data 要怎麼對應 不外乎就是 Naming Convention (這是M$的最愛之一) 而且做法其實與MVC 非常相近 這些都可以透過他們提供的 source code 得知 (不用看得很深入就可以感覺出來)