攔截 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。
參考資料


.gif)
沒有留言: