【WCF】自定義錯誤處理(IErrorHandler接口的用法)


當被調用的服務操作發生異常時,可以直接把異常的原始內容傳回給客戶端。在WCF中,服務器傳回客戶端的異常,通常會使用 FaultException,該異常由這么幾個東東組成:

1、Action:在服務調用中,action標頭比較重要,它是塞在SOAP消息的Headers元素下面的,是消息頭的一部分,action用來對服務操作進行定義的。用小學生能聽懂的話說,就是某個服務操作的“學號”,通道層在消息調度時,會根據它來尋找要調用的Operation。記得老周舉過例子,就好比你去王老板家里,你得知道王老板住在哪個窩里面。

2、FaultCode:姑且翻譯為SOAP錯誤碼吧,雖然叫“碼”,但它並不是純數字表示的,與HTTP Error Code不同的。HTTP錯誤碼是個數字,比如婦孺皆知的404錯誤。SOAP錯誤碼實際上是一個叫Fault的元素,它塞在Body中,Fault元素下面有個叫Code的元素,就是這個FaultCode對象了,其實這個玩意兒你可以不用手動去定義它的,為啥呢,待會兒告訴你。

3、FaultReason:主要用來指定描述SOAP錯誤的自定義文本,表示發生錯誤的原因,其實這個東西在許多時候你也不必手動定義,原因待會兒再說。不過,這個Reason可以指定不同語言版本的錯誤信息,比如中文的,鳥語的。比如這樣:

<Reason>
  <Text xml:lang="zh-CN">此錯誤的創建者未指定“原因”。</Text>
</Reason>

zh-cn表示簡體中文,你可以指定其他物種的語言,如龜語、貓語、兔崽子語種等。

 

不過,有時候,FaultException用起來不夠爽,於是,類庫又提供了一個從FaultException派生的類——FaultException<TDetail>,注意它有個泛型參數,這個類的亮點在於,你可以用某個類型來封裝你的錯誤信息,然后把那個類型定義為數據協定。數據協定懂吧,就是一個類,並且可以XML序列化。如果不能XML序列化,那怎么把它的內容塞進SOAP消息中呢,別忘了,WCF是基於SOAP消息通信的(當然它可以像Web API那樣,用JSON/XML通信),為了讓對象可以在不同的端之間傳遞,當然得支持XML序列化了,對吧。就像你要寄快遞,你不能叫快遞員上門取炸彈,因為炸彈是禁止快遞的,所以你必須寄允許的物品,這樣物流才能流通。

 

如果你的錯誤信息比較簡單,比如string、double、int這些,屬於基本類型,那你不用定義數據協定了,因為基礎類型是可以序列化的。例如,FaultException<int>。

 

好了,上面一堆F話,其實是為了讓大伙認識一下FaultException,因為它很帥,也很有用,后面例子會用到它。

下面介紹一下 IErrorHandler 接口,來,先看看它的長相。

    public interface IErrorHandler
    {
       
        bool HandleError(Exception error);
       
        void ProvideFault(Exception error, MessageVersion version, ref Message fault);
    }

長得不是很清秀,將就一點吧,代碼的顏值都差不多,C#的長相算是比較優雅的了,要是JS的話,估計代碼都看得你頭暈。

這個接口的作用就是用來給咱們擴展WCF的錯誤處理的,實現這個高大上的接口,我們可以自定義返回給客戶端的錯誤消息。接口不是很復雜,只有兩個方法,標准的二胎:

1、HandleError:方法參數是服務操作引發的異常,在這個方法里,你可以通過方法參數攔截這個異常。比如,你可以在這里實現自己的錯誤日志記錄。如果你已經處理了錯誤,應該讓方法返回true,這樣運行時知道你已經處理了,就不再往下拋異常了,不然,很有可能導至服務馬上掛掉(單實例模式除外)。

2、ProvideFault:這個方法相當有用,在這個方法里面,你可以自己根據需要產生一條發回客戶端的消息,當然是帶Fault元素的消息,因為它不是正常消息,是錯誤消息。方法有三個參數:

1)error:服務操作拋出來的原始異常,這個能理解吧。

2)version:SOAP消息需要的版本,隨后你產生Fault消息時要用到它。

3)fault:這是核心,Message,表示錯誤消息實例。方法被調用時,這個參數是null的,因此,在方法結束前,你必須給它賦值一條Message,就是因為這樣,這個參數才聲明為 ref。

 

實現這個接口后,你得想辦法把它放進 ChannelDispatcher 類的 ErrorHandlers 集合中,這個類其實你看它那名字就猜到,它是負責調度通道層的。

在WCF中,99.9957%的擴展都通過擴展 Behavior 來實現的,而服務的每個層面上都有各自的 behavior ,比如,IServiceBehavior、IEndpointBehavior、IContractBehavior、IOperationBehavior ……

至於說應該從哪個behavior擴展,沒有什么硬性規定,一切視實際應用而定,這很靈活,你知道的,世界上唯一不變的就是變化,學習編程不是背九九乘法表。

 

由於通道層與服務協定在正常情形下是對應的,而且在客戶端,是可以直接將服務協定(Service Contract)當成通道來用的,WCF內部會有默認的實現來完成這些轉化,當然你有本事的話也可以自己寫通道。一般也沒多大必要,因為我們常用的HTTP,TCP之類的通信協議,默認都有了,反正老周從沒寫過通道層。唉,老周水平低下,只能玩點簡單的東西,玩復雜的東西不行。

 

於是,老周今天提供的例子,是從服務協定的behavior來擴展,為了方便用,我還把它寫成Attribute,這個老周在前面的文章中說過的,寫成Attribute的擴展,WCF運行時也能自動識別,並插入對應的Behaviors集合中。

這個擴展稍后再扯,先扯重點,就是實現IErrorHandler接口。

直接上代碼,不難。

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

        public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
        {
            FaultException<string> fex = new FaultException<string>(error.Message);
            MessageFault mf = fex.CreateMessageFault();
            fault = Message.CreateMessage(version, mf, "http://zh-ja-demo/svfault");
        }
    }

這里老周不打算處理異常,所以HandleError直接返回true就行了,省事省代碼省時間。

因為錯誤消息不是很復雜,老周就選用簡單的FaultException<string>,以字符串來包裝錯誤的詳細信息。然后調用FaultException<string>的CreateMessageFault 方法就能夠得到一個 MessageFault 實例,有了這個 MessageFault 實例,就可以直接創建 Message 了。

還記得嗎,在前文中,介紹FaultException時,老周說過,FaultCode和FaultReasion可以不用自己來定義的,答案就在這里了,一個 CreateMessageFault 方法就全包了,省事省力。

最后,不要忘了,用 CreateMessage 方法創建一條發回客戶端的消息對象。

 

請大家記得,這個錯誤消息的 action 是:

http://zh-ja-demo/svfault

好像有點難記,老周也后悔了,干嗎用這么屌的 action 名字。為啥要注意action呢,因為雖然它是一條錯誤消息,可是那也是一條SOAP消息,是吧,既然是SOAP消息,它必須一個action頭來定位它要調用的操作,這個操作不是協定操作,而是讓客戶端能夠收到這條錯誤消息,如果沒有action,客戶端可能定位不到這條消息,當然不是絕對情況,這里面的事情很復雜,說不清。

 

這個 action 后面會用到。

 

接下來,擴展一下協定層的behavior。

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
    public class MyContractBehaviorAttribute : Attribute, IContractBehavior
    {
        public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
            
        }

        public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
        }

        public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
        {
            ServiceErrorHandler sverr = new ServiceErrorHandler();
            dispatchRuntime.ChannelDispatcher.ErrorHandlers.Add(sverr);
        }

        public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
        {
        }
    }

因為這個只是處理服務器上的錯誤,不處理客戶端,所以ApplyClientBehavior方法留空,不用管它。

 

然后把這個Attribute應用到協定接口或者服務類上即可,此處我應用到服務類上,反正不用向客戶端公開。

    [MyContractBehavior]
    class DemoService : IDemo
    {
         ……

 

現在,我們在實現服務的操作中引發一下異常。

        public long RunWork(int bs)
        {
            if (bs < 0)
            {
                throw new ArgumentException("參數不能小於0。");
            }
            if(bs > 1000000)
            {
                throw new ArgumentException("參數不能大於1000000。");
            }

            long res = 0L;
            int k = 0;
            while(k <= bs)
            {
                res += k;
                k += 1;
            }
            return res;
        }

這代碼的意思很簡單,我就不解釋了。

 

 

現在,在客戶端調用一下服務。

            ChannelFactory<Contracts.IDemo> fac = new ChannelFactory<Contracts.IDemo>("client_ep");
            Contracts.IDemo dmchannel = fac.CreateChannel();
            try
            {
                long r = dmchannel.RunWork(baseNum);
                tb.Text = $"計算結果:{r}";
            }catch(FaultException<string> fex)
            {
                MessageBox.Show(fex.Detail);
            }
            catch(Exception ex)
            { 
                MessageBox.Show(ex.Message);
            }
            fac.Close();

 

如下圖,調用時,故意輸入一個不符合要求的值,讓服務器引發異常。

 

很遺憾的是,沒有出現我們自定義的錯誤提示。

那是因為服務器沒有把錯誤消息回發給客戶端就把連接關閉了。

 

要排除這個問題也是TMD簡單,只需要在服務操作協定上加上這個Attribute即可。

        [FaultContract(typeof(string), Action = "http://zh-ja-demo/svfault")]
        long RunWork(int bs);

注意,FaultContractAttribute是應用在協定方法上的。

傳給構造函數的Type一定要與FaultException<TDetail>中的泛型匹配,記得吧,剛剛我們用的是string,所以這里也要用string。剛才在實現IErrorHandler時,老周說過,那個錯誤消息的 action 你要記住,因為這里要用,Action 所指定的值必須和我們剛剛在ErrorHandler中定義的action相匹配,否則客戶端找不到錯誤消息。

 

好好,加了這個Attribute后,問題就解決了,此時,就能顯示服務器上的錯誤消息了。

 

 

OK,本文的內容就扯完了,該開飯了。

示例源代碼下載

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM