異常消息與特定技術有關,.NET異常同樣如此,因而WCF並不支持傳統的異常處理方式。如果在WCF服務中采用傳統的方式處理異常,由於異常消息不能被序列化,因而客戶端無法收到服務拋出的異常,例如這樣的服務設計:
[ServiceContract(SessionMode = SessionMode.Allowed)]
public interface IDocumentsExplorerService
{
[OperationContract]
DocumentList FetchDocuments(string homeDir);
}
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class DocumentsExplorerService : IDocumentsExplorerService,IDisposable
{
public DocumentList FetchDocuments(string homeDir)
{
//Some Codes
if (Directory.Exists(homeDir))
{
//Fetch documents according to homedir
}
else
{
throw new DirectoryNotFoundException(
string.Format("Directory {0} is not found.",homeDir));
}
}
public void Dispose()
{
Console.WriteLine("The service had been disposed.");
}
}
則客戶端在調用如上的服務操作時,如果采用如下的捕獲方式是無法獲取該異常的:
catch (DirectoryNotFoundException ex)
{
//handle the exception;
}
為了彌補這一缺陷,WCF會將無法識別的異常均當作為FaultException異常對象,因此,客戶端可以捕獲FaultException或者Exception異常:
catch (FaultException ex)
{
//handle the exception;
}
catch (Exception ex)
{
//handle the exception;
}
然而,這樣捕獲的異常,卻無法識別DirectoryNotFoundException所傳遞的錯誤信息。尤為嚴重的是這樣的異常處理方式還會導致傳遞消息的通道出現錯誤,當客戶端繼續調用該服務代理對象的服務操作時,會獲得一個CommunicationObjectFaultedException異常,無法繼續使用服務。如果服務被設置為PerSession模式或者Single模式,異常還會導致服務對象被釋放,終止服務。
WCF為異常處理專門提供了FaultContract特性,它可以被應用到服務操作上,指明操作可能會拋出的異常類型。例如前面的服務契約就可以修改為:
[ServiceContract(SessionMode = SessionMode.Allowed)]
public interface IDocumentsExplorerService
{
[OperationContract]
[FaultContract(typeof(DirectoryNotFoundException))]
DocumentList FetchDocuments(string homeDir);
}
然而,即使通過FaultContract指定了操作要拋出的異常,然而如果服務拋出的異常並非FaultException或者FaultException<T>異常,同樣會導致通道發生錯誤。因此在服務實現中,正確的實現應該如下:
public class DocumentsExplorerService : IDocumentsExplorerService,IDisposable
{
public DocumentList FetchDocuments(string homeDir)
{
//Some Codes
if (Directory.Exists(homeDir))
{
//Fetch documents according to homedir
}
else
{
DirectoryNotFoundException exception = new DirectoryNotFoundException();
throw new FaultException<DirectoryNotFoundException>(exception,
new FaultReason(string.Format("Directory {0} is not found.", homeDir)));
}
}
}
我們可以將服務所要拋出的異常類型作為FaultException<T>的類型參數,然後創建一個FaultReason對象用以傳遞錯誤消息。客戶端在調用服務代理對象時,可以捕獲FaultException< DirectoryNotFoundException>異常,並且該異常不會使得通道發生錯誤,並且客戶端可以繼續使用該服務代理對象。即使服務為PerCall服務,客戶端仍然可以繼續調用服務操作。如果服務為Session服務或Singleton服務,那麼即使發生了異常,服務對象也不會被終結。
如果只是為了讓客戶端獲得異常消息,即使不施加FaultContract特性,或者拋出非FaultException異常,我們也可以通過ServiceBehavior特性,將服務的IncludeExceptionDetailInFaults設置為true(默認為false),此時,客戶端可以捕獲拋出的非FaultException異常信息,但該異常仍然會導致通道出現錯誤。
但是,在發布服務與部署服務時,我們應避免將服務的IncludeExceptionDetailInFaults設置為true。
如果不希望使用FaultContract,同時又要保證服務拋出的異常能夠被客戶端捕獲,並且不會導致通道錯誤,我們還可以通過錯誤處理擴展的方式實現。此時,我們可以將服務本身作為錯誤處理對象,令其實現System.ServiceModel.Dispatcher.IErrorHandler接口:
public class DocumentsExplorerService : IDocumentsExplorerService,IErrorHandler, IDisposable
{…}
在該接口的ProvideFault()方法中,可以將非FaultContract異常提升為FaultContract<T>異常,例如將DirectoryNotFoundException異常提升為FaultExceptino<DirectoryNotFoundException>異常:
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
if (error is DirectoryNotFoundException)
{
FaultException<DirectoryNotFoundException> faultException = new FaultException<DirectoryNotFoundException>(
new DirectoryNotFoundException(), new FaultReason(error.Message));
MessageFault messageFault = faultException.CreateMessageFault();
fault = Message.CreateMessage(version,messageFault,faultException.Action);
}
}
而在該接口的HandleError()方法中,則可以處理異常錯誤,例如記錄日志。
要使得錯誤處理擴展生效,還需要向服務通道安裝錯誤處理擴展。方法是讓服務類實現System.ServiceModel.Description.IServiceBehavior接口:
public class DocumentsExplorerService : IDocumentsExplorerService,IErrorHandler,IServiceBehavior,IDisposable
{…}
然後在ApplyDispatchBehavior()方法中安裝錯誤處理擴展:
public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
foreach (ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers)
{
dispatcher.ErrorHandlers.Add(this);
}
}
通過這樣的處理,即使服務拋出的異常為DirectoryNotFoundException異常,並且在服務契約中沒有通過FaultContract特性指定該異常,客戶端同樣能夠獲得異常的錯誤信息,且該異常不會導致通道發生錯誤,客戶端可以繼續調用服務代理對象的操作。