網友發問,ASP.NET MVC ActionFilter 可攔截檢查 MVC Action 執行結果,若 Action 傳回 return RedirectToAction("..."),ActionFilter 能否取得導向的 URL?

以下列程式為例,目標是在 TestActionFilter 取得 "/Home/Redirected":

using System.Web.Mvc;
using WebApplication1.Models;

namespace WebApplication1.Controllers
{
    public class HomeController : Controller
    {
        // GET: Home
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult Redirected()
        {
            return Content("Redirected");
        }

        [TestActionFilter]
        public ActionResult Redirect()
        {
            return RedirectToAction("Redirected");
        }
    }
}

依 ActionFilter 運作原理,覆寫 OnActionExecuted() 後可由 ActionExecutedContext 的 Result 屬性取得 Action return 傳回內容。return RedirectToAtion() 時傳回型別為 RedirectToRouteResult,因此第一步是在 ActionFilter 加入邏輯,偵測 Result 型別為 RedirectToRouteResult 進行特別處理。RedirectToRouteResult 與 URL 路徑有關的屬性為 RouteName 及 RouteValues,土法鍊鋼自己推算是種解法,但依循程式原有邏輯更穩當。於是我開了 JustDecompile 反組譯觀察 RedirectToRouteResult.ExecuteResult() 方法:

public override void ExecuteResult(ControllerContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException("context");
    }
    if (context.IsChildAction)
    {
        throw new InvalidOperationException(MvcResources.RedirectAction_CannotRedirectInChildAction);
    }
    string str = UrlHelper.GenerateUrl(this.RouteName, null, null, this.RouteValues, this.Routes, context.RequestContext, false);
    if (string.IsNullOrEmpty(str))
    {
        throw new InvalidOperationException(MvcResources.Common_NoRouteMatched);
    }
    context.Controller.TempData.Keep();
    if (this.Permanent)
    {
        context.HttpContext.Response.RedirectPermanent(str, false);
        return;
    }
    context.HttpContext.Response.Redirect(str, false);
}

internal RouteCollection Routes
{
    get
    {
        if (this._routes == null)
        {
            this._routes = RouteTable.Routes;
        }
        return this._routes;
    }
    set
    {
        this._routes = value;
    }
}

由以上程式碼得知 ExecuteResult() 內部是用 UrlHelper.GenerateUrl() 將 RouteName、RouteValues 轉成 URL,但還需要 this.Routes、context.RequestContext 兩個額外參數;ActionExecutedContext 也有 RequestContext,但 Routes 是 RedirectToRouteResult 的內部屬性無法動用,所幸再追進去發現 Routes 的來源是 RouteTable.Routes 為公開靜態屬性,這樣子要模擬 RedirectToRouteResult 計算 URL 就沒問題了。

綜合以上分析,最後 ActionFilter 會像這樣:

using NLog;
using System.Web.Mvc;
using System.Web.Routing;

namespace WebApplication1.Models
{
    public class TestActionFilterAttribute : ActionFilterAttribute
    {
        static ILogger logger = NLog.LogManager.GetLogger("Trace");
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            //由Result取出Action回傳結果
            var result = filterContext.Result;
            //檢查是否型別為 RediretToRouteResult
            if (result is RedirectToRouteResult)
            {
                var redirRouteRes = result as RedirectToRouteResult;
                //使用 UrlHelper 依 RedirectToRouteResult.RouteValues 計算出 URL
                string url = UrlHelper.GenerateUrl(
                    redirRouteRes.RouteName, null, null, redirRouteRes.RouteValues, 
                    RouteTable.Routes, filterContext.RequestContext, false);
                logger.Trace($"RedirectToRoute: {url}");
            }
            base.OnActionExecuted(filterContext);
        }
    }
}

執行 /Home/Redriect 後得到 NLlog 記錄如下,測試成功:

2019-10-13 11:12:46.7169 TRACE RedirectToRoute: /Home/Redirected

假日暖身伸展完畢!

Example of how to read RedirectToAtion url from ActionFilter in ASP.NET MVC


Comments

Be the first to post a comment

Post a comment