攔截 WCF 服務往返的的完整 SOAP 訊息雖然不是一兩行程式碼就能解決,但仍有一套頗固定的寫法。依樣畫葫蘆,倒也不難。這裡會說明如何撰寫自訂 MessageInspector 來攔截所有進出 WCF 服務的 SOAP 訊息。
應用場合
首先要知道的是,如果用戶端和服務端都是 WCF 應用程式,在攔截訊息的時候便有兩種選擇:
但如果我們同時負責撰寫 WCF 服務和用戶端程式,那就有兩個選擇。比如說,我的 WCF 服務只是個轉接站、中間人,亦即它接受用戶端程式的請求,然後轉而呼叫另一個第三方的 web service,並將回傳結果再轉交給用戶端程式。如下圖:
這種情況,我可以選擇圖中標示 (1) 的地方來攔截訊息,也就是在服務端攔截訊息。我也可以選擇 (2),此時我的 WCF 服務本身又同時擔任用戶端的角色,亦即在用戶端攔截訊息。
實作步驟
不管是在用戶端還是服務端攔截 WCF 的 SOAP 訊息,寫法都很類似,只有細微差異。
主要的步驟包括:
各實作步驟的詳細說明如下,先示範在服務端攔截 WCF 訊息的寫法。
Step 1: 撰寫自訂的 MessageInspector 類別
在 WCF 服務應用程式專案中加入一個新類別,此類別要實作 IDispatchMessageInspector。參考以下範例程式:
IDispatchMessageInspector 介面有兩個方法,各自處理訊息的一進一出:
這兩個方法都呼叫了 LogWcfMessage(),用來將訊息寫入至外部檔案。要特別注意的是這兩個方法所傳入的訊息物件(request 或 reply)都是以 pass by reference 的方式傳入,而且只能對它讀取一次。因此,在寫 log 時,必須先複製一份,並且對複本操作。若直接存取傳入之訊息物件,WCF 在進行後續處理時將會發生錯誤。
Step 2: 撰寫自訂的 EndpointBehavior 類別
在 WCF 服務應用程式專案中加入一個新類別,此類別須實作 IEndpointBehavior。範例如下:
重點在 ApplyDispatchBehavior 方法,這裡會用到上一個步驟的自訂 MessageInspector 類別。
Step 3: 註冊步驟 2 的 EndpointBehavior 類別
加入一個新類別,此類別須繼承自 BehaviorExtensionElement,並改寫 BehaviorType 屬性和 CreateBehavior 方法:
接著修改 web.config 組態檔,將我們的自訂 EndpointBehavior 類別套用至 WCF 服務端點。參考以下範例:
各區段之關係可參考下圖中的箭頭與註解:
完成上述步驟之後,每當用戶端呼叫一次我們的 WCF 服務(的某個方法),WCF 執行時期就會建立一個 MyMessageInspector 物件。也就是說,當多個用戶端對同一個 WCF 服務送出請求時,服務端會建立多個 MyMessageInspector 物件,而每一個物件的 BeforeSendReply 和 AfterReceiveRequest 方法必定屬於同一次請求。這表示我們可以在 BeforeSendReply 方法中取出訊息內容,暫存於 MyMessageInspector 類別的某個私有欄位,然後等到 AfterReceiveRequest 方法被呼叫時才一併將請求與回應結果的內容寫入 log,例如新增一筆記錄至資料庫。
此時再回過頭來看底下這張圖,應該會比較有感覺。本文範例中的 MyMessageInspector 就是在圖中標示 (1) 的地方發揮作用。
在用戶端攔截 WCF 訊息的寫法與前述步驟幾乎一樣,僅兩處不同。首先,步驟 1 的 IDispatchMessageInspector 要改為 IClientMessageInspector。然後,此介面的方法名稱也不一樣:
另一個不同的地方是應用程式組態檔。由於是用戶端應用程式,所以先前範例中的 web.config 裡面的 <services> 區段會變成 <client> 區段。內容如下:
收工!
後記:疑難排解
應用程式部署運行之後一段時間,從 Windows 事件檢視器中發現有這麼一條日誌:
A message was not logged.
Exception: System.ServiceModel.CommunicationException: There was an error in serializing body of message : 'There was an error generating the XML document.'. Please see InnerException for more details. ---> System.InvalidOperationException: There was an error generating the XML document. ---> System.ServiceModel.Diagnostics.PlainXmlWriter+MaxSizeExceededException: Exception of type 'System.ServiceModel.Diagnostics.PlainXmlWriter+MaxSizeExceededException' was thrown.
意思是欲記錄的訊息內容太大了,超過應用程式指定(或預設)的大小,故略過此訊息記錄。
解決方法是修改 WCF 服務應用程式的組態檔,加大 maxSizeOfMessageToLog 參數值。參考以下範例:
其中還有一個參數也要注意:maxMessagesToLog。此參數是用來控制應用程式最多要記錄幾筆訊息。你可以想像 WCF 內部有個計數器,每記錄一筆訊息,該計數器就加一,直到計數器達到 maxMessagesToLog 所設定的值,WCF 就不再記錄後續的訊息。而且,它不像 maxSizeOfMessageToLog 還會拋出 exception;它不會提醒你。相關參數之詳細說明請參考 MSDN:Configuring Message Logging。
參考資料
應用場合
首先要知道的是,如果用戶端和服務端都是 WCF 應用程式,在攔截訊息的時候便有兩種選擇:
- 在 WCF 用戶端程式中攔截 request 和 response 的 SOAP 訊息。
- 在 WCF 服務端程式中攔截 request 和 response 的 SOAP 訊息。
但如果我們同時負責撰寫 WCF 服務和用戶端程式,那就有兩個選擇。比如說,我的 WCF 服務只是個轉接站、中間人,亦即它接受用戶端程式的請求,然後轉而呼叫另一個第三方的 web service,並將回傳結果再轉交給用戶端程式。如下圖:
這種情況,我可以選擇圖中標示 (1) 的地方來攔截訊息,也就是在服務端攔截訊息。我也可以選擇 (2),此時我的 WCF 服務本身又同時擔任用戶端的角色,亦即在用戶端攔截訊息。
實作步驟
不管是在用戶端還是服務端攔截 WCF 的 SOAP 訊息,寫法都很類似,只有細微差異。
主要的步驟包括:
- 撰寫自訂的 MessageInspector 類別。若在用戶端攔截訊息,此類別須實作 IClientMessageInspector;若在服務端攔截訊息,則此類別須實作 IDispatchMessageInspector。
- 撰寫自訂的 EndpointBehavior 類別。此類別須實作 IEndpointBehavior。
- 註冊步驟 2 的 EndpointBehavior 類別,以套用此自訂行為。我是透過組態檔來註冊類別。在註冊之前,還得先寫一個類別,繼承自 BehaviorExtensionElement,並改寫其方法。
各實作步驟的詳細說明如下,先示範在服務端攔截 WCF 訊息的寫法。
Step 1: 撰寫自訂的 MessageInspector 類別
在 WCF 服務應用程式專案中加入一個新類別,此類別要實作 IDispatchMessageInspector。參考以下範例程式:
using System; using System.ServiceModel.Dispatcher; using System.Xml; using System.Text; using System.ServiceModel.Channels; namespace ServerApp { public class MyMessageInspector : IDispatchMessageInspector { public object AfterReceiveRequest( ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext) { LogWcfMessage(ref request, false); return null; } public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState) { LogWcfMessage(ref reply, true); } // 注意: 參數 'msg' 必須以 ref 方式傳遞!! private void LogWcfMessage(ref System.ServiceModel.Channels.Message msg, bool isResponse) { MessageBuffer buffer = msg.CreateBufferedCopy(Int32.MaxValue); msg = buffer.CreateMessage(); Message dupMsg = buffer.CreateMessage(); var filename = @"C:\temp\server_request.xml"; if (isResponse) { filename = @"C:\temp\server_response.xml"; } var writer = new XmlTextWriter(filename, Encoding.UTF8); dupMsg.WriteMessage(writer); writer.Close(); buffer.Close(); } } }
IDispatchMessageInspector 介面有兩個方法,各自處理訊息的一進一出:
- AfterReceiveRequest - WCF 會在收到用戶端請求之後呼叫此方法。
- BeforeSendReply - WCF 會在傳回結果之前呼叫此方法。
這兩個方法都呼叫了 LogWcfMessage(),用來將訊息寫入至外部檔案。要特別注意的是這兩個方法所傳入的訊息物件(request 或 reply)都是以 pass by reference 的方式傳入,而且只能對它讀取一次。因此,在寫 log 時,必須先複製一份,並且對複本操作。若直接存取傳入之訊息物件,WCF 在進行後續處理時將會發生錯誤。
Step 2: 撰寫自訂的 EndpointBehavior 類別
在 WCF 服務應用程式專案中加入一個新類別,此類別須實作 IEndpointBehavior。範例如下:
using System.ServiceModel.Description; namespace ServerApp { public class MyMessageInspectorBehavior : IEndpointBehavior { public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime) { } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher) { var inspector = new MyMessageInspector(); endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector); } public void Validate(ServiceEndpoint endpoint) { } } }
重點在 ApplyDispatchBehavior 方法,這裡會用到上一個步驟的自訂 MessageInspector 類別。
Step 3: 註冊步驟 2 的 EndpointBehavior 類別
加入一個新類別,此類別須繼承自 BehaviorExtensionElement,並改寫 BehaviorType 屬性和 CreateBehavior 方法:
using System; using System.ServiceModel.Configuration; namespace ServerApp { public class MyMessageInspectorConfigElement : BehaviorExtensionElement { public override Type BehaviorType { get { return typeof(MyMessageInspectorBehavior); } } protected override object CreateBehavior() { return new MyMessageInspectorBehavior(); } } }
接著修改 web.config 組態檔,將我們的自訂 EndpointBehavior 類別套用至 WCF 服務端點。參考以下範例:
<system.serviceModel> <extensions> <behaviorExtensions> <add name="myMessageInspector" type="ServerApp.MyMessageInspectorConfigSection, ServerApp" /> </behaviorExtensions> </extensions> <behaviors> <endpointBehaviors> <behavior name="messageInspector"> <myMessageInspector /> </behavior> </endpointBehaviors> <serviceBehaviors> <behavior name=""> <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" /> <serviceDebug includeExceptionDetailInFaults="false" /> </behavior> </serviceBehaviors> </behaviors> <services> <service name="ServerApp.MyService"> <endpoint address="" binding="basicHttpBinding" contract="Server.IMyService" behaviorConfiguration="messageInspector" /> </service> </services> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" /> </system.serviceModel>
各區段之關係可參考下圖中的箭頭與註解:
完成上述步驟之後,每當用戶端呼叫一次我們的 WCF 服務(的某個方法),WCF 執行時期就會建立一個 MyMessageInspector 物件。也就是說,當多個用戶端對同一個 WCF 服務送出請求時,服務端會建立多個 MyMessageInspector 物件,而每一個物件的 BeforeSendReply 和 AfterReceiveRequest 方法必定屬於同一次請求。這表示我們可以在 BeforeSendReply 方法中取出訊息內容,暫存於 MyMessageInspector 類別的某個私有欄位,然後等到 AfterReceiveRequest 方法被呼叫時才一併將請求與回應結果的內容寫入 log,例如新增一筆記錄至資料庫。
此時再回過頭來看底下這張圖,應該會比較有感覺。本文範例中的 MyMessageInspector 就是在圖中標示 (1) 的地方發揮作用。
圖片出處:http://msdn.microsoft.com/en-us/magazine/cc163302.aspx |
在用戶端攔截 WCF 訊息的寫法與前述步驟幾乎一樣,僅兩處不同。首先,步驟 1 的 IDispatchMessageInspector 要改為 IClientMessageInspector。然後,此介面的方法名稱也不一樣:
- AfterReceiveReply - 收到回應之後會呼叫此方法。
- BeforeSendRequest - 送出請求之前會呼叫此方法。
另一個不同的地方是應用程式組態檔。由於是用戶端應用程式,所以先前範例中的 web.config 裡面的 <services> 區段會變成 <client> 區段。內容如下:
<client> <endpoint address="http://localhost:12146/MyService.svc" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IMyService" contract="Demo.IMyService" behaviorConfiguration="messageInspector" name="BasicHttpBinding_IMyService" /> </client>
收工!
後記:疑難排解
應用程式部署運行之後一段時間,從 Windows 事件檢視器中發現有這麼一條日誌:
A message was not logged.
Exception: System.ServiceModel.CommunicationException: There was an error in serializing body of message : 'There was an error generating the XML document.'. Please see InnerException for more details. ---> System.InvalidOperationException: There was an error generating the XML document. ---> System.ServiceModel.Diagnostics.PlainXmlWriter+MaxSizeExceededException: Exception of type 'System.ServiceModel.Diagnostics.PlainXmlWriter+MaxSizeExceededException' was thrown.
解決方法是修改 WCF 服務應用程式的組態檔,加大 maxSizeOfMessageToLog 參數值。參考以下範例:
<diagnostics> <messageLogging logEntireMessage="true" logMalformedMessages="true" logMessagesAtServiceLevel="true" logMessagesAtTransportLevel="false" maxMessagesToLog="30000" maxSizeOfMessageToLog="20000" /> </diagnostics>
其中還有一個參數也要注意:maxMessagesToLog。此參數是用來控制應用程式最多要記錄幾筆訊息。你可以想像 WCF 內部有個計數器,每記錄一筆訊息,該計數器就加一,直到計數器達到 maxMessagesToLog 所設定的值,WCF 就不再記錄後續的訊息。而且,它不像 maxSizeOfMessageToLog 還會拋出 exception;它不會提醒你。相關參數之詳細說明請參考 MSDN:Configuring Message Logging。
參考資料
沒有留言: