[Web API]LogMessageHandler of MessageProcessingHandler

  • 4696
  • 0

[Web API]LogMessageHandler of MessageProcessingHandler

前言

一般來說,要自訂 message handler 都會繼承自 DelegatingHandler ,然後 override SendAsync() 的方法。然而, DelegatingHandler 還有一個子類: MessageProcessingHandler ,也是拿來自訂 MessageHandler 的用途,而什麼情況下要用到 MessageProcessingHandler 呢?這篇文章以 LogMessageHandler 來當例子進行說明。

 

MessageHandler Class Diagram

首先先來看一下 Message Handler 的相關 class digram, 如下所示:

Message Handler class diagram

  1. DelegatingHandler : 要自訂 message handler 只需要繼承自 DelegatingHandler 並 override SendAsync() ,就可以定義 request 與 response 的內容,以及在什麼情境下需要往後面的 message handler 送,或是需要直接 response 給 client 端。
  2. MessageProcessingHandler : 繼承自 DelegatingHandler 的 abstract class ,負責的職責更加單純,需 override ProcessRequest() 與 ProcessResponse() 兩個方法。也就是可以定義 request 與 response 的內容,或取內容來做事。跟 DelegatingHandler 比較起來, MessageProcessingHandler 不管怎麼往後送,或是是否要截斷直接 response 回 client 端。

這裡用一個 MyDelegatingHandler 以及 MyMessageProcessingHandler 來當範例,相信讀者就可以看到 MessageProcessingHandler 內部可能長什麼樣子,如下所示:


    public abstract class MyDelegatingHandler : DelegatingHandler
    {
        protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            return base.SendAsync(request, cancellationToken);
        }
    }

    public abstract class MyMessageProcessingHandler : DelegatingHandler
    {
        protected abstract HttpRequestMessage ProcessRequest(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken);

        protected abstract HttpResponseMessage ProcessResponse(HttpResponseMessage httpResponseMessage, System.Threading.CancellationToken cancellationToken);

        protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            // 原本的程式碼 return base.SendAsync(request, cancellationToken);
            return base.SendAsync(this.ProcessRequest(request, cancellationToken), cancellationToken).ContinueWith(task => this.ProcessResponse(task.Result, cancellationToken));
        }
    }

MessageProcessingHandler 其實就是定義兩個 abstract 的 function 來讓子類可以自行決定,如何處理 HttpRequestMessage 與 HttpResponseMessage ,然後 SendAsync() 則不是 abstract ,而是幫子類把 request message 往後送,並接到 response 往回送。

也因為 MessageProcessingHandler 已經決定好 SendAsync() 的行為,因此繼承自 MessageProcessingHandler 的子類無法變更這個行為的內容。

 

LogMessageHandler–繼承自 MessageProcessingHandler

LogMessageHandler 的職責,就是進出 Web API 時,要把 request 與 response 相關的資訊記錄下來供 audit 使用。如下圖所示:

image

因為與傳遞訊息無關,甚至不會異動到原本的 request 與 response ,因此 LogMessageHandler 繼承自 MessageProcessingHandler 比 繼承自 DelegatingHandler 來得更乾淨、精準與好維護。

來看一下範例程式碼:


    public interface ILog
    {
        void Save(string logContent);
    }

    public interface ISerializer
    {
        string Serialize<T>(RequestLogInfo info);

        string Serialize<T>(ResponseLogInfo info);
    }

    public class LogMessageHandler : MessageProcessingHandler
    {
        private ILog _log;
        private ISerializer _serializer;

        public LogMessageHandler(ILog log, ISerializer serializer)
        {
            this._log = log;
            this._serializer = serializer;
        }

        /// <summary>
        /// 將 request 相關訊息記錄 log
        /// </summary>
        /// <param name="request">The HTTP request message to process.</param>
        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>
        /// Returns <see cref="T:System.Net.Http.HttpRequestMessage" />.The HTTP request message that was processed.
        /// </returns>
        protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            var info = new RequestLogInfo
            {
                HttpMethod = request.Method.Method,
                UrlAccessed = request.RequestUri.AbsoluteUri,
                IpAddress = HttpContext.Current != null ? HttpContext.Current.Request.UserHostAddress : "0.0.0.0",
                RequestTime = ContextHelper.GetNow(),
                Token = this.GetToken(request),
                Signature = this.GetSignature(request),
                Timestamp = this.GetTimestamp(request),
                BodyContent = request.Content == null ? string.Empty : request.Content.ReadAsStringAsync().Result
            };

            var logContent = this._serializer.Serialize<RequestLogInfo>(info);
            this._log.Save(logContent);

            return request;
        }

        /// <summary>
        /// 將 response 相關訊息記錄 log
        /// </summary>
        /// <param name="response">The HTTP response message to process.</param>
        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <returns>
        /// Returns <see cref="T:System.Net.Http.HttpResponseMessage" />.The HTTP response message that was processed.
        /// </returns>
        protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, System.Threading.CancellationToken cancellationToken)
        {
            if (response == null)
            {
                throw new ArgumentNullException("response");
            }

            var info = new ResponseLogInfo
            {
                StatusCode = ((int)response.StatusCode).ToString(),
                ResponseTime = ContextHelper.GetNow(),
                ReturnCode = this.GetReturnCode(response),
                ReturnMessage = this.GetReturnMessage(response),
                Signature = this.GetSignature(response),
                BodyContent = response.Content == null ? string.Empty : response.Content.ReadAsStringAsync().Result
            };

            var logContent = this._serializer.Serialize<ResponseLogInfo>(info);
            this._log.Save(logContent);

            return response;
        }

        private string GetReturnCode(HttpResponseMessage response)
        {
            return HeaderHelper.GetHeaderValue(response.Headers, "api-returnCode").Item2;
        }

        private string GetReturnMessage(HttpResponseMessage response)
        {
            return HeaderHelper.GetHeaderValue(response.Headers, "api-returnMessage").Item2;
        }

        private string GetSignature(HttpResponseMessage response)
        {
            return HeaderHelper.GetHeaderValue(response.Headers, "api-signature").Item2;
        }

        private string GetSignature(HttpRequestMessage request)
        {
            return HeaderHelper.GetHeaderValue(request.Headers, "api-signature").Item2;
        }

        private string GetTimestamp(HttpRequestMessage request)
        {
            return HeaderHelper.GetHeaderValue(request.Headers, "api-timestamp").Item2;
        }

        private string GetToken(HttpRequestMessage request)
        {
            return HeaderHelper.GetHeaderValue(request.Headers, "api-token").Item2;
        }
    }

