Silverlight を利用してサービスアクセスを行う場合、Silverlight 2 以前ではサービス内で例外が発生してもブラウザーのネットワークスタックの制限により、Silverlight 内でこれらのメッセージの本文にアクセスすることはできず、既定では WCF サービスは、エラーメッセージを HTTP 500 応答コードで返します。
Silverlight 3 からはエラーメッセージをオーバーライドし、詳細なメッセージを取得できるようになりました。
手順としてはやや面倒ですがご紹介します。詳細な内容はこちらを参照してください。MSDN(Silverlight でのエラーの作成と処理)
サンプルプログラムには、前回作成した「SilverlightServiceSample」ソリューションを使用します。
主に変更を行うのは WCF サービス側となります。
最初に、「WcfService1」プロジェクトに「SilverlightFaultBehavior.cs」という名前でクラスを追加します。
○SilverlightFaultBehavior.cs
using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Configuration; using System.ServiceModel.Description; using System.ServiceModel.Dispatcher; namespace WcfService1 { public class SilverlightFaultBehavior : BehaviorExtensionElement, IEndpointBehavior { public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { SilverlightFaultMessageInspector inspector = new SilverlightFaultMessageInspector(); endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector); } public class SilverlightFaultMessageInspector : IDispatchMessageInspector { public void BeforeSendReply(ref Message reply, object correlationState) { if (reply.IsFault) { HttpResponseMessageProperty property = new HttpResponseMessageProperty(); // ここでレスポンスコードを200に変更する property.StatusCode = System.Net.HttpStatusCode.OK; reply.Properties[HttpResponseMessageProperty.Name] = property; } } public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext) { return null; } } public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { } public void Validate(ServiceEndpoint endpoint) { } public override System.Type BehaviorType { get { return typeof(SilverlightFaultBehavior); } } protected override object CreateBehavior() { return new SilverlightFaultBehavior(); } } }
SilverlightFaultBehavior のクラス名は適当でも構いませんが、クラスの構成は上記が最低限となります。
基本的に変更することはあまりないと思われます。
続いて、WCF サービスの Web.config に SilverlightFaultBehavior の定義を追加します。
○Web.config
<system.serviceModel> <extensions> <behaviorExtensions> <add name="silverlightFaults" type="WcfService1.SilverlightFaultBehavior, WcfService1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> </behaviorExtensions> </extensions> <behaviors> <endpointBehaviors> <behavior name="SilverlightFaultBehavior"> <silverlightFaults /> </behavior> </endpointBehaviors> </behaviors> <services> <service name="WcfService1.Service1"> <endpoint address="" binding="basicHttpBinding" contract="WcfService1.IService1" behaviorConfiguration="SilverlightFaultBehavior" /> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> </service> </services> </system.serviceModel>
構成はすべて <system.serviceModel> エレメント内に記述します。
特に重要なのが、<extensions><behaviorExtensions> エレメント内の <add> 要素になります。
<add> 要素の「type」属性には「SilverlightFaultBehavior」クラスを定義した完全修飾名を指定する必要があります。
このとき、各値をカンマ(,)で区切る必要があるのですが、各値同士はスペースで区切られている必要があります。スペースが挿入されていない場合、正しく読み込まれず実行時にカスタム例外を受け取れませんので注意してください。
続いて、<behaviors><endpointBehaviors><behavior> エレメントに定義している <silverlightFaults /> 要素の名称は、<behaviorExtensions> 要素で定義した name 属性と同じ値を指定してください。
最後に、<services><service> エレメント内の <endpoint> 要素に behaviorConfiguration 属性で <behavior> 要素で定義した name 属性の値を指定します。
ここまでで、WCF サービスでエラーを 構成する準備が完了しましたので、続いて実際にエラーを作成してみます。
サービスメソッド内で例外を発生させる場合には、FaultException を使用して例外を発生させることにより、Silverlight 側で例外の詳細を受け取ることができるようになります。
そのため、サービス内で発生した例外はすべてキャッチして、FaultException で再送出する必要があります。
以下に、サービスで例外を発生させるように書き換えてみます。
○IService1.cs
using System; using System.Runtime.Serialization; using System.ServiceModel; namespace WcfService1 { [ServiceContract(Name = "IService1", Namespace = "WcfService1.IService1")] public interface IService1 { [OperationContract] [FaultContract(typeof(ServiceError))] string GetData(int value); } [ServiceContract(Name = "ServiceError", Namespace = "WcfService1.ServiceError")] public class ServiceError { [DataMember] public string ErrorMessage { get; set; } } }
まずはエラーの内容を通知するため、エラー情報を格納するクラスを定義します。
ここでは例として ServiceError という名前でクラス定義していますが、通知したい内容に合わせて自由にクラスメンバをカスタマイズ可能です。
また、クラスメンバにパラメータを持たせる際に DataMember 属性を付加する必要があるため、「参照設定」に「System.Runtime.Serialization」を追加しておいてください。
エラー通知用のクラスを定義した後は、サービスメソッドに「FaultContract」属性を追加します。
「FaultContract」属性を定義することで、サービスメソッドがどんな例外を発生させるかをあらかじめ定義します。
[FaultContract(typeof(ServiceError))]
○Service1.svc.cs
using System; using System.ServiceModel; using System.ServiceModel.Activation; namespace WcfService1 { [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class Service1 : IService1 { public string GetData(int value) { try { return string.Format("You entered: {1}", value); } catch (Exception ex) { throw new FaultException<ServiceError>(new ServiceError() { ErrorMessage = ex.Message }); } } } }
サービスでは処理全体を try で囲み、 例外が発生した場合には FaultException で再送出しています。
上記の例では、string.Format のパラメータが1つにもかかわらず、インデックスに 1 を指定しているため、必ず「FormatException」例外が発生します。
続いては、Silverlight 側のエラー処理を作成します。
まずは、「IService1Async.cs」へ ServiceError クラスの追加を行います。
○IService1Async.cs
using System; using System.Runtime.Serialization; using System.ServiceModel; namespace SilverlightServiceSample { [ServiceContract(Name = "IService1", Namespace = "WcfService1.IService1")] public interface IService1Async { [OperationContract(AsyncPattern = true)] [FaultContract(typeof(WcfService1.ServiceError))] IAsyncResult BeginGetData(int value, AsyncCallback callback, object state); string EndGetData(IAsyncResult result); } } namespace WcfService1 { [ServiceContract(Name = "ServiceError", Namespace = "WcfService1.ServiceError")] public class ServiceError { [DataMember] public string ErrorMessage { get; set; } } }
上記の例で注意しなければならないことは、Silverlight 側で ServiceError の定義を記述するときには、名前空間もサービス側で定義した名称と一致させる必要があります。
サービス側でクラス定義を分離し、Silverlight 側ではソースファイルをリンクで参照するのがよいかもしれません。
APM で記述されたサービス定義については、サービス側と同じく「FaultContract」を付加します。
呼び出し側についてはさほど大きな変更はなく、End メソッドを呼び出した際の例外処理でエラーメッセージを受け取るだけとなります。
○MainPage.xaml.cs
private void MainPage_Loaded(object sender, RoutedEventArgs e) { try { // サービスを呼び出す var asyncResult = _service.BeginGetData(5, async => { Deployment.Current.Dispatcher.BeginInvoke(() => { try { // 戻り値を取得する var result = ((IService1Async)async.AsyncState).EndGetData(async); MessageBox.Show(result, "ExecuteService", MessageBoxButton.OK); } catch (Exception ex) { // エラーメッセージを取得 var error = ex as FaultException<ServiceError>; MessageBox.Show(error != null ? error.Detail.ErrorMessage : ex.Message); } }); }, _service); } catch (Exception ex) { MessageBox.Show(ex.Message); } }
エラーメッセージはサービス側で FaultException<ServiceError> 例外を投げていれば FaultException<ServiceError> に変換可能であるため、キャストして例外を取得できます。
ここで注意しなければならないのは、例外を投げた際の FaultException のコンストラクタによって、reason パラメータを指定していなかった場合には、Exception.Message パラメータには必ず
このフォールトの作成者が Reason を指定しませんでした。
という文字列が格納されています。
これを変更したい場合には、
throw new FaultException<ServiceError>(new ServiceError() { ErrorMessage = ex.Message }, “カスタムエラー”);
throw new FaultException<ServiceError>(new ServiceError() { ErrorMessage = ex.Message },new FaultReason(“カスタムエラー”));
上記を含めた何れかの記述方法で、Exception.Message パラメータの内容を変更することができます。
詳細は MSDN を参照してください。
エラーが発生した場合の実行結果は以下のようになります。
例外の種類が FaultException であった場合、Detail パラメータに FaultContract で指定したクラスが格納されます。
上記の例では FaultException に reason を指定していないため、Message に既定の文字列が指定されていることも確認できます。
以上で、Silverlight でサービスエラーを受け取れるようになりましたが、エラーを表示する際には「公開しても安全な情報」のみとするようにしてください。
本番環境では StackTrace などの重要情報が表示されないよう、十分に注意を払ってください。
コメントを残す
コメントを投稿するにはログインしてください。