Silverlight 3 + WCF 的例外處理

  • 20460
  • 0

在發開Silverlight Client(簡稱前端)與使用WCF Web Service(簡稱後端)時,有時候後端在執行過程中發生了例外狀況(Exception),雖然前端可以從System.ComponentModel.AsyncCompletedEventArgs.Error是否為Null知道有沒有例外發生,但收到的錯誤訊息永遠都是
The remote server returned an error: Not Found.
這樣的訊息要怎麼在前端回報錯誤,因為你根本不知道後端發生了什麼錯誤,以前為了要收到詳細的錯誤訊息,必需要自己處理如建立一些Class,如範例1,非常的麻煩,所以我想很多人都懶的處理錯誤訊息了,只產生一種訊息「伺服器發生錯誤」來打發用戶,這在Silverlight 3 這個情況已有所改變,但不是預設的必需有隔外的處理。

範例程式碼可從http://ppt.cc/a998 下載

  • 現況- Silverlight + WCF 例外處理的問題。
  • 怎麼回事-為什麼Silverlight 無法處理例外(Exception)
  • 如何改善-增加自訂Behavior來改變狀態碼(StatusCode)500改成200
  • 更豐富的錯誤訊息。
  • 延伸討論-怎麼不用一個一個加Try Catch來補捉例外
  • 延伸討論-回報伺服器狀態。

現況 - Silverlight + WCF 例外處理的問

在發開Silverlight Client(簡稱前端)與使用WCF Web Service(簡稱後端)時,有時候後端在執行過程中發生了例外狀況(Exception),雖然前端可以從System.ComponentModel.AsyncCompletedEventArgs.Error是否為Null知道有沒有例外發生,但收到的錯誤訊息永遠都是

The remote server returned an error: Not Found.

這樣的訊息要怎麼在前端回報錯誤,因為你根本不知道後端發生了什麼錯誤,以前為了要收到詳細的錯誤訊息,必需要自己處理如建立一些Class,如範例1,非常的麻煩,所以我想很多人都懶的處理錯誤訊息了,只產生一種訊息「伺服器發生錯誤」來打發用戶,這在Silverlight 3 這個情況已有所改變,但不是預設的必需有隔外的處理。

 

[DataContract]
public class ResultMessage<T>
{
      [DataMember]
      public string Message { get; set; }
      [DataMember]
      public bool IsError { get; set; }
      [DataMember]
      public T Result { get; set; }
}

[OperationContract]
public void DoWork()
{
      ResultMessage<List<string>> result = new ResultMessage<List<string>>();
      try
      {
            //Do Nothing...
            result.Message = "正常結束";
      }
      catch (Exception ex)
      {
            result.IsError = true;
            result.Message = ex.Message;
      }
}

範例1 傳統在Silverlight上回傳訊息的方法

 

怎麼回事-為什麼Silverlight 無法處理例外(Exception)

為什麼會產生Not Found的訊息呢?有二個主要的問題:

  • 無法將例外轉換成SOAP
  • 阻擋狀態碼500的回應

在Silverlight,當後端丟出例外後,HTTP的回應狀態碼[1]為變成500,而Silverlight的Http網路堆疊(Networking Stack[2])只處理狀態碼200(OK)及404(Not Found),而狀態碼500的回應Silverlight Client不會處理,由後端所丟出的Exception其中的訊息,無法傳遞給前端,另一個問題是Silverlight Client無辦法讀取SOAP Fault[3],所以就算收到了訊息也沒有辦法轉成Exception。

 

如何改善-增加自訂Behavior來改變狀態碼(StatusCode)500改成200

在Silverlight 3 改善了Silverlight 2無法處理SOAP Fault的功能,所以我們只要處理的問題,將狀態碼500改成200,使前端可以順利的讀取由後端例外產生的SOAP Fault。

操作可分為下幾個步驟

一、建立SilverlightFaultBehavior專案

