首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

Enterprise Library 深入解析與靈活應用(8):透過 WCF 擴展實現與 EHAB 整合(上)

     編輯:關於.NET

前言

在《WCF 技術剖析(卷1)》最後一章的 PetShop 範例中,我展示了如何利用 WCF 擴展實現 AOP、IoC 等模式,並整合企業庫的 Exception Handling Application Block(EHAB)。當時因背景知識限制未深入實作細節,本文將完整說明整合原理與實作。

一、基本原理介紹

在 WCF 分散式應用中,服務端與客戶端需分別處理異常。服務端呼叫 ExceptionPolicy.HandleException 即可輕鬆整合 EHAB。關鍵在於:如何讓 EHAB 處理客戶端呼叫服務時拋出的異常?

客戶端捕獲的異常型別永遠是 FaultException(含泛型版本),而 EHAB 依據異常型別決定處理方式。若服務端不處理直接拋出,客戶端只能捕獲非泛型 FaultException,導致 EHAB 僅能套用單一處理策略。雖然服務端可拋出不同 FaultException<TDetail> 以區分情境,但在快速開發中難以預知所有錯誤場景,這正是需要可配置異常處理框架的原因。

解決方案架構

採用變通機制:服務端異常先經 EHAB 處理,再將處理後資訊(含異常型別 AssemblyQualifiedName)封裝成可序列化的 ServiceExceptionDetail,建立 MessageFault 並回傳 Fault 訊息;客戶端收到後提取資訊,透過反射重建異常並拋出,使客戶端彷彿直接捕獲服務端原始異常,進而套用客戶端設定的 EHAB 策略處理。

核心類型 ServiceExceptionDetail 繼承自 ExceptionDetail,定義為資料契約:

[DataContract(Namespace = "https://www.artech.com/")]
public class ServiceExceptionDetail : ExceptionDetail
{
    public const string FaultSubCodeNamespace = "https://www.artech.com/exceptionhandling/";
    public const string FaultSubCodeName = "ServiceError";
    public const string FaultAction = "https://www.artech.com/fault";

    [DataMember]
    public string AssemblyQualifiedName { get; private set; }

    [DataMember]
    public new ServiceExceptionDetail InnerException { get; private set; }

    public ServiceExceptionDetail(Exception ex) : base(ex)
    {
        this.AssemblyQualifiedName = ex.GetType().AssemblyQualifiedName;
        if (ex.InnerException != null)
            this.InnerException = new ServiceExceptionDetail(ex.InnerException);
    }

    public override string ToString() => this.Message;
}

WCF與EHAB集成實現原理

圖1 WCF 與 EHAB 整合實現原理

註:雖然此做法類似開啟 IncludeExceptionDetailInFaults 可能洩露敏感資訊,但若在異常處理策略中加入屏蔽敏感資訊的處理器,傳遞給客戶端的資訊即為經過處理的安全資料。

二、異常處理、封裝與重建

整個架構需解決三大功能:

  1. 服務端:透過 EHAB 處理原始異常
  2. 服務端:透過 MessageFault 封裝處理後異常並傳遞
  3. 客戶端:重建異常並拋出,套用客戶端 EHAB 策略

透過兩個 WCF 擴展元件實現:自訂 ErrorHandler 處理前兩項,自訂 MessageInspector 處理第三項。

1. 自訂 ErrorHandler 實現基於 EHAB 的異常處理與封裝

public class ServiceErrorHandler : IErrorHandler
{
    public string ExceptionPolicyName { get; private set; }

    public ServiceErrorHandler(string exceptionPolicyName)
    {
        if (string.IsNullOrEmpty(exceptionPolicyName))
            throw new ArgumentNullException(nameof(exceptionPolicyName));
        this.ExceptionPolicyName = exceptionPolicyName;
    }

    public bool HandleError(Exception error) => false;

    public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
    {
        if (error is FaultException) return; // 人為拋出的 FaultException 不再處理

        try
        {
            if (ExceptionPolicy.HandleException(error, this.ExceptionPolicyName))
            {
                fault = Message.CreateMessage(version, BuildFault(error), ServiceExceptionDetail.FaultAction);
            }
        }
        catch (Exception ex)
        {
            fault = Message.CreateMessage(version, BuildFault(ex), ServiceExceptionDetail.FaultAction);
        }
    }

    private MessageFault BuildFault(Exception error)
    {
        ServiceExceptionDetail exceptionDetail = new ServiceExceptionDetail(error);
        return MessageFault.CreateFault(
            FaultCode.CreateReceiverFaultCode(ServiceExceptionDetail.FaultSubCodeName, ServiceExceptionDetail.FaultSubCodeNamespace),
            new FaultReason(error.Message),
            exceptionDetail);
    }
}

ProvideFault 中先排除 FaultException,再呼叫 ExceptionPolicy.HandleException 處理異常,最後透過 BuildFault 封裝成 ServiceExceptionDetail 並建立 Fault 訊息。

2. 自訂 MessageInspector 實現異常重建

