Skip to main content

[探索 10 分鐘] ASP.NET MVC 操作 HTTP handler 與 module

首先, 一個整體觀, HttpHandler 與 HttpModule 這兩種處理模式, 可以互相搭配運用, 也可以互相取代, 取決於需求。他們的關係如 Implementing HTTPHandler and HTTPModule in ASP.NET 文章提到, 是這樣:
一個 request 可以通過許多 module (IHttpModule 介面), 被整個生命週期期間 (BeginRequest() ~ EndRequest()) 被各種事件函式做加工, 如驗證, 轉址, 寫 Log; 而後透過路由導到 handler (IHttpHandler 介面) 做最後 ProcessRequest() 加工, 如副檔名 .jpg 要做什麼, .zip 要做什麼, 並產生 HttpRequest 的 response。前者 module 類可以攔截所有 request, 而後者 handler 類專注在被分配到的特定資源請求處理。對於一個 web 應用, 這些都是可以選配的類別。

HttpHandler

MSDN 解釋:
An ASP.NET HTTP handler is the process that runs in response to a request that is made to an ASP.NET Web application. The most common handler is an ASP.NET page handler that processes .aspx files. When users request an .aspx file, the request is processed by the page handler.
ASP.NET HTTP 處理常式是為了回應對 ASP.NET Web 應用程式所提出之要求而執行的處理序。最普通的處理常式是處理 .aspx 檔案的 ASP.NET 網頁處理常式。當使用者要求 .aspx 檔案時,會透過網頁處理常式處理要求。HttpHandler 負責特定副檔名資源請求的處理常式, 如
  • ASP.NET page handler (*.aspx)
  • Web service handler (*.asmx)
  • Generic Web handler (*.ashx)
  • Trace handler (trace.axd)
  • 其他自訂 Handler (如 *.jpg, *.zip)
解釋很冗長, 我個人推薦 YouTube: What is HttpHandler and How to Implement Custom HttpHandler in ASP.NET MVC 他的解釋:
HttpHandlers are classes that implement IHttpHandler and generate a response to HttpRequest.
再搭配文章一開始的圖片, 較容易聚焦他的定位。然而在 ASP.NET MVC 專案中, 已經有豐富的 filter 可以針對 controller 或 action 做 request 加工, 若要再外掛客製的 handler, 可行嗎 ? 今天試了一下是可以的, 但動了滿多手腳才試出來, 包括 web.config, routing, 以及實作 IHttpHandler 都必須兼顧。

web.config

<system.webServer>
    <modules runAllManagedModulesForAllRequests="true" />
    <handlers>
        <add name="CountHttpHandler" path="*.count" verb="*" type="CountHttpHandler" />
    </handlers>
</system.webServer>

RouteConfig.cs

routes.IgnoreRoute("{resource}.count/{*pathInfo}");

CountHttpHandler.cs

