由於WCF采用.NET托管語言(C#和NET)作為其主要的編程語言,注定以了基於WCF的編程方式不可能很復雜。同時,WCF設計的一個目的就是提供基於非業務邏輯的通信實現,為編程人員提供一套簡單易用的應用編程接口(API)。WCF編程模式的簡單性同樣體現在異常處理上面,本篇文章的主要目的就是對WCF基於異常處理的編程模式做一個簡單的介紹。
一、當異常從服務端拋出
對於一個典型的WCF服務調用,我個人傾向於將潛在拋出的異常費為兩種類型:應用異常(Application Exception)和基礎結構(Infrastructure Exception)。前者為應用級別,主要體現為執行某個服務操作的業務邏輯拋出的異常;而後者則是業務無關的,通過WCF本身的基礎架構拋出,主要體現在對象的序列化、消息的處理、消息傳輸和消息的分發等等。在這裡我們更多地關注與應用異常。
首先,我們在不做任何異常處理相關操作的情況下,看看如果在服務端執行某個服務操作的過程中拋出異常後,客戶端會得到怎樣的結果。我們通過實例的形式來演示這中場景。處於簡單和易於理解考慮,我們照例沿用計算服務的例子。
我們照例采用典型的四層結構(Contract、Service、Hosting和Client),具體的層次在VS解決方案的劃分如圖1所示:
圖1 異常拋出實例解決方案結構
下面代碼片斷表示服務契約(ICalculator)和服務類型(CalculatorService)的定義。為了簡潔,在服務契約接口中,我們僅僅定義了唯一一個用於進行兩個整數觸發預算的方法Divide。服務契約和服務類型類型分別定義在項目Contracts和Services中。
1: using System.ServiceModel;
2: namespace Artech.WcfServices.Contracts
3: {
4: [ServiceContract(Namespace = "http://www.artech.com/")]
5: public interface ICalculator
6: {
7: [OperationContract]
8: int Divide(int x, int y);
9: }
10: }
1: using Artech.WcfServices.Contracts;
2: namespace Artech.WcfServices.Services
3: {
4: public class CalculatorService : ICalculator
5: {
6: public int Divide(int x, int y)
7: {
8: return x / y;
9: }
10: }
11: }
接下來是通過Console應用程序(Hosting項目)對上面定義的WCF服務(CalculatorService)進行寄宿(Host)的代碼和相關配置。
1: using System;
2: using System.ServiceModel;
3: using Artech.WcfServices.Services;
4: namespace Artech.WcfServices.Hosting
5: {
6: public class Program
7: {
8: static void Main(string[] args)
9: {
10: using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
11: {
12:
13: host.Open();
14: Console.Read();
15: }
16: }
17: }
18: }
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <services>
5: <service name="Artech.WcfServices.Services.CalculatorService">
6: <endpoint address="http://127.0.0.1:3721/calculatorservice" binding="wsHttpBinding" contract="Artech.WcfServices.Contracts.ICalculator" />
7: </service>
8: </services>
9: </system.serviceModel>
10: </configuration>
最後在代表客戶端的Console應用程序(Client項目)中對計算服務CalculatorService進行調用。相關的服務調用代碼和配置如下所示,為了讓服務端在執行Divide操作的時候拋出異常,特意將第二個參數設置為0,以便服務在進行除法運算的時候拋出System.DivideByZeroException異常。
1: using System;
2: using System.ServiceModel;
3: using Artech.WcfServices.Contracts;
4: namespace Artech.WcfServices.Clients
5: {
6: class Program
7: {
8: static void Main(string[] args)
9: {
10: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>(
11: "calculatorservice"))
12: {
13: ICalculator calculator = channelFactory.CreateChannel();
14: using (calculator as IDisposable)
15: {
16: int result = calculator.Divide(1, 0);
17: }
18: }
19: }
20: }
21: }
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <client>
5: <endpoint address="http://127.0.0.1:3721/calculatorservice"
6: binding="wsHttpBinding" contract="Artech.WcfServices.Contracts.ICalculator" name="calculatorservice" />
7: </client>
8: </system.serviceModel>
9: </configuration>
在啟動服務寄宿程序(Hosting)後執行客戶端服務調用程序,在客戶端將會跑出如圖2所示的類型為System.ServiceModel.FaultException的異常,其錯誤消息為:
“由於內部錯誤,服務器無法處理該請求。有關該錯誤的詳細信息,請打開服務器上的 IncludeExceptionDetailInFaults (從 ServiceBehaviorAttribute 或從 <serviceDebug> 配置行為)以便將異常信息發送回客戶端,或在打開每個 Microsoft .NET Framework 3.0 SDK 文檔的跟蹤的同時檢查服務器跟蹤日志。”
圖2 客戶端捕獲從服務端拋出的異常
從上面的實例演示中,我們可以獲知WCF在默認情況下的異常處理行為:對於服務端拋出的異常(這裡主要指應用異常),客戶端捕獲到的總一個具有相同異常消息的System.ServiceModel.FaultException異常。由於異常類型和消息固定不變,對於服務的客戶端來說,直接通過捕獲到的異常相關的信息是無法確定服務端在執行服務操作的時候遇到的具體的錯誤是什麼。
WCF如此設計的一個主要的目的為了安全。原因很簡單,由於我們不能保證服務端直接拋出的異常不包含任何敏感信息,所以直接將服務端原始的異常信息暴露給客戶端(對於服務提供者來說,該客戶端可能使一個不受信任或者部完全受信任的第三方)。
二、異常細節的傳輸
通過上面的介紹,我們已經意識到了:在默認的情況下,如果異常(主要指應用異常)在執行服務操作的過程中拋出,其真正的異常信息並不能被客戶端捕獲。實際上,服務端具體的異常細節信息僅限於服務端可見,並不會傳遞到客戶端。
然後,不論對於開發階段的調試,還是維護階段的糾錯、排錯,如果在客戶端調用某個服務操作後能夠很直接地獲取到從服務端拋出異常的所有細節,這無疑是一件很有價值的事情。那麼,WCF能夠做到這一點呢?答案是肯定的。
實際上,對於細心的讀者,看到客戶端捕獲的FaultException異常的消息,就能從中找到解決方案。消息中指出,如果試圖得到服務端具體的錯誤信息,需要開啟IncludeExceptionDetailInFaults這麼一個開關。具體來講,又具有兩種等效的方式:配置的方式和應用自定義特性(Custom Attribute)的方式。
通過在服務端的配置中,為寄宿的服務定義相應的服務行為(Service Behavior),並把serviceDebug配置項的includeExceptionDetailInFaults屬性設為True。具體配置如下所示:
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <behaviors>
5: <serviceBehaviors>
6: <behavior name="serviceDebuBehavior">
7: <serviceDebug includeExceptionDetailInFaults="true" />
8: </behavior>
9: </serviceBehaviors>
10: </behaviors>
11: <services>
12: <service behaviorConfiguration="serviceDebuBehavior" name="Artech.WcfServices.Services.CalculatorService">
13: <endpoint address="http://127.0.0.1:3721/calculatorservice" binding="wsHttpBinding" contract="Artech.WcfServices.Contracts.ICalculator" />
14: </service>
15: </services>
16: </system.serviceModel>
17: </configuration>
大部分系統自定義服務行為都可以直接通過在服務類型上應用System.ServiceModel.ServiceBehaviorAttribute這麼一個自定義特性一樣,includeExceptionDetailInFaults服務調試(ServiceDebug)行為也不另外。在ServiceBehaviorAttribute中定義了一個IncludeExceptionDetailInFaults屬性,當我們將ServiceBehaviorAttribute特性應用到具體的服務類型上的時候,只需將此屬性設為true即可。
1: [AttributeUsage(AttributeTargets.Class)]
2: public sealed class ServiceBehaviorAttribute : Attribute, IServiceBehavior
3: {
4: //其他成員
5: public bool IncludeExceptionDetailInFaults { get; set; }
6: }
所以如果不采用上面的配置,在服務類型CalculatorService上面應用ServiceBehaviorAttribute特性,並進行如下的設置,也可以到達相同的效果。
1: using Artech.WcfServices.Contracts;
2: using System.ServiceModel;
3: namespace Artech.WcfServices.Services
4: {
5: [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
6: public class CalculatorService : ICalculator
7: {
8: //省略服務成員
9: }
10: }
當IncludeExceptionDetailInFaults被開啟的ServiceDebug服務屬性通過上述兩種方式應用到我們例子中的服務CalculatorService的情況下,運行客戶端應用程序,將會捕獲包含有錯誤明細信息的異常,運行的結果如圖3所示:
圖3 客戶端捕獲到具有明細信息的異常
從圖3中,我們可以看出客戶端捕獲到的實際上是一個泛型的System.ServiceModel.FaultException<TDetail>異常。FaultException<TDetail>繼承自FaultException,這兩種典型的異常類型在WCF異常處理中具有重要的地位,在本章後續章節中還會重點講述,在這裡先做一點簡單的介紹。
對於所有從服務端拋出的異常,只有FaultException和直接或間接繼承自FaultException的異常才能被序列化,並最終通過消息返回給服務的調用端。FaultException可以通過文本的形式保存相應的錯誤信息。FaultException<TDetail>在FaultException現有的基礎上,增加了一個額外的特性:將錯誤信息通過一個具體的對象表示,其類型便是范型類型TDetail,該對象可以通過屬性Detail設置或者獲取。
1: [Serializable]
2: public class FaultException<TDetail> : FaultException
3: {
4: // 其他成員
5: public FaultException(TDetail detail);
6: public TDetail Detail { get; }
7: }
對於上面例子對應的場景,客戶端捕獲的異常類型實際上是FaultException< System.ServiceModel.ExceptionDetail>,也就是說其具體的泛型類型參數為System.ServiceModel.ExceptionDetail。ExceptionDetail的定義如下:
1: [DataContract]
2: public class ExceptionDetail
3: {
4: // 其他成員
5: public ExceptionDetail(Exception exception);
6:
7: [DataMember]
8: public string HelpLink { get; private set; }
9: [DataMember]
10: public ExceptionDetail InnerException { get; private set; }
11: [DataMember]
12: public string Message { get; private set; }
13: [DataMember]
14: public string StackTrace { get; private set; }
15: [DataMember]
16: public string Type { get; private set; }
17: }
ExceptionDetail是一個數據契約(Data Contract),也就意味ExceptionDetail是一個可以被DataContractSerializer進行序列化的對象。再仔細察看具體的屬性成員列表,我想很多讀者肯定有一種是曾相識的感覺:是不是和System.Exception的屬性成員定義很相似。實際上,ExceptionDetail是WCF專門設計出來用於封裝服務端拋出的異常信息的,其個屬性HelpLink、InnerException和StackTrace各自和System.Exception的同名屬性向對應,而屬性Type表示異常的類型。
也就是說,對於應用了開啟IncludeExceptionDetailInFaults的ServiceDebug服務行為的WCF服務,在執行服務操作拋出的異常信息,可以通過包含在客戶端捕獲的FaultException<ExceptionDetail>異常中的ExceptionDetail對象獲取。比如,在下面的代碼中,我修改了客戶端的代碼,將具體的錯誤信息輸出到控制台上:
1: using System;
2: using System.ServiceModel;
3: using Artech.WcfServices.Contracts;
4: namespace Artech.WcfServices.Clients
5: {
6: class Program
7: {
8: static void Main(string[] args)
9: {
10: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>(
11: "calculatorservice"))
12: {
13: ICalculator calculator = channelFactory.CreateChannel();
14: using (calculator as IDisposable)
15: {
16: try
17: {
18: int result = calculator.Divide(1, 0);
19: }
20: catch (FaultException<ExceptionDetail> ex)
21: {
22: Console.WriteLine("Message:{0}", ex.Detail.Message);
23: Console.WriteLine("Type:{0}", ex.Detail.Type);
24: Console.WriteLine("StackTrace:{0}", ex.Detail.StackTrace);
25: Console.WriteLine("HelpLink:{0}", ex.Detail.HelpLink);
26: (calculator as ICommunicationObject).Abort();
27: }
28: }
29: }
30: }
31: }
32: }
輸出結果:
1: Message:試圖除以零。
2: Type:System.DivideByZeroException
3: StackTrace: 在 Artech.WcfServices.Services.CalculatorService.Divide(Int32 x, Int32 y) 位置 D:\Demos\Artech.WcfServices\Services\CalculatorService.cs:行號 13
4: 在 SyncInvokeDivide(Object , Object[] , Object[] )
5: 在 System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]&; outputs)
6: 在System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc&; rpc)
7: 在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc&; rpc)
8: 在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc&; rpc)
9: 在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage3(MessageRpc&; rpc)
10: 在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage2(MessageRpc&; rpc)
11: 在 System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage1(MessageRpc&; rpc)
12: 在 System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)
13: HelpLink:
注:在catch程序塊中,我們通過代碼((calculator as ICommunicationObject).Abort();)將會話信道強行中斷。原因在於,對於基於會話信道(Sessionful Channel)的服務調用,服務端拋出的異常會將該信道的狀態轉變為出錯狀態(Faulted),處於Faulted狀態的會話信道將不能再用於後續的通信,即使你調用Close方法將其關閉。在這種情況下,需要調用Abort方法對其進行強行中止。具體的原理,在《WCF技術剖析(卷1)》的第9章有詳細的介紹。
對於服務行為SerivceDebug的IncludeExceptionDetailInFaults屬性,我需要再次重申一遍:由於會導致敏感信息洩露的潛在危險,一般地我們僅僅在調試的時候才會開啟該屬性。對於已經發布、付諸使用的服務,這個開關一般是關閉的。實際上,我們從這個服務行為的命名也可以看出,SerivceDebug,也是用於調試服務的服務行為罷了。