public class ExceptionHandlingMessageInspector : IClientMessageInspector
{
    public void AfterReceiveReply(ref Message reply, object correlationState)
    {
        if (!reply.IsFault) return;

        if (reply.Headers.Action == ServiceExceptionDetail.FaultAction)
        {
            MessageFault fault = MessageFault.CreateFault(reply, int.MaxValue);
            if (fault.Code.SubCode.Name == ServiceExceptionDetail.FaultSubCodeName &&
                fault.Code.SubCode.Namespace == ServiceExceptionDetail.FaultSubCodeNamespace)
            {
                var exception = (FaultException<ServiceExceptionDetail>)FaultException.CreateFault(fault, typeof(ServiceExceptionDetail));
                throw GetException(exception.Detail);
            }
        }
    }

    private Exception GetException(ServiceExceptionDetail exceptionDetail)
    {
        if (exceptionDetail.InnerException == null)
        {
            return (Exception)Activator.CreateInstance(
                Type.GetType(exceptionDetail.AssemblyQualifiedName),
                exceptionDetail.Message);
        }

        Exception innerException = GetException(exceptionDetail.InnerException);
        return (Exception)Activator.CreateInstance(
            Type.GetType(exceptionDetail.AssemblyQualifiedName),
            exceptionDetail.Message,
            innerException);
    }

    public object BeforeSendRequest(ref Message request, IClientChannel channel) => null;
}

AfterReceiveReply 驗證 Action 與 SubCode 後,提取 ServiceExceptionDetail 並透過反射重建異常物件。預設呼叫接受 (string, Exception) 建構函式,大多數異常型別均支援;若某異常(如 SqlException)無此建構函式,服務端需以替換機制轉換為可建立的型別。

3. 透過行為套用自訂 ErrorHandler 與 MessageInspector

定義一個同時實作四種行為介面的 Attribute:

public class ExceptionHandlingBehaviorAttribute : Attribute, IOperationBehavior, IContractBehavior, IEndpointBehavior, IServiceBehavior
{
    public string ExceptionPolicyName { get; private set; }

    public ExceptionHandlingBehaviorAttribute(string exceptionPolicyName)
    {
        if (string.IsNullOrEmpty(exceptionPolicyName))
            throw new ArgumentNullException(nameof(exceptionPolicyName));
        this.ExceptionPolicyName = exceptionPolicyName;
    }

    // IOperationBehavior
    public void ApplyClientBehavior(OperationDescription od, ClientOperation co)
        => co.Parent.MessageInspectors.Add(new ExceptionHandlingMessageInspector());

    public void ApplyDispatchBehavior(OperationDescription od, DispatchOperation dop)
        => dop.Parent.ChannelDispatcher.ErrorHandlers.Add(new ServiceErrorHandler(this.ExceptionPolicyName));

    // IEndpointBehavior
    public void ApplyClientBehavior(ServiceEndpoint se, ClientRuntime cr)
        => cr.MessageInspectors.Add(new ExceptionHandlingMessageInspector());

    // IServiceBehavior
    public void ApplyDispatchBehavior(ServiceDescription sd, ServiceHostBase shb)
    {
        foreach (ChannelDispatcher cd in shb.ChannelDispatchers)
            cd.ErrorHandlers.Add(new ServiceErrorHandler(this.ExceptionPolicyName));
    }

    // IContractBehavior
    public void ApplyClientBehavior(ContractDescription cd, ServiceEndpoint se, ClientRuntime cr)
        => cr.MessageInspectors.Add(new ExceptionHandlingMessageInspector());

    public void ApplyDispatchBehavior(ContractDescription cd, ServiceEndpoint se, DispatchRuntime dr)
        => dr.ChannelDispatcher.ErrorHandlers.Add(new ServiceErrorHandler(this.ExceptionPolicyName));

    // 其他介面方法略...
}

可直接套用至作業方法、契約介面或服務類別:

[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);
}

亦可透過配置套用(需定義 BehaviorExtensionElement):

<behaviorExtensions>
  <add name="exceptionHandling" type="Artech.EnterLibIntegration.WcfExtensions.ExceptionHandlingBehaviorElement, Extensions" />
</behaviorExtensions>

三、實例示範

定義異常處理策略 myExceptionPolicy,針對 DivideByZeroException 以 WrapHandler 包裝為自訂 CalculationException:

<exceptionHandling>
  <exceptionPolicies>
    <add name="myExceptionPolicy">
      <exceptionTypes>
        <add type="System.DivideByZeroException, mscorlib" postHandlingAction="ThrowNewException" name="DivideByZeroException">
          <exceptionHandlers>
            <add exceptionMessage="計算錯誤" wrapExceptionType="Artech.WcfServices.Contracts.CalculationException, Artech.WcfServices.Contracts" type="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.WrapHandler, Microsoft.Practices.EnterpriseLibrary.ExceptionHandling" name="Wrap Handler" />
          </exceptionHandlers>
        </add>
      </exceptionTypes>
    </add>
  </exceptionPolicies>
</exceptionHandling>

客戶端程式碼與上篇相同,執行結果:

計算錯誤
InnerException
Type:System.DivideByZeroException
Message:嘗試除以零.

本文完整原始碼可供下載參考。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved