MVC 路由將 URL 對應至 Controller/Action,但當「同一 URL 需根據用戶角色回傳不同 View」時,傳統做法在 Action 內部 if/else 判斷角色並 return View("ViewName"),違反開放封閉原則。本文介紹 ActionMethodSelectorAttribute 讓路由引擎根據自訂條件自動選擇正確 Action 方法。
URL 皆為 /Book/List,不想在單一 Action 中寫大量分支邏輯。
ActionMethodSelectorAttribute 抽象類別提供 IsValidForRequest(ControllerContext, MethodInfo) 方法,路由引擎在動作選擇階段呼叫,回傳 true 表示該 Action 符合目前請求。
using System;
using System.Reflection;
using System.Web.Mvc;
namespace MyProject.ActionSelectors
{
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class OnlyInRoleAttribute : ActionMethodSelectorAttribute
{
private readonly string _role;
public OnlyInRoleAttribute(string role)
{
_role = role ?? throw new ArgumentNullException(nameof(role));
}
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
if (controllerContext == null) throw new ArgumentNullException(nameof(controllerContext));
return controllerContext.HttpContext.User.IsInRole(_role);
}
}
}
搭配 [ActionName("List")] 將不同方法對應至相同 Action 名稱:
public class BookController : Controller
{
[OnlyInRole("Admin")]
[ActionName("List")]
public ActionResult ListForAdmin()
{
var books = _bookService.GetAllBooks();
return View("List_Admin", books);
}
[OnlyInRole("Client")]
[ActionName("List")]
public ActionResult ListForClient()
{
var books = _bookService.GetAvailableBooks();
return View("List_Client", books);
}
[OnlyInRole("Agent")]
[ActionName("List")]
public ActionResult ListForAgent()
{
var books = _bookService.GetAgentBooks(User.Identity.Name);
return View("List_Agent", books);
}
// 預設 fallback(無角色或角色不符)
[ActionName("List")]
public ActionResult ListDefault()
{
return View("List_Guest", _bookService.GetFeaturedBooks());
}
}
/Book/List。BookController 所有 [ActionName("List")] 方法。OnlyInRoleAttribute.IsValidForRequest。true 的方法被選中執行。false,拋出 HttpException 404(或落入無屬性的 fallback 方法)。| 優點 | 注意事項 |
|---|---|
| Action 專注單一角色邏輯,符合 SRP | 角色檢查發生在 Authorization 階段之後,確保 User 已驗證 |
| 無需在 Action 內寫 if/else,易測試 | 方法數隨角色增加,建議搭配共用私有方法減少重複 |
| URL 保持乾淨,無需查詢字串區分 | 順序敏感:先宣告的優先匹配,fallback 放最後 |
| 支援單元測試模擬 ControllerContext | 複雜條件建議改用 Policy-based Authorization (ASP.NET Core) |
可建立 WhenAttribute 接受 Func<ControllerContext, bool>,支援任意條件:
public class WhenAttribute : ActionMethodSelectorAttribute
{
private readonly Func<ControllerContext, bool> _predicate;
public WhenAttribute(Func<ControllerContext, bool> predicate) { _predicate = predicate; }
public override bool IsValidForRequest(ControllerContext cc, MethodInfo mi) => _predicate(cc);
}
// 使用範例
[When(c => c.HttpContext.Request.IsAjaxRequest())]
[ActionName("List")]
public ActionResult ListAjax() { return PartialView("_ListPartial"); }
ActionMethodSelectorAttribute 是 MVC 管線中鮮為人知但極具威力的擴展點,能將「請求條件判斷」從 Action 移至框架層,實現真正的關注點分離。雖 ASP.NET Core 推薦使用 IActionConstraint 與 Policy-based Authorization,但此模式在 MVC 5 維護專案中依然實用且優雅。