1.建立新的Class Library專案,命名為SilverlightFaultBehavior

Note:
也可以不必建立Class Library專案,放在Web專案下也可以,只是在設定時必要指定類別與組件的全名,為了日後Web專案有所變動,使類別與組件的全名與設定不相符合,而影響系統的運行外也可以增加複用性,可以方便移到別的專案使用

 

2.加入System.ServiceModel與System.Configuration組件參考。

3. 在SilverlightFaultBehavior專案下新增Class項目,命名為SilverlightFaultMessageInspector.cs。

4. SilverlightFaultMessageInspector類別實作System.ServiceModel.Dispatcher.IDispatchMessageInspector[4]介面,在BeforeSendReply加入發生例外時可將狀態碼改成200,完成如範例2。

 

/// <summary>
///
可以在服務應用程式中啟用傳入和傳出應用程式訊息的自訂檢查或修改
/// </summary>
public class SilverlightFaultMessageInspector : IDispatchMessageInspector
{
      /// <summary>
      /// 會在作業回傳之後但傳送回覆訊息之前初始化呼叫。
      /// </summary>
      public void BeforeSendReply(ref Message reply, object correlationState)
      {
            if (reply.IsFault)
            {
                  HttpResponseMessageProperty property = new HttpResponseMessageProperty();
                  /// 修改StatusCode為200.
                  property.StatusCode = System.Net.HttpStatusCode.OK;
                  reply.Properties[HttpResponseMessageProperty.Name] = property;
            }
      }

      /// <summary>
      /// 在收到傳入訊息之後但分派該訊息至預定作業之前初始化呼叫
      /// </summary>           
      public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
      {
            /// 沒有用到
            return null;
      }
}

範例2 實作System.ServiceModel.Dispatcher.IDispatchMessageInspector介面

 

4.在SilverlightFaultBehavior專案下新增Class項目,命名為FaultBehavior.cs。

5. FaultBehavior類別繼承System.ServiceModel.Configuration.BehaviorExtensionElement[5]類別並覆寫成員,使FaultBehavior可以加入Web.Config中,完成如範例3。

public class FaultBehavior : BehaviorExtensionElement
{
      public override Type BehaviorType
      {
            get { return typeof(FaultBehavior); }
      }

      protected override object CreateBehavior()
      {
            return new FaultBehavior();
      }
}

範例3 繼承System.ServiceModel.Configuration.BehaviorExtensionElement類別並覆寫成員。

 

6.FaultBehavior類別實作System.ServiceModel.Description.IEndpointBehavior[6],在ApplyDispatchBehavior方法中自訂訊息的處理,完成如範例4

#region IEndpointBehavior Members
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
      ///讓錯誤可以正常傳到前端
      endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new SilverlightFaultMessageInspector());
}

public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}

public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
}

public void Validate(ServiceEndpoint endpoint)
{
}
#endregion

範例4 實作System.ServiceModel.Description.IEndpointBehavior介面。

 

Note:
要自訂Behavior可實作的介面有
System.ServiceModel.Description.IEndpointBehavior 實作可用於延伸服務或用戶端應用程式中端點的執行階段行為的方法。
System.ServiceModel.Description.IContractBehavior 可用於在服務或用戶端應用程式中延伸合約的執行階段行為的實作方式。
System.ServiceModel.Description.IOperationBehavior 用於延伸服務或用戶端應用程式中作業的執行階段行為。
System.ServiceModel.Description.IServiceBehavior 提供機制以在整個服務上修改或插入自訂延伸

 

二、建立Silverlight Application專案

 

1. 在方案中加入Silverlight Application的新專案,命名為SilverlightAndWcfErrorPolicy

2.使用建立新Web Project選項,建立名為SilverlightAndWcfErrorPolicy.Web專案

圖1 建立新的Web Project

 

3.在SilverlightAndWcfErrorPolicy.Web上新增Silverlight-enabled WCF Service項目,命名為Service.svc。

