我想寫程式捕捉 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 得知 (不用看得很深入就可以感覺出來)

Post a comment