public class CountHttpHandler : IHttpHandler
{
    public static int gCountRequest = 0;
    public void ProcessRequest(HttpContext context)
    {
        gCountRequest++;
        context.Response.Write(string.Format("<script>alert('CountRequest = {0} ');</script>", gCountRequest));
        context.Response.End();
     }
    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

HomeController.cs

[Route("home/home.count")]
public ActionResult Count()
{
    return Content(string.Format("<script>alert('CountRequest = {0} ');</script>", CountHttpHandler.gCountRequest));
}
這邊試了兩種方式, 一個是傳統ASP.NET HttpHandler 寫法, 在 web.config 新增一個 path 在 <handlers> 區域, 這個例子是 path = ".count" 的 request, 都交給 CountHttpHandler 去處理,  並簡單把 count 變量 alert 出來 (Response JavaScript 字串)。要注意由於非 MVC 的 routing, 故須在 web.config > system.webServer > modules > 這個路徑額外設定 runAllManagedModulesForAllRequests = "true" (default MVC 專案屬性值為 false), 以及 routing table 刻意忽略 .count 的路由。以上有一個沒處理好都會報錯, 同學可以自己試試。

另外一個 handler, 是故意命中 MVC 的路由 home/home.count, 結果不會由上述的 handler 處理, 而是標註有 [Route("home/home.count")] 的 action 函式來處理, 換句話說, 路由順序是:
  1. 符合 MVC 路由優先
  2. 符合非 MVC 路由 (runAllManagedModulesForAllRequests="true")
於是可以得到兩種不同的 response 畫面, 請注意左右網址不同, 返回的數字邏輯也不相同, "home/home.count" 怎麼重新整理, count 值不會變; home.count 甚至 *.count 的返回值會不斷 +1。

雖然可以這樣玩, 但有沒有發現怎麼都沒用到 MVC 的 routing ? 等於是在基礎路由上又客製另一組路由 (雖然一開始就說要玩), 在 MVC 專案中, 強烈建議還是透過 RoutingConfig 來分配路由, 可以稍微這麼調整:

web.config (不需要額外配置)

<system.webServer>
    <handlers>
        <add name="CountHttpHandler" path="*.count" verb="*" type="CountHttpHandler" />
    </handlers>
</system.webServer>

RouteConfig.cs (不需要額外排除 route, 而是額外增加較高優先權的 route)

routes.IgnoreRoute("{resource}.count/{*pathInfo}");
routes.Add(new Route("home/count", new CountRouteHandler()));

CountRouteHandler.cs (增加 IRouteHandler)

public class CountRouteHandler : IRouteHandler
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new CountHttpHandler(requestContext);
    }
}

CountHttpHandler.cs (增加建構式)

public class CountHttpHandler : IHttpHandler
{
    public static int gCountRequest = 0;
    private RequestContext _requestContext;

    public CountHttpHandler(RequestContext requestContext)
    {
        _requestContext = requestContext;
    }
    public void ProcessRequest(HttpContext context)
    {
        gCountRequest++;
        context.Response.Write(string.Format("<script>alert('CountRequest = {0} ');</script>", gCountRequest));
        context.Response.End();
     }
    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

HomeController.cs (刪除 route 屬性, 刪除重複的 handler 實作內容)

[Route("home/home.count")]
public ActionResult Count()
{
    return Content(string.Format("<script>alert('CountRequest = {0} ');</script>", CountHttpHandler.gCountRequest));    
    return View();
}
接下來網址輸入 ~/home/count, 就可以達到原來的效果了, action 幾乎不需要有邏輯代碼 (因為都在 handler 做掉了), 僅是額外多一個 IRouteHandler 的實作, 但整體看下來是否覺得關注點更分離, 代碼更簡潔?

HttpModule

MSDN 解釋:
Modules are called before and after the handler executes. Modules enable developers to intercept, participate in, or modify each individual request. Modules implement the IHttpModule interface, which is located in the System.Web namespace.
處理常式 (handler) 在執行前後,會呼叫模組 (module)。 模組可以讓開發人員攔截、參與或修改每個要求。 模組所實作的 IHttpModule 介面,是位於 System.Web 命名空間中。HttpModule 相對單純, 畢竟所有請求都會通過, 不像 handler 會與 routing; module 交互。

HttpModule 比較難記憶的是事件的執行順序, 官網列出他的可用事件很多, 我自己用到的也不到一半, 依序為:
  1. BeginRequest
  2. AuthenticateRequest
  3. AuthorizeRequest che
  4. AcquireRequestState
  5. PreRequestHandlerExecute
  6. PostRequestHandlerExecute
  7. ReleaseRequestState
  8. UpdateRequestCache
  9. EndRequest
每一個方法的說明如下:
  • BeginRequest: 已經啟動要求。如果要在要求前執行某個動作 (例如, 在每頁頁首顯示廣告橫幅), 請同步處理這個事件。
  • AuthenticateRequest: 如果要插入您的自訂驗證配置 (例如, 在資料庫中查看使用者, 以驗證密碼), 則可以建置模組, 以便同步處理這個事件並驗證使用者。
  • AuthorizeRequest: 您可以在內部使用這個事件, 以實作授權機制 (例如, 將存取控制清單 (ACL) 儲存在資料庫, 而非存入檔案系統)。您也可以覆寫這個事件, 但無此必要。
  • AcquireRequestState: 工作階段狀態是擷取自狀態儲存區。如果要建置自已的狀態管理模組, 則可以同步處理這個事件, 以便從狀態儲存區擷取「工作階段」狀態。
  • PreRequestHandlerExecute: 這個事件會在執行 HTTP 處理常式之前產生。
  • PostRequestHandlerExecute: 這個事件會在執行 HTTP 處理常式之後產生。
  • ReleaseRequestState: 工作階段狀態會存回狀態儲存區。如果要建置自訂工作階段狀態模組, 則必須將狀態存回狀態儲存區。
  • UpdateRequestCache: 這個事件會將輸出內容寫回輸出快取。如果要建置自訂快取模組,則必須將輸出內容寫回快取。
  • EndRequest: 要求已完成。您可能想要建置偵錯模組, 以便收集要求的全部資訊, 然後再將資訊寫入網頁中。
特別注意當中 HttpHandler 的執行位置, 一開始介紹的圖片其實隱含 handler 處理完畢會返回 module 的一個閉環, 所以 HttpModule 處理週期的中間才會夾 PreRequestHandlerExecute 跟 PostRequestHandlerExecute 的事件。更精確來說, 起於 IHttpModule 類, 然後 IRoutingHandler 類做 Routing, 然後分派至 IHttpHandler 類, 然後返回 IHttpModule 類結束。

舉一個寫 Log 的 LogHttpModule 來說, 要做的事情極少
  1. 設定 HTTP 模組 (web.config)
  2. 建立 HTTP 模組 (建立 IHttpModule 類)

web.config

<system.webServer>
    <modules>
        <add name="LogHttpModule" type="LogHttpModule" />
    </modules>
</system.webServer>

LogHttpModule.cs 

public class LogHttpModule : IHttpModule
{
    private static readonly ILog _logger = LogManager.GetLogger("CommonLogger");
    
