在上一篇文章中,我們詳細介紹了如何透過自訂 ClientMessageInspector 與 ErrorHandler,實現 WCF 與微軟企業庫中 Exception Handling Application Block(EHAB)的整合。核心思路是:當服務端拋出異常時,利用 EHAB 依據預設的異常處理策略進行處理;接著將處理後的異常封裝進 ServiceExceptionDetail 物件,序列化後置入 Fault 訊息回傳給客戶端;客戶端收到 Fault 訊息後,提取並重建異常,最後拋出以便依據客戶端設定的策略進一步處理。
為了讓 WCF 能正確序列化與反序列化 ServiceExceptionDetail,必須透過 FaultContractAttribute 將其定義為錯誤契約,如下所示:
[ServiceContract(Namespace = "https://www.artech.com/")]
public interface ICalculator
{
[OperationContract]
[ExceptionHandlingBehavior("myExceptionPolicy")]
[FaultContract(typeof(ServiceExceptionDetail), Action = "https://www.artech.com/fault")]
int Divide(int x, int y);
}
若服務提供給第三方使用,這類錯誤契約定義是必要的,因為型別資訊需透過中繼資料發布。但若服務僅供內部應用呼叫,我們可以在執行期動態加入錯誤描述,免去在每個作業方法上重複標註 Attribute。
我們可以繼承 ServiceHost,在 OnOpening 方法中為所有作業加入基於 ServiceExceptionDetail 的錯誤描述:
public class ExceptionHandlingServiceHost : ServiceHost
{
public ExceptionHandlingServiceHost(Type t, params Uri[] baseAddresses)
: base(t, baseAddresses) { }
protected override void OnOpening()
{
base.OnOpening();
foreach (ServiceEndpoint endpoint in this.Description.Endpoints)
{
foreach (OperationDescription operation in endpoint.Contract.Operations)
{
FaultDescription faultDescription = new FaultDescription(ServiceExceptionDetail.FaultAction);
faultDescription.DetailType = typeof(ServiceExceptionDetail);
operation.Faults.Add(faultDescription);
}
}
}
}
邏輯簡單:遍歷所有端點與作業,加入指定 Action 的 FaultDescription。此自訂 ServiceHost 適用於非 IIS/WAS 託管情境;若需在 IIS/WAS 中使用,則需搭配 ServiceHostFactory:
public class ExceptionHandlingServiceHostFactory : ServiceHostFactory
{
protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
{
return new ExceptionHandlingServiceHost(serviceType, baseAddresses);
}
}
客戶端同樣需要錯誤描述以利反序列化,我們透過覆寫 CreateDescription 方法實現:
public class ExceptionHandlingChannelFactory<TChannel> : ChannelFactory<TChannel>
{
public ExceptionHandlingChannelFactory(string endpointConfigurationName)
: base(endpointConfigurationName) { }
protected override ServiceEndpoint CreateDescription()
{
ServiceEndpoint serviceEndpoint = base.CreateDescription();
foreach (OperationDescription operation in serviceEndpoint.Contract.Operations)
{
FaultDescription faultDescription = new FaultDescription(ServiceExceptionDetail.FaultAction);
faultDescription.DetailType = typeof(ServiceExceptionDetail);
operation.Faults.Add(faultDescription);
}
return serviceEndpoint;
}
}
移除介面上的 FaultContractAttribute:
[ServiceContract(Namespace = "https://www.artech.com/")]
public interface ICalculator
{
[OperationContract]
[ExceptionHandlingBehavior("myExceptionPolicy")]
int Divide(int x, int y);
}
服務託管時直接使用自訂 Host:
using (ExceptionHandlingServiceHost host = new ExceptionHandlingServiceHost(typeof(CalculatorService)))
{
host.Open();
Console.Read();
}
客戶端使用自訂 ChannelFactory 建立 Proxy:
using (ExceptionHandlingChannelFactory<ICalculator> channelFactory = new ExceptionHandlingChannelFactory<ICalculator>("calculatorservice"))
{
ICalculator calculator = channelFactory.CreateChannel();
using (calculator as IDisposable)
{
try
{
int result = calculator.Divide(1, 0);
}
catch (CalculationException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine("InnerException");
Console.WriteLine("\tType:{0}", ex.InnerException.GetType());
Console.WriteLine("\tMessage:{0}", ex.InnerException.Message);
}
}
}
執行結果與上篇一致:
計算錯誤
InnerException
Type:System.DivideByZeroException
Message:嘗試除以零.
本文完整原始碼可供下載參考。