LogMessageHandler 只負責針對 request 與 response 記錄 log,至於實際上怎麼記,該記到哪,這不是 LogMessageHandler 的職責,或者這麼說會更精準:「應該由使用端來決定,該怎麼記與記到哪。」所以,把怎麼記跟記到哪的職責,委託給 ILog 的實體來決定。

因為這邊透過 RequestLogInfo 與 ResponseLogInfo 來當 log 內容的容器,因此需要透過一個序列化的動作來產生字串,供 ILog 記錄。怎麼序列化,應該也交由使用端來決定,序列化的格式,不該讓 LogMessageHandler 直接將序列化的方式焊死。

記住,抽象地設計,只依賴於介面,這也就是介面導向設計與 IoC 的目的。

Log 的 value object 與 HeaderHelper 請參考下列程式碼:


    public class RequestLogInfo
    {
        public string BodyContent { get; set; }

        public string HttpMethod { get; set; }

        public string IpAddress { get; set; }

        public DateTime RequestTime { get; set; }

        public string Signature { get; set; }

        public string Timestamp { get; set; }

        public string Token { get; set; }

        public string UrlAccessed { get; set; }
    }

    public class ResponseLogInfo
    {
        public string BodyContent { get; set; }

        public DateTime ResponseTime { get; set; }

        public string ReturnCode { get; set; }

        public string ReturnMessage { get; set; }

        public string Signature { get; set; }

        public string StatusCode { get; set; }
    }

    internal class HeaderHelper
    {
        internal static Tuple<bool, string> GetHeaderValue(HttpResponseHeaders httpResponseHeaders, string headerName)
        {
            var result = string.Empty;
            var specialHeaders = Enumerable.Empty<string>();
            var isExistHeader = httpResponseHeaders.TryGetValues(headerName, out specialHeaders);

            if (isExistHeader)
            {
                result = specialHeaders.LastOrDefault();
            }

            return Tuple.Create(isExistHeader, result);
        }

        internal static Tuple<bool, string> GetHeaderValue(HttpRequestHeaders httpRequestHeaders, string headerName)
        {
            var result = string.Empty;
            var specialHeaders = Enumerable.Empty<string>();
            var isExistHeader = httpRequestHeaders.TryGetValues(headerName, out specialHeaders);

            if (isExistHeader)
            {
                result = specialHeaders.LastOrDefault();
            }

            return Tuple.Create(isExistHeader, result);
        }
    }

這邊用 Tuple<bool, string> 只是為了避免需要用 out string 來回傳兩個結果,也因為夠單純,因此用 Tuple 這種「類似動態」但強型別的型別來回傳。

透過這樣的抽象與彈性設計,使用端就可以自行決定記錄 Log 的方式,以及序列化的格式,並且可以針對 LogMessageHandler 有效地進行單元測試。至於怎麼測試 LogMessageHandler 與 ILog 的互動,下一篇文章會進行介紹。

 

結論

在前面兩篇文章的例子:

  1. [Web API]如何針對 Message Handler 進行單元測試 : AuthenticationMessageHandler
  2. [Unit Test][Rhino.Mocks]讓 Stub 物件同一方法被呼叫兩次,回傳不同值 : SignatureMessageHandler

這兩個 scenario 都適合繼承自 DelegatingHandler, 因為當驗證不通過時,需要截斷 message pipeline, 直接回傳 Unauthorized 的 response 給呼叫端,需自訂訊息傳送方式,因此需繼承自 DelegatingHandler 。

而 LogMessageHandler 與訊息傳送方式無關,而只關注在 request 的訊息與 response 的訊息本身,因此繼承自 MessageProcessingHandler 會讓程式碼更乾淨、易懂。

經過這篇文章的介紹,相信讀者們應該也了解,事實上絕對可以用 DelegatingHandler 來取代 MessageProcessingHandler ,只是需不需要把職責分離地更清楚而已。

因此,請依據您的 scenario, 好好挑選 MessageHandler 的父類吧。

筆者也希望透過這幾篇文章的範例來示範與說明,該如何依賴於抽象,使得流程更加 stable, 讓程式碼更具可讀性,也可以因應不同 context 端的需求,既做到重用性,又可做到彈性抽換。

 

Reference

  1. Designing Evolvable Web APIs with ASP.NET (五顆星強推!)
  2. HTTP Message Handlers
  3. Huan-Lin 學習筆記: ASP.NET Web API 訊息處理器

blog 與課程更新內容,請前往新站位置:http://tdd.best/