    public LogHttpModule()
    {
    }

    public void Init(HttpApplication context)
    {
        HttpContext.Current.ApplicationInstance.AuthorizeRequest += new EventHandler(this.context_AuthorizeRequest);
        HttpContext.Current.ApplicationInstance.AuthenticateRequest += new EventHandler(this.context_AuthenticateRequest);
        HttpContext.Current.ApplicationInstance.PreRequestHandlerExecute += new EventHandler(this.context_PreRequestHandlerExecute);
        HttpContext.Current.ApplicationInstance.PostRequestHandlerExecute += new EventHandler(this.context_PostRequestHandlerExecute);
        HttpContext.Current.ApplicationInstance.BeginRequest += new EventHandler(this.context_BeginRequest);
        HttpContext.Current.ApplicationInstance.EndRequest += new EventHandler(this.context_EndRequest);
    }
    
    public void context_AuthenticateRequest(object sender, EventArgs e)
    {
        _logger.Log("AuthenticateRequest: " + ((System.Web.HttpApplication)(sender)).Request.RawUrl);
    }

    public void context_AuthorizeRequest(object sender, EventArgs e)
    {
        _logger.Log("AuthorizeRequest: " + ((System.Web.HttpApplication)(sender)).Request.RawUrl);
    }

    public void context_PreRequestHandlerExecute(object sender, EventArgs e)
    {
        _logger.Log("PreRequestHandlerExecute: " + ((System.Web.HttpApplication)(sender)).Request.RawUrl);
    }
    public void context_PostRequestHandlerExecute(object sender, EventArgs e)
    {
        _logger.Log("PostRequestHandlerExecute: " + ((System.Web.HttpApplication)(sender)).Request.RawUrl);
    }

    public void context_BeginRequest(object sender, EventArgs e)
    {
        _logger.Log("BeginRequest: " + ((System.Web.HttpApplication)(sender)).Request.RawUrl);
    }

