摘要:介紹 ASP.NET Web API 訊息處理器(message handlers)的基礎概念,並提供一個簡易的實作範例,可將任何 HTTP 請求的內容寫入 log 檔。

概觀

ASP.NET Web API 是以 HttpRequestMessage 類別來代表用戶端發出的 HTTP 請求,並以 HttpResponseMessage 類別來代表伺服器端的回應結果。也就是說,ASP.NET Web API 從收到用戶端請求開始到產生回應結果的過程當中,會在某個適當時間分別建立 HttpRequestMessage 和 HttpResponseMessage 物件實體。這個收到請求、處理請求、至產生回應的過程,一般稱為管線(pipeline)。

稱為管線,因為 HTTP 請求就像水一般從第一條水管流入,行經數個相互銜接的水管,直到最後一個;處理完請求並產生回應之後,接著又讓回應結果循原路回去,流經先前的各條水管。在這個過程當中,我們也可以安插自己的水管,在裡面動點手腳。

這一截截的「水管」對應到 Web API 的實作,就是訊息處理器(message handler)了。下圖概略描繪了 ASP.NET Web API 的「請求-回應」管線:


圖中的 HttpClient 是個 .NET 類別,但在這裡僅代表某個 HTTP 用戶端。它也是 .NET 4.5 的新成員(若為 .NET 4,可透過 NuGet 取得),隸屬 System.Net.Http 命名空間,功能有點類似 HttpWebRequestWebClient。由於 HttpClient 並非本文主角,就此簡短交代過去。

圖中的 HttpServer 類別隸屬 System.Web.Http 命名空間,其父類別是 DelegatingHandler,祖父則是抽象類別 HttpMessageHandler。也就是說,HttpSever 本身就是個訊息處理器。根據 MSDN 網站的說明,其責任是分派 HttpRequestMessage,以及建立 HttpResponseMessage。

DelegatingHandler

DelegatingHandler 的用途是處理 HttpRequestMessage 和 HttpResponseMessage 物件,並將處理過的物件交給下一棒繼續處理(於是形成了前面提過的管線機制)。當然,在處理的時候,也可以不要傳遞給下一個處理器,亦即停止後續的管線。

DelegatingHandler 有個 InnerHandler 屬性,型別是 HttpMessageHandler(它就是 DelegatingHandler 的父類別)。這個 InnerHandler 會指向下一條管線──這裡稍微改個說法:InnerHandler 會指向下一層管線。所以,你應該可以想像得到,就像拆禮物時,外包裝盒打開,裡面還有個盒子,再打開,裡面又還有個盒子…如此層層串接,各層之間的聯繫就是透過 InnerHandler 屬性。你也可以把它想成俄羅斯娃娃的結構,如果你知道這玩意的話。

俄羅斯娃娃(取自 wikipedia.org)

OK! 文字說明的部分差不多夠了,實作看看吧。

撰寫訊息處理器

之前遇過幾次,撰寫用戶端應用程式的開發人員一直收不到正確結果,老懷疑是 Web API 有 bug 而不肯檢視自己發出的請求內容是否有問題。最後只得將 HTTP 請求的詳細內容全部寫入 log 檔,作為證據。這裡就用一個簡單的訊息處理器來示範如何記錄 HTTP 請求的內容,程式碼是參考自 WebAPIContrib 裡面的 LoggingHandler,只是簡化了些 。

我們的自訂訊息處理器要繼承自前面提過的 DelegatingHandler。我先建立一個 ASP.NET MVC 4 應用程式專案:MessageHandlerDemo。接著在專案的根目錄下建一個 Handlers 子目錄,用來放自己寫的訊息處理器:RequestLogHandler。

建立 RequestLogHandler 類別之後,令它繼承自 DelegatingHandler,然後改寫(override)SendAsync 方法。程式碼如下:

using System;
using System.Web;
using System.Net.Http;

namespace MessageHandlerDemo.Handlers
{
    public class RequestLogHandler : DelegatingHandler
    {
        protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            LogRequest(request);

            return base.SendAsync(request, cancellationToken);
        }

        private void LogRequest(HttpRequestMessage request)
        {
            var info = new RequestLogInfo
            {
                HttpMethod = request.Method.Method,
                UriAccessed = request.RequestUri.AbsoluteUri,
                IPAddress = HttpContext.Current != null ? HttpContext.Current.Request.UserHostAddress : "0.0.0.0",
            };

            if (request.Content != null)
            {
                request.Content.ReadAsByteArrayAsync()
                    .ContinueWith((task) =>
                    {
                        info.BodyContent = System.Text.UTF8Encoding.UTF8.GetString(task.Result);
                    });
            }

            // Serialize to JSON string.
            string json = Newtonsoft.Json.JsonConvert.SerializeObject(info);
            string uniqueid = DateTime.Now.Ticks.ToString();
            string logfile = String.Format("C:\\Temp\\{0}.txt", uniqueid);
            System.IO.File.WriteAllText(logfile, json);
        }
    }

    public class RequestLogInfo
    {
        //public List<string> Header { get; set; }
        public string HttpMethod { get; set; }
        public string UriAccessed { get; set; }
        public string IPAddress { get; set; }
        public string BodyContent { get; set; }
    }
}

程式碼的邏輯很簡單,就不多解釋了。

註冊訊息處理器

訊息處理器寫好之後,還得註冊,才會起作用。我將註冊的動作寫在 App_Start\WebApiConfig.cs 裡面。參考下列程式碼:

using System;
using System.Web.Http;

namespace MessageHandlerDemo
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // ...略...

            config.MessageHandlers.Add(new Handlers.RequestLogHandler());
        }
    }
}

執行此範例程式時,每當用戶端發出請求,C:\Temp 目錄下就會產生對應的 log 檔案。檔案內容是 JSON 格式的字串。

最後附上一張類別圖:

小結
  • 對於 HTTP 請求與回應,ASP.NET Web API 對應的實作分別是 HttpRequestMessage 與 HttpResponseMessage。
  • HttpSever 本身也是一個訊息處理器(繼承自 DelegatingHandler),它的責任是分派 HttpRequestMessage,以及建立 HttpResponseMessage。
  • 撰寫自訂訊息處理器的步驟:建立一個新類別,令它繼承自 DelegatingHandler,並改寫(override)SendAsync 方法,然後將它加入(註冊)到 Web API 處理管線中。
  • DelegatingHandler 繼承自抽象類別 HttpMessageHandler。
  • DelegatingHandler 有個 InnerHandler 屬性,型別為 HttpMessageHandler,用來指向下一個處理器。管線(或俄羅斯娃娃)的訊息處理結構即由此屬性串接而成。  
文中的範例係修剪自 WebAPIContrib 裡面的 LoggingHandler。WebAPIContrib 裡面還提供了數個現成的訊息處理器以及其他輔助工具,可節省我們不少時間,也是不錯的學習材料。

參考資料