圖2新增Silverlight-enabled WCF Service項目。

 

4.在Service.svc.cs中寫入必發生例外的方法如範例5

public class Service
{
      [OperationContract]
      public void DoWork()
      {
            throw new InvalidProgramException("發生例外");
      }
}

範例5 Service.svc方法。


5.修改Web.Config,WCF在Web.Config的項目為system.serviceModel,通常位於結尾,在加入Silverlight-enabled WCF Service時會自動加入,修加4個部分如範例6。

<system.serviceModel>
      <!--
1.增加擴展-->
      <
extensions>
          <
behaviorExtensions>
            <
add name="FaultBehavior" type="SilverlightFaultBehavior.FaultBehavior, SilverlightFaultBehavior, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          </behaviorExtensions>
      </
extensions>
      <
behaviors>
          <
endpointBehaviors>
            <!--
2.增為行為-->
            <
behavior name="FaultBehavior">
                <FaultBehavior />
            </
behavior>
          </
endpointBehaviors>
          <
serviceBehaviors>
            <
behavior name="SilverlightApplication.Web.ServiceBehavior">
                <serviceMetadata httpGetEnabled="true" />
                <!--3.使用完整錯誤訊息-->
                <
serviceDebug includeExceptionDetailInFaults="true" />
            </behavior>
          </
serviceBehaviors>
      </
behaviors>
      <
bindings>
          <
customBinding>
            <
binding name="customBinding0">
                <binaryMessageEncoding />
                <
httpTransport>
                  <
extendedProtectionPolicy policyEnforcement="Never" />
                </httpTransport>
            </
binding>
      </
customBinding>
      </
bindings>
      <
serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
      <services>
          <
service behaviorConfiguration="SilverlightApplication.Web.ServiceBehavior"
                          name="SilverlightApplication.Web.Service">
            <!--4.使用行為-->
            <
endpoint address="" binding="customBinding" bindingConfiguration="customBinding0"
                        behaviorConfiguration="FaultBehavior"
                        contract="SilverlightApplication.Web.Service" />
            <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
          </service>
      </
services>
</
system.serviceModel>

範例6 WCF於Web.Config設定

 

Note:
於3. serviceDebug 的includeExceptionDetailInFaults的選項如果為False時,將不會將Exception轉換成Soap,也可以如下
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class Service{}

5.測試Service.svc,看有沒有錯誤,OK做到這裡以完成了所需要的步驟,現在我們就在後端測測看,是不是可以取得後端的例外。

 

三、Silverlight中測試

1.增加Web參考,按下Discover 找到SilverlightAndWcfErrorPolicy.Web的Service.svc,命名空間命名為WcfService。

2.在MainPage.xaml下增加TextBlock,命名為MessageTextBlock。

3.在MainPage.xaml.cs,呼叫WcfService.DoWork,然後將錯誤訊息顯示在MessageTextBlock上,完成如範例7。

private void OnLoaded(object sender, RoutedEventArgs e)
{
      ServiceClient client = new ServiceClient();
      client.DoWorkAsync();
      client.DoWorkCompleted += new EventHandler<AsyncCompletedEventArgs>(OnClientDoWorkCompleted);
}

private void OnClientDoWorkCompleted(object sender, AsyncCompletedEventArgs e)
{
      if (e.Error !=null)
      {
            this.MessageTextBlock.Text = e.Error.Message;
      }
}

範例7 測試

4.可以從圖3中的Message如同後端例外的訊息。

圖3 測試結果

 

更豐富的錯誤訊息

仔細點你會發現,e.Error的例外類別是System.ServiceModel.FaultException,那是因為.Net的例外是行程內(In Process)的錯誤處理機制,對於WCF這類跨平台的服務,會轉換成SOAP Fault,而轉換的內容只有

  • InnerException    
  • Message
  • StackTrace

