天天看點

【WCF】錯誤處理(四):一刀切——IErrorHandler

前面幾篇爛文中所介紹到的錯誤方式,都是在操作協定的實作代碼中抛出 FaultException 或者帶泛型參數的detail方案,有些時候,錯誤的處理方法比較相似,可是要每個操作協定去處理,似乎也太麻煩,此時就應當考慮統一處理了。

在 System.ServiceModel.Dispatcher 命名空間下,有一個 IErrorHandler 接口,這個接口就是讓我們統一處理錯誤的。

先來認識一下這個接口。

public interface IErrorHandler
    {

        bool HandleError(Exception error);
        

        void ProvideFault(Exception error, MessageVersion version, ref Message fault);

    }      

HandleError 方法是先判斷一下,這個錯誤是否應該讓會話中止,通常我們直接傳回 true,表示會話不會中止,如果傳回false,通信會話會被中止,這會使得服務執行個體被釋放(單個執行個體模式除外,因為單例模式下如果把執行個體釋放,就導緻其他用戶端無法調用服務了),進而使資料丢失,是以,還是傳回true好一點,表示錯誤由你來處理。

ProvideFault 方法就是用來産生 SOAP 錯誤消息的,可以在這裡對異常進行統一封裝。

IErrorHandler 的實作類隻能插入到 Channel Dispatcher中,這表明它是跟通道層相關的。最簡單的方法就是把實作了 IErrorHandler 接口的類型執行個體直接通過 ServiceHost 來擷取通道排程程式并加入到 ErrorHandlers 集合中。但,為了便于擴充和配置,老周還是建議甯可麻煩一點,擴充一個 behavior 來添加這個 handler。

按照老周一向的風格,理論和原理部分已經說完了,下面就是上菜,哦不,是上示例時間。

照舊,我們得先弄個示例服務。下面代碼定義了一個計算加法運算和二次方運算的協定。

[ServiceContract(Namespace = "sp-test", Name = "demo", ConfigurationName = "testContract")]
    public interface ITest
    {
        [OperationContract]
        int Add(int a, int b);
        [OperationContract]
        int SQR(int n);
    }      

然後,實作它。

[ServiceBehavior(ConfigurationName = "sv")]
    class TestSvr : ITest
    {
        public int Add(int a, int b)
        {
            if (a < 0) throw new ArgumentException("值不能小于0", nameof(a));
            if (b < 0) throw new ArgumentException("值不能小于0", nameof(b));
            return a + b;
        }

        public int SQR(int n)
        {
            if (n == 0) throw new Exception("參數不能為0");
            return n * n;
        }
    }      

在實作代碼中,都對傳入參數進行驗證,并抛出對應的異常,這個相信大夥能看懂。

接下來才是主角出場。

來,實作一個自定義的 Error Handler。

public class CustErrorHandler : IErrorHandler
    {
        public bool HandleError(Exception error)
        {
            return true;
        }

        public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
        {
            // 1、封裝異常
            string errText = error.Message;
            FaultException fex = new FaultException(errText);
            // 2、産生 MessageFault
            MessageFault msg_fault = fex.CreateMessageFault();
            // 3、産生新的消息
            fault = Message.CreateMessage(version, msg_fault, "cust-fault");
        }
    }      

用 FaultException 來封裝錯誤資訊是關鍵一步,如果必要可以用 FaultException<TDetail> ,因為這裡沒有定義錯誤協定類,故而用 FaultException 就OK了。

随後,通過 FaultException ,可以生成一個 MessageFault 對象,最後就是産生新的 Message ,這條消息是要傳回用戶端的。此處咱們用的是以下CreateMessage 方法。

public static Message CreateMessage(MessageVersion version, MessageFault fault, string action);      

version 參數好辦,隻直接引用 ProvideFault 方法中傳入的值即可,第二個參數就是剛剛産生的 MessageFault 執行個體,最後還得指定一個 action,這個你可以自己取,反正一個字元串就行。

下面實作 behavior,這裡以實作End Point 層面的 behavior 為例。

public class CustEndpointBehavior : IEndpointBehavior
    {
        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
            return;
        }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            return; //無需處理
        }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            ChannelDispatcher cndisp = endpointDispatcher.ChannelDispatcher;
            // 加入自定義的錯誤處理程式
            CustErrorHandler cehdlr = null;
            cehdlr = (CustErrorHandler)cndisp.ErrorHandlers.FirstOrDefault();
            if (cehdlr == null)
            {
                cehdlr = new CustErrorHandler();
            }
            cndisp.ErrorHandlers.Add(cehdlr);
        }

        public void Validate(ServiceEndpoint endpoint)
        {
            return;
        }
    }      

ApplyClientBehavior 方法是用戶端上面調用的,此處我們不需要,隻要實作 ApplyDispatchBehavior 方法即可,它是用在伺服器上的。

按理說,到這裡就完事了,但是,如果用配置檔案來配置的話,還是不夠的,當然了,用代碼來配置是沒問題的。

為了能讓這個自定義的behavior能在配置檔案中用,需要實作抽象類 BehaviorExtensionElement。

public class CustBehaviorExtElement : BehaviorExtensionElement
    {
        public override Type BehaviorType => typeof(CustEndpointBehavior);

        protected override object CreateBehavior()
        {
            return new CustEndpointBehavior();
        }
    }      

這個我不解釋了,簡單。

打開配置檔案,我們需要在配置檔案中注冊這個自定義的元素。

<system.serviceModel>
    <extensions>
      <behaviorExtensions>
        <add name="custerr" type="TestSvrSample.CustBehaviorExtElement, TestSvrSample"/>
      </behaviorExtensions>
    </extensions>
  </system.serviceModel>      

注意,type 屬性指定的是我們剛剛實作抽象類 BehaviorExtensionElement 的那個類,不僅要寫上完整類型路徑,還要寫上所在程式集的名字,不然會找不到。name 屬性用來指定在配置中使用時,其元素的名稱。

比如,可以這樣配置。

<behaviors>
      <endpointBehaviors>
        <behavior name="bhv">
          <custerr />
        </behavior>
      </endpointBehaviors>
    </behaviors>      

其中的 custerr 元素就是剛才在 behaviorExtensions 中所指定的 name 屬性。

最後可以在終結點配置中引用這個 behavior。

<service name="sv">
        <endpoint address="http://127.0.0.1:3655/test" binding="basicHttpBinding" contract="testContract" behaviorConfiguration="bhv"/>
      </service>      

好了,大功告成,現在,可以測試一下調用,調用中,我故意傳個不合适的參數值。

ITest ch = factory.CreateChannel();
                    try
                    {
                        int r = ch.Add(-1, 6);
                        Console.WriteLine(r);
                    }
                    catch(FaultException faultex)
                    {
                        FaultReason reason = faultex.Reason;
                        string text = reason.GetMatchingTranslation().Text;
                        Console.WriteLine($"錯誤:{text}");
                    }      

運作後,用戶端捕捉到的錯誤資訊如下圖所示。

【WCF】錯誤處理(四):一刀切——IErrorHandler

OK,本文到此結束,開飯。

示例代碼下載下傳:https://files.cnblogs.com/files/tcjiaan/IErrorHandlerSample.zip

繼續閱讀