[Web API]LogMessageHandler of MessageProcessingHandler
前言
一般來說,要自訂 message handler 都會繼承自 DelegatingHandler ,然後 override SendAsync() 的方法。然而, DelegatingHandler 還有一個子類: MessageProcessingHandler ,也是拿來自訂 MessageHandler 的用途,而什麼情況下要用到 MessageProcessingHandler 呢?這篇文章以 LogMessageHandler 來當例子進行說明。
MessageHandler Class Diagram
首先先來看一下 Message Handler 的相關 class digram, 如下所示:
-
DelegatingHandler
: 要自訂 message handler 只需要繼承自 DelegatingHandler 並 override SendAsync() ,就可以定義 request 與 response 的內容,以及在什麼情境下需要往後面的 message handler 送,或是需要直接 response 給 client 端。 -
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 使用。如下圖所示:
因為與傳遞訊息無關,甚至不會異動到原本的 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 的互動,下一篇文章會進行介紹。
結論
在前面兩篇文章的例子:
- [Web API]如何針對 Message Handler 進行單元測試 : AuthenticationMessageHandler
- [Unit Test][Rhino.Mocks]讓 Stub 物件同一方法被呼叫兩次,回傳不同值 : SignatureMessageHandler
這兩個 scenario 都適合繼承自 DelegatingHandler, 因為當驗證不通過時,需要截斷 message pipeline, 直接回傳 Unauthorized 的 response 給呼叫端,需自訂訊息傳送方式,因此需繼承自 DelegatingHandler 。
而 LogMessageHandler 與訊息傳送方式無關,而只關注在 request 的訊息與 response 的訊息本身,因此繼承自 MessageProcessingHandler 會讓程式碼更乾淨、易懂。
經過這篇文章的介紹,相信讀者們應該也了解,事實上絕對可以用 DelegatingHandler 來取代 MessageProcessingHandler ,只是需不需要把職責分離地更清楚而已。
因此,請依據您的 scenario, 好好挑選 MessageHandler 的父類吧。
筆者也希望透過這幾篇文章的範例來示範與說明,該如何依賴於抽象,使得流程更加 stable, 讓程式碼更具可讀性,也可以因應不同 context 端的需求,既做到重用性,又可做到彈性抽換。
Reference
- Designing Evolvable Web APIs with ASP.NET (五顆星強推!)
- HTTP Message Handlers
- Huan-Lin 學習筆記: ASP.NET Web API 訊息處理器
blog 與課程更新內容,請前往新站位置:http://tdd.best/