鳥瞰 ASP.NET MVC 生命週期筆記

2020-07-15

重新學習 ASP.NET MVC 框架,不僅是使用框架來進行 Web 業務需求,更試圖以設計模式以及物件導向的邏輯來詮釋 ASP.NET MVC 框架,深入框架的原理與細節,讓自己在開發上能夠有最大設計彈性。而為了達到這個學習目標,使用鳥瞰的方式先理解應用程式生命週期,從而發現關注點分離精神下,每一個商業邏輯客製化實踐 (Custome, Override) 的最適方式,並且讓未來的維護與擴充更為方便。

logo

ASP.NET MVC Application Lifecycle

整個 MVC 的生命週期是被包含在 IIS 生命週期之中的,來自 Client 端的各種 Requests 一定是先傳送至 IIS 之中。而在 IIS 經由路由 (Routing) 處理後,判斷應該交由 MVC Handler 處理,直到 MVC 完成 View Render 後又再重新交棒回 IIS 的生命週期。

要理解整個 IIS 生命週期,所需的學習的成本不低,例如要探索原始碼、爬梳 IIS 原文資料等等,

Application_Start

這個步驟由 Global.asax 進行,預設會執行下列程式碼,其中最重要的就是 Filter Config 與 Route Config 的註冊。

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
}

Routing

預設 Route Config 第一條就是 Ignore 副檔名為 axd 的項目,而 axd 通常是 ASP.NET WebForm 所關聯的程式,因此同一個網站下是可以並存 WebForm 與 MVC 的。而藉由 Route 的查詢,如果找到對應的路由規則,IIS 框架就會交棒給 MVC Handler ,自此 MVC 框架下的處理流程正式展開 (勇者鬥惡龍的 BGM 奏樂 🎼)。

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

而如果沒有辦法吻合任何一條 route 則仍是 IIS 繼續處理;而如果是靜態檔案 Html, Css, Png, Js 等,則會由 iis_api 做判斷,如果判斷是靜態檔案就不交由 MVC Handler 而是直接以靜態檔案回應。


此外這邊可以有一個有趣的對比,如果呼叫 /NotExists.Asp 與 /Home/NotExistsAction ,前者因為不符合任何的 Route 會由 IIS 負責回應 HttpStatus 404;後者則因為有對應到 Route ,雖然此 Action 無效,但會由 MvcHandler 負責此 404 的產生,十分有趣。

IIS 負責的 HTTP 404

MVC 負責的 HTTP 404

Controller Init

MVC Handler 的入口執行點是 ProcessRequest Method,根據 HttpContext 利用工廠模式產生適合的 controller,初始化本次生命週期下的 controller 物件。 並將 requestContext 作為參數執行 IController 下的 Execute Method。

// namespace System.Web.Mvc
// public class MvcHandler : IHttpAsyncHandler, IHttpHandler, IRequiresSessionState

protected virtual void ProcessRequest(HttpContext httpContext)
{
    HttpContextBase httpContextBase = new HttpContextWrapper(httpContext);
    ProcessRequest(httpContextBase); // OverLoading 呼叫不同 signature 的 ProcessRequest
}

protected internal virtual void ProcessRequest(HttpContextBase httpContext)
{
    IController controller;
    IControllerFactory factory;
    ProcessRequestInit(httpContext, out controller, out factory); // 根據 HttpContext 取得 Controller 與 Factory

    try
    {
        controller.Execute(RequestContext); //多型的執行 Execute
    }
    finally
    {
        factory.ReleaseController(controller);
    }
}

private void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory)
{
    // If request validation has already been enabled, make it lazy. This allows attributes like [HttpPost] (which looks
    // at Request.Form) to work correctly without triggering full validation.
    // Tolerate null HttpContext for testing.
    HttpContext currentContext = HttpContext.Current;
    if (currentContext != null)
    {
        bool? isRequestValidationEnabled = ValidationUtility.IsValidationEnabled(currentContext);
        if (isRequestValidationEnabled == true)
        {
            ValidationUtility.EnableDynamicValidation(currentContext);
        }
    }

    AddVersionHeader(httpContext);
    RemoveOptionalRoutingParameters();

    // Get the controller type
    string controllerName = RequestContext.RouteData.GetRequiredString("controller");

    // Instantiate the controller and call Execute
    factory = ControllerBuilder.GetControllerFactory();
    controller = factory.CreateController(RequestContext, controllerName);
    if (controller == null)
    {
        throw new InvalidOperationException(
            String.Format(
                CultureInfo.CurrentCulture,
                MvcResources.ControllerBuilder_FactoryReturnedNull,
                factory.GetType(),
                controllerName)
        );
    }
}

public interface IController
{
    void Execute(RequestContext requestContext);
}

Action Execution

Controller 會藉由 ActionInvoker 來選擇 Action ,此時加入的 Action Method Selector (HttpGet / HttpPost) 便可助於 ActionInvoker 區別要使用的 Action 為何。

public interface IActionInvoker
{
    //
    // 摘要:
    //     使用指定的控制器內容,叫用指定的動作。
    //
    // 參數:
    //   controllerContext:
    //     控制器內容。
    bool InvokeAction(ControllerContext controllerContext, string actionName);
}

Authorization Filter

而 Authorization Filter 是先於 Action 被執行前,這邊會依照 IAuthroization 的 ;
AuthroizeCore;
如果 OnChallenge 時則會回應以

Model Binding

  • Model Binding 是如何完成的
  • ViewBag / ViewData / TempData 是如何實踐的

這個時候儘管 Action 的回傳型別是 ActionResult ,但實際上 Result 的具體內容仍未被產生,而是 Action 會再交棒給 Result Execution 去完成。

Result Execution

如果 Action 回傳的是 ViewResult 的話,MVC 會進一步地呼叫實作 IViewEngine 物件中的 FindView Method。Find View 會回傳一個實作 IView Interface 的物件,經由呼叫此物件的 Render 正式將 View 生成 並回傳給用戶端。

而如果不是 ViewResult 則會以 ActionReuslt


參考資料

ASP.NET MVC 4 開發實戰


Lifecycle of an ASP.NET MVC 5 Application

從Asp.net框架角度進入Asp.net MVC原始碼

GitHub - System.Web.Mvc

ASP.NET Application Life Cycle Overview for IIS 7.0

Detailed ASP.NET MVC Pipeline

Authentication and Authorization in ASP.NET Web API

ASP.NET MVC - Life Cycle

Understanding Action Filters (C#)

Microsoft - Reference Source