ASP.NET MVC 核心設計原則是 **「慣例優於配置」** 與 **「全管線可替換」**。Simone Chiaretta 整理的 13 大擴展點涵蓋從請求進入到視圖輸出的完整管線,掌握這些點即掌握 MVC 框架的靈魂。以下逐一深度解析並附實務應用場景。
介面:`IRouteConstraint` (`bool Match(HttpContext, Route, string, RouteValueDictionary, RouteDirection)`)
應用:複雜參數驗證 (如日期有效性、跨參數關聯驗證)。
public class ValidDateConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (values.TryGetValue("year", out var y) &&
values.TryGetValue("month", out var m) &&
values.TryGetValue("day", out var d))
{
return DateTime.TryParse($"{y}-{m}-{d}", out _);
}
return false;
}
}
// 路由註冊
routes.MapRoute("Archive", "archive/{year}/{month}/{day}",
new { controller = "Blog", action = "Archive" },
new { year = @"\d{4}", month = @"\d{2}", day = @"\d{2}" }, // 正則
new[] { "MyApp.Controllers" });
// 或使用約束物件
routes.MapRoute("Archive", "archive/{year}/{month}/{day}",
new { controller = "Blog", action = "Archive" },
new { year = new ValidDateConstraint() });
介面:`IRouteHandler` (`IHttpHandler GetHttpHandler(RequestContext)`)
應用:將特定路由導向非 MVC 處理器 (Web Forms、靜態檔案、SignalR、自訂 HttpHandler)。
public class LegacyAspxRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
string virtualPath = "~/Legacy/Pages/" + requestContext.RouteData.Values["page"] + ".aspx";
return BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(Page)) as IHttpHandler;
}
}
// 註冊
routes.Add(new Route("legacy/{page}", new LegacyAspxRouteHandler()));
抽象類:`DefaultControllerFactory` / 介面 `IControllerFactory`
應用:依賴注入 (IoC) 整合、多租戶控制器解析、動態載入外掛控制器。
// 整合 Autofac / Unity / Ninject / 建構子注入
public class IoCControllerFactory : DefaultControllerFactory
{
private readonly IContainer _container;
public IoCControllerFactory(IContainer container) => _container = container;
protected override IController GetControllerInstance(RequestContext context, Type controllerType)
{
if (controllerType == null) return null;
return (IController)_container.Resolve(controllerType); // IoC 容器建構並注入依賴
}
}
// Application_Start
ControllerBuilder.Current.SetControllerFactory(new IoCControllerFactory(container));
類別:`ControllerActionInvoker`
應用:自訂動作選擇邏輯、參數繫結規則、Filter 執行順序、非同步動作包裝。
public class CustomActionInvoker : ControllerActionInvoker
{
protected override ActionDescriptor FindAction(ControllerContext context, ControllerDescriptor controller, string actionName)
{
// 例:支援動作名稱別名、大小寫不敏感
return base.FindAction(context, controller, actionName.ToLowerInvariant());
}
protected override IDictionary GetParameterValues(ControllerContext context, ActionDescriptor action)
{
// 例:從 Header、Claim、Session 自動繫結參數
return base.GetParameterValues(context, action);
}
}
// 控制器掛載
public class BaseController : Controller
{
protected override IActionInvoker CreateActionInvoker() => new CustomActionInvoker();
}
抽象類:`ActionMethodSelectorAttribute` (`bool IsValidForRequest(ControllerContext, MethodInfo)`)
應用:依據 Accept-Language、User-Agent、自訂 Header 選擇不同動作。
public class BrowserSpecificAttribute : ActionMethodSelectorAttribute
{
public string BrowserType { get; set; } // "Mobile", "Desktop", "Bot"
public override bool IsValidForRequest(ControllerContext context, MethodInfo methodInfo)
{
string ua = context.HttpContext.Request.UserAgent?.ToLower() ?? "";
return BrowserType switch
{
"Mobile" => ua.Contains("mobile") || ua.Contains("android") || ua.Contains("iphone"),
"Desktop" => !ua.Contains("mobile"),
"Bot" => ua.Contains("bot") || ua.Contains("crawler"),
_ => true
};
}
}
// 使用
public class HomeController : Controller
{
[BrowserSpecific(BrowserType = "Mobile")]
public ActionResult Index() => View("Index.Mobile");
[BrowserSpecific(BrowserType = "Desktop")]
public ActionResult Index() => View("Index.Desktop");
}
介面:`IAuthorizationFilter` (`void OnAuthorization(AuthorizationContext)`)
應用:自訂權限檢查、多因子驗證、IP 白名單、API Key 驗證。
public class ApiKeyAuthorizationFilter : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
string apiKey = filterContext.HttpContext.Request.Headers["X-Api-Key"];
if (!ApiKeyStore.IsValid(apiKey))
{
filterContext.Result = new HttpUnauthorizedResult("Invalid API Key");
}
}
}
// 全域註冊 (GlobalFilters)
GlobalFilters.Filters.Add(new ApiKeyAuthorizationFilter());
// 或控制器/動作層級
[ApiKeyAuthorizationFilter]
public ActionResult SecureData() { ... }
抽象類:`ActionFilterAttribute` (OnActionExecuting, OnActionExecuted, OnResultExecuting, OnResultExecuted)
應用:記錄、快取、ViewData 填充、模型狀態處理、單位工作提交。
public class ViewDataPopulateAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// 所有 View 共用的資料集中注入
filterContext.Controller.ViewBag.CurrentUser = GetCurrentUser();
filterContext.Controller.ViewBag.SiteSettings = GetSiteSettings();
filterContext.Controller.ViewBag.MenuItems = GetMenuItems();
}
}
[ViewDataPopulate]
public class BaseController : Controller { }
介面:`IResultFilter` (OnResultExecuting, OnResultExecuted)
應用:修改回應 Header、壓縮、加入通用 Footer、PDF/Excel 匯出包裝。
介面:`IExceptionFilter` (`void OnException(ExceptionContext)`)
應用:全域例外處理、記錄、友善錯誤頁面、API 錯誤格式統一。
public class GlobalExceptionFilter : IExceptionFilter
{
private readonly ILogger _logger;
public GlobalExceptionFilter(ILogger logger) => _logger = logger;
public void OnException(ExceptionContext filterContext)
{
if (filterContext.ExceptionHandled) return;
_logger.Error(filterContext.Exception, "Unhandled exception");
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new JsonResult
{
Data = new { error = "Server error", detail = filterContext.Exception.Message },
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
else
{
filterContext.Result = new ViewResult { ViewName = "Error" };
}
filterContext.ExceptionHandled = true;
}
}
介面:`IModelBinder` (`object BindModel(ControllerContext, ModelBindingContext)`)
應用:複雜型別繫結 (JSON Body、多部分表單、自訂格式)、加密 ID 解密、實體自動載入。
public class EntityModelBinder : IModelBinder where T : class, IEntity
{
private readonly IRepository _repo;
public EntityModelBinder(IRepository repo) => _repo = repo;
public object BindModel(ControllerContext cc, ModelBindingContext bc)
{
var value = bc.ValueProvider.GetValue(bc.ModelName);
if (value == null) return null;
if (Guid.TryParse(value.AttemptedValue, out var id))
return _repo.GetById(id);
return null;
}
}
// 註冊
ModelBinders.Binders.Add(typeof(Product), new EntityModelBinder(container.Resolve>()));
應用:自訂驗證規則 (跨欄位、資料庫唯一性、條件驗證)、移除預設 DataAnnotations 驗證。
應用:支援新請求格式 (JSON、XML、Protobuf、QueryString 陣列格式)。
// ASP.NET MVC 3+ 內建 JsonValueProviderFactory
// 自訂範例:支援 application/x-msgpack
public class MsgPackValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext cc)
{
if (cc.HttpContext.Request.ContentType?.StartsWith("application/x-msgpack") == true)
{
var dict = MsgPackSerializer.Deserialize>(cc.HttpContext.Request.InputStream);
return new DictionaryValueProvider(dict, CultureInfo.CurrentCulture);
}
return null;
}
}
// 註冊
ValueProviderFactories.Factories.Add(new MsgPackValueProviderFactory());
介面:`IViewEngine` (FindView, FindPartialView, ReleaseView)
應用:多主題/皮膚、多租戶視圖解析、Razor 以外模板引擎 (Spark、NVelocity、Handlebars.NET)。
public class ThemedRazorViewEngine : RazorViewEngine
{
private readonly string _theme;
public ThemedRazorViewEngine(string theme) => _theme = theme;
protected override IView CreateView(ControllerContext cc, string viewPath, string masterPath)
{
// 先找 Themes/{Theme}/Views/... 再找預設 Views/...
return base.CreateView(cc, viewPath.Replace("~/Views/", $"~/Themes/{_theme}/Views/"), masterPath);
}
}
// 註冊 (依租戶/主題動態決定)
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new ThemedRazorViewEngine(currentTenant.Theme));
Request
→ RouteCollection (RouteConstraint)
→ RouteHandler (IRouteHandler)
→ MvcHandler
→ ControllerFactory (IControllerFactory)
→ Controller
→ ActionInvoker (ControllerActionInvoker)
→ ActionMethodSelectorAttribute
→ AuthorizationFilter (IAuthorizationFilter)
→ ActionFilter (OnActionExecuting)
→ ModelBinder (IModelBinder) + ModelValidator
→ Action 執行
→ ActionFilter (OnActionExecuted)
→ ResultFilter (OnResultExecuting)
→ ActionResult 執行 (ViewResult → ViewEngine → View)
→ ResultFilter (OnResultExecuted)
→ ExceptionFilter (發生例外時)
Response
熟悉這 13 個擴展點,即能在不修改框架原始碼前提下,將 MVC 彎曲為任何業務形狀。