攔截 WCF 服務往返的 SOAP 訊息

攔截 WCF 服務往返的的完整 SOAP 訊息雖然不是一兩行程式碼就能解決,但仍有一套頗固定的寫法。依樣畫葫蘆,倒也不難。這裡會說明如何撰寫自訂 MessageInspector 來攔截所有進出 WCF 服務的 SOAP 訊息。

應用場合

首先要知道的是,如果用戶端和服務端都是 WCF 應用程式,在攔截訊息的時候便有兩種選擇:
  1. 在 WCF 用戶端程式中攔截 request 和 response 的 SOAP 訊息。
  2. 在 WCF 服務端程式中攔截 request 和 response 的 SOAP 訊息。
通常只會採用其中一種。比如說,我們只是撰寫 WCF 服務來提供其他用戶端應用程式使用,此時當然就只會在我們自己的 WCF 服務中攔截 SOAP 訊息。目的也許是從中修改一些訊息內容,或者只是單純地記錄,以便日後資料出錯引發雙方爭議時,有證據可以核對。

但如果我們同時負責撰寫 WCF 服務和用戶端程式,那就有兩個選擇。比如說,我的 WCF 服務只是個轉接站、中間人,亦即它接受用戶端程式的請求,然後轉而呼叫另一個第三方的 web service,並將回傳結果再轉交給用戶端程式。如下圖:


這種情況,我可以選擇圖中標示 (1) 的地方來攔截訊息,也就是在服務端攔截訊息。我也可以選擇 (2),此時我的 WCF 服務本身又同時擔任用戶端的角色,亦即在用戶端攔截訊息。

實作步驟

不管是在用戶端還是服務端攔截 WCF 的 SOAP 訊息,寫法都很類似,只有細微差異。

主要的步驟包括:
  1. 撰寫自訂的 MessageInspector 類別。若在用戶端攔截訊息,此類別須實作 IClientMessageInspector;若在服務端攔截訊息,則此類別須實作 IDispatchMessageInspector
  2. 撰寫自訂的 EndpointBehavior 類別。此類別須實作 IEndpointBehavior
  3. 註冊步驟 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 - 送出請求之前會呼叫此方法。
實作程式碼則與先前步驟一的範例沒啥分別。當然,我們也可以讓同一個自訂的 MessageInspector 類別同時實作 IDispatchMessageInspector 和 IClientMessageInspector,這樣就只需要寫一個類別,便可同時用於伺服器端與用戶端。

另一個不同的地方是應用程式組態檔。由於是用戶端應用程式,所以先前範例中的 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

參考資料

沒有留言:

技術提供:Blogger.
回頂端⬆️