這三個欄位,如果想要有更多訊息,就必需自訂有宣告DataContractAttribute的類別來完成。

1.在SilverlightAndWcfErrorPolicy.Web中加入System.Runtime.Serialization的參考。

2.建立FaultMessage類別,然後在Service新增DoWork2方法與加上[FaultContract(typeof(FaultMessage))]屬性宣告,如範例8。

[DataContract]
public class FaultMessage
{
      [DataMember]
      public string Where { get; set; }
      [DataMember]
      public string Owner { get; set; }
      [DataMember]
      public string How { get; set; }
}

[OperationContract]
[FaultContract(typeof(FaultMessage))]
public void DoWork2()
{
      throw new FaultException<FaultMessage>(new FaultMessage() { Owner = "Me", How = "Shutdown", Where = "There" }, "自定訊息");
}

範例8自訊息

3.後端在新增呼叫DoWork2的處理,就可以從e.Error收到自定義FaultMessage的訊息,如圖4。

圖4 FaultMessage訊息

 

延伸討論-怎麼不用一個一個加Try Catch來補捉例外

開發程式,為了方便除錯寫Log是非常重要的一件事,畢竟不是什麼時候都可以使用Visual Studio下中斷點去Debug,可是WCF的錯誤無法用ASP.NET的Global.asax中Application_Error事件處理常式中處理,必需在每一個方法中加入Try Catch對每一個方法作Log記錄,方法多就覺得同樣的事情寫在每一個方法中很不妥,WCF難到沒有全域的錯誤處理嗎?

有的,可以實作System.ServiceModel.Dispatcher.IErrorHandler,自定一個類別來處理錯誤。

1.在SilverlightAndWcfErrorPolicy中新增ErrorHandler類別實作System.ServiceModel.Dispatcher.IErrorHandler。

2.在ProvideFault寫Log的處理。

3.同範例4在FaultBehavior的ApplyDispatchBehavior加入ErrorHandler如範例9。

public class ErrorHandler : IErrorHandler
{
      public bool HandleError(System.Exception error)
      {
            return true;
      }

      public void ProvideFault(System.Exception error, MessageVersion version, ref Message fault)
      {
            //Log處理
      }
}

public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
      //錯誤事件的捕捉
      endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(new ErrorHandler());
      //讓錯誤可以正常傳到前端
      endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new SilverlightFaultMessageInspector());
}

範例9錯誤事件的捕捉

 

延伸討論-回報伺服器狀態

有時會有必需更新資料庫,必需讓使用者停止使用,有試過

  • 停掉IIS Web站台
  • 加入App_offline.htm

但是這些在Silverlight上沒有辦法得到正確的訊息,後來想到一樣也是判斷檔案,但是是自己定的檔案如Offline.lock,每一次後端呼叫時,會檢查目錄下有沒有Offline.lock檔案,如果有就丟出含有資料庫更新訊息的FaultException,前端在將含有收到是資料庫更新的FaultException,回報請使用者知道。

1.在SilverlightFaultMessageInspector(範例2)的AfterReceiveRequest方法中加入判斷,因為在處理需求前就發生例外,所以不會執行使用者呼叫的方法,如範例 10

/// <summary>
///
在收到傳入訊息之後但分派該訊息至預定作業之前初始化呼叫
/// </summary>
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
     ///用Substring(6)要去掉 file:\\,因為Path類別不支援URI的格式。
      string path = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).Substring(6);
      if (File.Exists(Path.Combine(path, "..\\Offline.lock")))
      {
            throw new FaultException("Database Update", null, "DbUpdate");
      }
      else
      {
            return null;
      }
}
 

範例10 回執停機的訊息。


2.在前端增加訊息的顯示,如範例11

FaultException fe = e.Error as FaultException;

if (fe.Action == "DbUpdate")
{
MessageBox.Show("資料庫現在更新,請稍後在操作");
}

範例11 回報使用者。

 

參考資