    public void context_EndRequest(object sender, EventArgs e)
    {
        _logger.Log("EndRequest: " + ((System.Web.HttpApplication)(sender)).Request.RawUrl);
    }

    public void Dispose()
    {
    }
}

MiniProfiler

延伸應用。MiniProfiler 是簡單好用的系統執行效能紀錄 (含UI) 套件,  Install-Package MiniProfiler 後, 參考官網, 需配置處理常式, 方便訪問後台
  • ~/mini-profiler-resources/results
  • ~/mini-profiler-resources/results-index

web.config

<system.webServer>
    <handlers>
        <add name="MiniProfiler" path="mini-profiler-resources/*" verb="*" type="System.Web.Routing.UrlRoutingModule" resourceType="Unspecified" preCondition="integratedMode" />
    </handlers>
</system.webServer>

Global.asax

protected void Application_BeginRequest()
{
    if (Request.IsLocal)
    {
        MiniProfiler.Start();
    }
}

protected void Application_EndRequest()
{
    MiniProfiler.Stop();
}
就 work 了。而且從 path 屬性我們可以很知道他的路由就是跟 /mini-profiler-resources/* 有關, 馬上學以致用。

但這邊有個問題, 如果我有一台 server 決定不做性能的稽核想要關閉 MiniProfiler, 那是不是要修改代碼, 上版, 發布 ? 結果部署之後每一台 server 都失去性能追蹤的眼睛了... (因為程式碼同一份阿)。利用上面所學, 我們就可以不照官網而把它移到 ProfileHttpModule 來做。

web.config

<system.webServer>
    <modules>
        <add name="ProfileHttpModule" type="ProfileHttpModule" />
    </modules>
</system.webServer>

ProfileHttpModule.cs

public class ProfileHttpModule : IHttpModule
{
    public ProfileHttpModule()
    {
    }

    public void Init(HttpApplication context)
    {
        System.Web.HttpContext.Current.ApplicationInstance.BeginRequest += new EventHandler(this.context_BeginRequest);
        System.Web.HttpContext.Current.ApplicationInstance.EndRequest += new EventHandler(this.context_EndRequest);
    }

    public void context_BeginRequest(object sender, EventArgs e)
    {
        MiniProfiler.Start();
        if (((System.Web.HttpApplication)(sender)).Request.RawUrl != "/")
            Thread.Sleep(3000);
    }

    public void context_EndRequest(object sender, EventArgs e)
    {
        MiniProfiler.Stop();
    }

    public void Dispose()
    {
    }
}
故意在非首頁的訪問等待三秒。
實證結果, 的確除了首頁, 其他頁面的訪問至少都被拖延 3 秒, 並且都被 Miniprofiler 給記錄下來了。以後要開關這個 module, 只要修改配置檔即可, 是不是靈活多了。

參考資料

  • https://support.microsoft.com/zh-tw/help/307985/info-asp-net-http-modules-and-http-handlers-overview
  • https://msdn.microsoft.com/zh-tw/library/bb398986.aspx#Features
  • https://www.codeproject.com/Articles/335968/Implementing-HTTPHandler-and-HTTPModule-in-ASP-NET
  • https://weblog.west-wind.com/posts/2015/nov/13/serving-urls-with-file-extensions-in-an-aspnet-mvc-application#EnablerunAllManagedModulesForAllRequests
  • https://www.codeproject.com/Articles/595520/MvcRouteHandler-and-MvcHandler-in-ASP-NET-MVC-Fram
  • http://patrickdesjardins.com/blog/how-to-create-a-httphandler-with-asp-mvc
  • http://miniprofiler.com/
  • https://stackoverflow.com/questions/40424538/http-modules-and-http-handlers-in-asp-net-mvc
  • https://stackoverflow.com/questions/1446097/registering-httpmodules-in-web-config
  • https://social.msdn.microsoft.com/Forums/vstudio/en-US/edffd022-1b1f-4070-a890-b9b0cd323c2e/getting-error-50022-from-configuration-validation-module-after-setting-fam-and-sam-modules-for-the?forum=Geneva

Comments

Post a Comment