在任何Application的開發中,對不可預知的異常進行troubleshooting時,異常處理顯得尤為重要。對於一般的.NET系統來說,我們簡單地借助try/catch可以很容易地實現這一功能。但是對於 一個分布式的環境來說,異常處理就沒有那麼簡單了。按照面向服務的原則,我們把一些可復用的業務邏輯以Service的形式實現,各個Service處於一個自治的環境中,一個Service需要和另一個Service進行交互,只需要獲得該Service的描述(Description)就可以了(比如WSDL,Schema和Strategy)。借助標准的、平台無關的通信構架,各個Service之間通過標准的Soap Message進行交互。Service Description、Standard Communication Infrastructure、Soap Message based Communication促使各Service以松耦合的方式結合在一起。但是由於各個Service是自治的,如果一個Service調用另一個Service,在服務提供方拋出的Exception必須被封裝在Soap Message中,方能被處於另一方的服務的使用者獲得、從而進行合理的處理。下面我們結合一個簡單的Sample來簡單地介紹我們可以通過哪些方式在WCF中進行Exception Handling。
一、傳統的Exception Handling
我們沿用我們一直使用的Calculator的例子和簡單的4層構架:
1.Service Contract- Artech.ExceptionHandling.Contract
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
namespace Artech.ExceptionHandling.Contract
{
[ServiceContract]
public interface ICalculator
{
[OperationContract]
double Divide(double x, double y);
}
}
定義了一個單一的進行除法運算的Operation。
2.Service:Artech.ExceptionHandling.Service. CalculatorService
using System;
using System.Collections.Generic;
using System.Text;
using Artech.ExceptionHandling.Contract;
namespace Artech.ExceptionHandling.Service
{
public class CalculatorService:ICalculator
{
ICalculator Members#region ICalculator Members
public double Divide(double x, double y)
{
if (y == 0)
{
throw new DivideByZeroException("Divide by zero");
}
return x / y;
}
#endregion
}
}
如果被除數是零,拋出一個DivideByZeroException Exception。
3.Service Hosting
Configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="calculatorServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="calculatorServiceBehavior" name="Artech.ExceptionHandling.Service.CalculatorService">
<endpoint binding="basicHttpBinding" bindingConfiguration="" contract="Artech.ExceptionHandling.Contract.ICalculator" />
<host>
<baseAddresses>
<add baseAddress="http://localhost:8888/Calculator" />
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration>
Program
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
using Artech.ExceptionHandling.Service;
namespace Artech.ExceptionHandling.Hosting
{
class Program
{
static void Main(string[] args)
{
using (ServiceHost calculatorHost = new ServiceHost(typeof(CalculatorService)))
{
calculatorHost.Opened += delegate
{
Console.WriteLine("The Calculator service has begun to listen via the address:{0}", calculatorHost.BaseAddresses[0]);
};
calculatorHost.Open();
Console.Read();
}
}
}
}
4.Client
Configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<client>
<endpoint address=http://localhost:8888/Calculator binding="basicHttpBinding" contract="Artech.ExceptionHandling.Contract.ICalculator"
name="defualtEndpoint" />
</client>
</system.serviceModel>
</configuration>
Program
using System;
using System.Collections.Generic;
using System.Text;
using Artech.ExceptionHandling.Contract;
using System.ServiceModel;
namespace Artech.ExceptionHandling.Client
{
class Program
{
static void Main(string[] args)
{
ChannelFactory<ICalculator> calculatorFactory = new ChannelFactory<ICalculator>("defualtEndpoint");
ICalculator calculator = calculatorFactory.CreateChannel();
try
{
Console.WriteLine("Try to invoke Divide method");
Console.WriteLine("x / y = {2} when x = {0} and y = {1}", 2, 0, calculator.Divide(2,0));
}
catch (Exception ex)
{
Console.WriteLine("An Exception is thrown.\n\tException Type:{0}\n\tError Message:{1}", ex.GetType(), ex.Message);
}
Console.Read();
}
}
}
把Service調用放在一個try/catch block中,看看Service端拋出的DivideByZeroException Exception能否被Catch。
我們運行這個程序,看看Client有怎樣的輸出:
我們發現Client catch住的不是我們Service端真正拋出的DivideByZeroException Exception,而是一個比較General的FaultException。Error message也是很general:
"Theserverwasunabletoprocesstherequestduetoaninternalerror.Formoreinformationabouttheerror,eitherturnonIncludeExceptionDetailInFaults(eitherfromServiceBehaviorAttributeorfromthe<serviceDebug>configurationbehavior)ontheserverinordertosendtheexceptioninformationbacktotheclient,orturnontracingaspertheMicrosoft.NETFramework3.0SDKdocumentationandinspecttheservertracelogs."
二、基於ServiceDebug的Exception Handling
很顯然Client端Catch住的Exception對我們進行troubleshooting。為了利於我們進行有效的Debug,WCF提供了ServiceDebug Service Behavior。我們通過includeExceptionDetailInFaults屬性設為true,那麼如果Service拋出Exception,WCF會簡單得包裝這個Exception並把它置於Soap中Response到Service的訪問者。介於此,我修改了Hosting的Configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="calculatorServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="calculatorServiceBehavior" name="Artech.ExceptionHandling.Service.CalculatorService">
<endpoint binding="basicHttpBinding" bindingConfiguration="" contract="Artech.ExceptionHandling.Contract.ICalculator" />
<host>
<baseAddresses>
<add baseAddress="http://localhost:8888/Calculator" />
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>
</configuration>
現在再次運行程序,看看現在的運行結果:
可以看到我們我們Catch的是一個FaultException< ExceptionDetail>Type的Exception,不是原來的FaultException。該Exception的Detail屬性就是Service拋出的DivideByZeroException Exception。有興趣的人可以自己測試一下。而且我們在Service端指定的Error Message也被Client獲得。這種方式的Exception Handling方式確實比上面一種具有很強的指示性,對我們進行Debug確實很有幫助。但是這種方式確實不能正式用於我們最終發布的版本中,因為它會把Exception所有的信息返回到Client端,很容易洩露一些很敏感的信息。這也正是WCF把這個列入ServiceDebug Service Behavior的原因。
三、基於Fault Contract 的Exception Handling
既然上面通過定制ServiceDebug只能用於Debug階段。我們必須尋求另外一種Exception Handling的方式。那就是我們現在將要介紹的基於FaultContract的解決方案。我們知道WCF采用一種基於Contract,Contract定義了進行交互的雙方進行消息交換所遵循的准則和規范。Service Contract定義了包含了所有Operation的Service的接口,Data Contract定義了交互的數據的結構,而FaultContract實際上定義需要再雙方之間進行交互的了異常、錯誤的表示。我們現在來看看如何來使用基於FaultContract的Exception Handling。
我們首先來定義一個表示Fault的類:MathError。考慮到這個類需要在Service 和Client使用,我把它定義在Artech.ExceptionHandling.Contract中:
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization;
namespace Artech.ExceptionHandling.Contract
{
[DataContract]
public class MathError
{
private string _operation;
private string _errorMessage;
public MathError(string operation, string errorMessage)
{
this._operation = operation;
this._errorMessage = errorMessage;
}
[DataMember]
public string Operation
{
get { return _operation; }
set { _operation = value; }
}
[DataMember]
public string ErrorMessage
{
get { return _errorMessage; }
set { _errorMessage = value; }
}
}
}
在MathError中定義了兩個成員:表示出錯操作的Operation和出錯信息的ErrorMessage。由於該類的對象需要在Endpoint之間傳遞,所以必須是可序列化的,在WCF中,我們一般用兩個不同的Serializer實現Object和XML的Serialization和Deserialization:Datacontract Serializer和XML Serializer。而對於Fault,只能使用前者。
定義了MathError,我們需要通過FaultContract將其運用到Service Contract中制定的Operation上面,我們通過下面的方式來實現:
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;
namespace Artech.ExceptionHandling.Contract
{
[ServiceContract]
public interface ICalculator
{
[OperationContract]
[FaultContract(typeof(MathError))]
double Divide(double x, double y);
}
}
我們在Divide上運用了FaultContract,並指定了封裝了Fault對應的類型,那麼最終這個基於MathError類型的FaultContract會被寫入Service Description中,Client通過獲取該Service Description(一般是獲取WSDL),它就被識別它,就會將從接收到的Soap中對該Fault的XML Mapping到具體的MathError類型。
接著我們在Service Implementation中以拋出Exception的方式植入這個MathError對象:
using System;
using System.Collections.Generic;
using System.Text;
using Artech.ExceptionHandling.Contract;
using System.ServiceModel;
namespace Artech.ExceptionHandling.Service
{
public class CalculatorService:ICalculator
{
ICalculator Members#region ICalculator Members
public double Divide(double x, double y)
{
if (y == 0)
{
MathError error = new MathError("Divide", "Divided by zero");
throw new FaultException<MathError>(error,new FaultReason("Parameters passed are not valid"),new FaultCode("sender"));
}
return x / y;
}
#endregion
}
}
在被除數為0的時候,拋出FaultException<MathError> Exception,並指定具體的MathError對象,以及一個FaultCode(一般指明出錯的來源)和FaultReason(出錯的原因)。
我們現在先不修改Client的Exception Handling的相關代碼,先運行Hosting,看看WSDL中什麼特別之處:
通過上面的Screenshot,我們可以看到,在PortType section中的Divide Operation定義了Message為tns:ICalculator_Divide_MathErrorFault_FaultMessage 的<wsdl:fault>節點。通過查看Message Section,我們發現tns:ICalculator_Divide_MathErrorFault_FaultMessage的Element為q1:MathError,該q1:MathError type實際上是被定義在一個XSD中,其Uri為http://localhost:8888/Calculator?xsd=xsd2,我們定義的所有DataContract都在其中,下面的整個內容:
<?xml version="1.0" encoding="utf-8"?>
<xs:schema elementFormDefault="qualified" targetNamespace="http://schemas.datacontract.org/2004/07/Artech.ExceptionHandling.Contract" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://schemas.datacontract.org/2004/07/Artech.ExceptionHandling.Contract">
<xs:complexType name="MathError">
<xs:sequence>
<xs:element minOccurs="0" name="ErrorMessage" nillable="true" type="xs:string"/>
<xs:element minOccurs="0" name="Operation" nillable="true" type="xs:string"/>
</xs:sequence>
</xs:complexType>
<xs:element name="MathError" nillable="true" type="tns:MathError"/>
</xs:schema>
弄清楚了Fault在WSDL中表示後,我們來修改我們Client端的代碼,來有效地進行Exception Handling:
static void Main(string[] args)
{
ChannelFactory<ICalculator> calculatorFactory = new ChannelFactory<ICalculator>("defualtEndpoint");
ICalculator calculator = calculatorFactory.CreateChannel();
try
{
Console.WriteLine("Try to invoke Divide method");
Console.WriteLine("x / y = {2} when x = {0} and y = {1}", 2, 0, calculator.Divide(2, 0));
}
catch (FaultException<MathError> ex)
{
MathError error = ex.Detail;
Console.WriteLine("An Fault is thrown.\n\tFault code:{0}\n\tFault Reason:{1}\n\tOperation:{2}\n\tMessage:{3}", ex.Code, ex.Reason, error.Operation, error.ErrorMessage);
}
catch (Exception ex)
{
Console.WriteLine("An Exception is thrown.\n\tException Type:{0}\n\tError Message:{1}", ex.GetType(), ex.Message);
}
Console.Read();
}
下面是運行後的輸出結果: