僕はやはり、
「業務エラーは例外で表現するべき」
と考えます。
では、その理由について語っていきます。
【 目次 】
1. 例外の利点
2. 業務エラーとしての例外
3. 注意点
4. まとめ
5. 参考文献
【 用語 】
この記事で使用する用語について、以下のように定義します。
・例外
System.Exceptionから派生する全ての例外クラスを指します。
・エラー
正常系ではないフローを指します。エラーは業務エラーとシステムエラーに大別できます。
・業務エラー
エンドユーザーの操作次第で発生しうるエラーを指します。
業務を遂行する上で間違った操作をエンドユーザーが行った場合に発生するエラーや、エンドユーザーが操作を行っている間に、他のエンドユーザーが何らかの操作を行ったことによって、現在の操作が有効でなくなった場合に発生するエラーなどです。
・システムエラー
システム異常やネットワーク異常、プログラムの欠陥など、エンドユーザーに非がないエラーを指します。
【 1. 例外の利点 】
業務エラーとかシステムエラーとかはひとまず置いといて、まずは「エラー」を例外で表す場合の利点を挙げてみます。
- 例外時の処理コードを分離できる
- バグを検出しやすい
- 詳細な情報を保持している
それぞれ見ていきましょう。
[ 1-1. 例外時の処理コードを隔離できる ]
構造化例外処理は、その名が表すように、「例外処理コードを分離・隔離」することによって「構造化」します。つまり、可読性、保守性に優れるということです。
エラーを戻り値で表す場合はどうでしょうか?
これは、if文やswitch文を用いて処理コードを分離することができます。
しかし、確実に構造化例外処理より劣ります。
構造化例外処理なら、例えば例外をスローする可能性のあるメソッドを呼び出す時、例外が発生したとしても呼び出し元で特に処理する必要がなければ、呼び出し元には例外に関するコードを一切記述する必要がありません。例外はそのまま上位へと伝わり、適切な箇所で処理が行われます。
[ 1-2. バグを検出しやすい ]
例外はハンドルされなければCLRに検出されます。また、ハンドルし損ねた例外を最上位 ( ASP.NETで言えばGlobal.asaxのエラーハンドラ ) で全てハンドルし、ログを残すといったことも可能です。
僕の場合、この最上位のハンドラで例外を検出したら、「予期しない例外が発生しました。」というようなメッセージをエンドユーザーに見せて、裏ではログを残すという手法をよく使います。
エラーが戻り値で帰ってきた場合、そのエラーを適切に処理しなくても、CLRには当然検出されません。すると、どこかで不整合が生じます。その結果アプリケーショ ンがおかしな挙動を起こしたとしても、何が原因なのかは例外に比べて判別がつきづらくなります。また、アプリケーションが ( 一見すると ) 正常な動作を続けてしまうかもしれません。その場合、バグを検出することは困難になります。
[ 1-3. 詳細な情報を保持している ]
デバッグの最中にハンドルされない例外が発生した場合、その例外の各プロパティを参照すれば色々な情報を得ることができます。具体的には、どんな例外なのか ( 型やMessageプロパティ ) 、どのアセンブリで発生したのか ( Sourceプロパティ ) 、どのメソッドで発生したのか ( TargetSiteプロパティ ) 、どういう経緯で発生したのか ( StackTraceプロパティ ) 、例外の元となった例外について ( InnerExceptionプロパティ ) 、といった情報となります。
先ほど書いたように、ハンドルされなかった例外が発生したら例外の情報をログに残すようにすれば、運用時に発覚したバグも調べやすくなります。
エラーを戻り値で表した場合、これらの情報を取得するのは言うまでもなく大変困難です。
【 2. 業務エラーとしての例外 】
ここまでで、例外の利点について見てきました。これらは全て、業務エラーを例外で表した場合にも適用される利点です。
ここからは、業務エラーとしての例外について別の視点から考察していきます。
[ 2-1. 一つのメソッドは一つの機能を表す ]
例えば、「DBへの反映を行うメソッド」があったとします。
このメソッドは「DBへの反映を行うメソッド」であり、「DBへ反映できるようなら反映するメソッド」ではありません。
このメソッドは、楽観同時実行制御を行います。つまり、他者が先にDBを更新していた場合、DBへの反映は行えず、業務エラーとなります。
業務エラーは、一般的に想定されるエラーと見なされます。しかし、想定されるエラーだからと言って戻り値で返してはいけません。想定されていようがいなかろうが、DBへの反映を行うメソッドでDBへの反映ができないのであれば、それは紛れもない「例外的事象」なのです。
「じゃあメソッド名を変えれば良い」と思うかもしれません。例えば先ほど挙げた 「DBへ反映できるようなら反映するメソッド」 というメソッド名でしょうか?でもこれ、一つのメソッドで二つの機能を表してますね。可能であるかどうかを調べ ( 検証 ) 、可能であればHogeを反映 ( 実行 ) するわけです ( 内部のロジックがどうであれ使う側にしてみればそうなります ) 。構造化の観点からすると、一つのメソッドが複数の機能を表すのはよろしくありません。
では、実行用のメソッドの他に検証用のメソッドを用意すればいいかというと、一概にそうとは言えません。
基本的に検証用メソッドは、例外発生のコストによるパフォーマンスの低下が無視できない状況であったり、前もって検証を行いたい理由がある場合 ( これは業務エラーの表現方法に非依存 ) に用意します。
なお、検証メソッドは検証結果を戻り値で返します。検証メソッドは、言うまでもなく検証を行うためのメソッドなので、結果を戻り値で返すのは自然 ( 当然 ) なことです。
[ 2-2. コスト ]
例外にはコストがかかります。
でも、基本的には問題にならないはずです。
例えば、DBの更新を非接続型データアクセスにて行う場合楽観同時実行制御が行われます。この時、他者が既にDBの更新を行っていたために更新に失敗した場合、これは業務エラーとなります。でもこれ、そう頻繁に発生することではありません ( 頻繁に発生するのなら楽観同時実行制御では不適切です ) 。頻繁に発生しない業務エラーで多少レスポンスが悪くても、それは問題にはならないでしょう。
では頻繁に発生する業務エラーがあったとします。これも問題にはなりません。2-1にて前述したように、「例外発生のコストによるパフォーマンスの低下が無視できない状況」では、検証用のメソッドを用意するからです。
例えば、エンドユーザーからの入力値の妥当性検証は必ず行います。エンドユーザーからの入力値が不正な値であることは ( 故意であろうがなかろうが ) よくあることです。入力値の妥当性検証は、パフォーマンスの向上以外にも、可読性の大幅な向上にも繋がります。
ちなみに、入力値の妥当性を検証した後の処理では、入力値は必ず妥当であるはずですので、もし検証した後の処理で入力値が妥当でないことが検出された 場合、それは業務エラーではなくシステムエラーとなります。これを表す例外クラスは、大抵の場合 ArgumentException クラス、またはその派生クラスとなります。
【 3. 注意点 】
ここまでで、業務エラーを例外で表すことの素晴らしさを語ってきました。
次に、業務エラーを例外で表す際の注意点をいくつか挙げます。
[ 3-1. 適切な実装の上で必要となるのは判別情報のみ ]
判別情報、つまり例外の場合は型です。各業務エラーに応じた適切な例外クラスを用意し、それぞれをハンドルできるようにします。
Exception クラスの持つ各プロパティ ( Exception.Message プロパティや Exception.StackTrace プロパティ ) は、業務エラーが適切に処理されている限り利用することはありません。例えば、 Exception.Message プロパティをそのままエンドユーザーに表示したりしてはいけません。例外クラスの持つ各プロパティを利用するのは、例外の処理漏れ ( ハンドルされない例外の発生 ) があった場合となります ( デバッガやログファイルを通して参照します ) 。
なお、これらは業務エラー・システムエラー問わず言える事です。 ( システムエラーの場合、SQLException.Numberプロパティなど、型以外に判別情報を提供している場合もありますが。。。 )
[ 3-2. 業務エラーはExceptionクラスから派生 ]
Microsoftは以前、アプリケーション固有の例外はApplicationExceptionから派生させることを推奨していました。しかし、現在はこれを推奨していません。変わりにExceptionから派生させることを推奨しています。
なぜでしょうか?これは、Microsoftに原因があります。
Microsoftはこのようなルールを推奨しようとしていました。
- CLRがスローする例外は SystemException クラス (System) 派生の例外
- アプリケーションがスローする例外は ApplicationException クラス (System) 派生の例外
しかし、.NET Framework クラスライブラリ ( 以降、FCL ) 自身がこのルールを守れなかったのです。実は、FCLに用意されている例外の中には、ApplicationExceptionから派生しているクラスや、Exceptionクラスから直接派生しているクラスが紛れ込んでいます。FCLがルールを守れていない以上、このルールはもう台無しです。ApplicationExceptionから派生させる意味はなくなります。
【 4. まとめ 】
- 業務エラーは例外としてスローし、上位で適切な処置を行う必要があります。
- 業務エラーを例外で表すことにより、可読性・保守性の向上に繋がります。
- 業務エラーを例外で表しても、適切な設計を行えばパフォーマンスに問題はありません。
- 業務エラーを表す例外に対して適切な処理を行っている限り、型以外の例外情報は不要です。
- 業務エラーを表す例外がハンドルされなかった場合、例外情報がとても重宝します。
- 業務エラーを表す例外はException クラスから派生させます。
【 5. 参考文献 】
プログラミング Microsoft .NET Framework 第2版
この記事を書くに当たって、この書籍の例外に関するページが大変参考になりました。業務エラー・システムエラーという観点では特に書かれていませんが、是非一読してみることをお勧めします。
以下のページから無償でダウンロードできます。
ダウンロードの詳細 : Virtual PC 2007
もちろん、Vista対応です。
IIS7とVirtual PC2007を使って、WCFやASP.NET AJAXで遊んでみるのもいいですね。
R.田中一郎さんの記事を読んで、僕もふと、コーディングのたびに気になる事を思い出しました。
何かと言うと、usingブロック内で得た値を戻り値で返す場合です。
例えば、
public string Foo()
{
using (Hoge hoge1 = new Hoge())
{
hoge1.Fuga = "ふがふが";
string piyoResult = hoge1.Piyo():
return piyoResult;
}
}
と書くか、
public string Foo()
{
string piyoResult;
using (Hoge hoge1 = new Hoge())
{
hoge1.Fuga = "ふがふが";
piyoResult = hoge1.Piyo():
}
return piyoResult;
}
と書くか。
前者はusingブロックの中でreturnして、後者はusingブロックを抜けた後でreturnしてます。
こういう時は、僕の場合前者、つまりusingブロックの中でreturnしてしまいます。
でも、hoge1オブジェクトの操作を終えた後、「他の処理」が少々入る場合は、
public string Foo()
{
using (Hoge hoge1 = new Hoge())
{
hoge1.Fuga = "ふがふが";
string piyoResult = hoge1.Piyo():
string fooResult = piyoResult.Replace('-', '_')
return fooResult;
}
}
と書くのではなく、
public string Foo()
{
string piyoResult;
using (Hoge hoge1 = new Hoge())
{
hoge1.Fuga = "ふがふが";
piyoResult = hoge1.Piyo():
}
string fooResult = piyoResult.Replace('-', '_')
return fooResult;
}
という風に書きます。
理由は、「他の処理」を行う前にusingブロックを抜けることができるのに、無駄にhoge1を延命させてしまうのは無意味だからです。
「他の処理」が複数行に渡る場合、hoge1オブジェクトが既に不要となっていることが一目ではわかりづらいし、「他の処理」が重い場合、それだけ解放処理 ( Dispose ) が遅くなります。
こっちのパターンはこれでいいです。そんな気がします。
でも、最初のパターン、つまり「他の処理」が入らないパターンも、統一してusingブロックの前でreturnするべきな気がしてきます。
いやまてよ・・・、そもそもreturnの位置は重要でなく、usingブロックの範囲が重要なのかな。「他の処理」が入らずreturnするだけなら、usingブロック内に含めても何ら問題ないもんなぁ。その方が可読性は良いし。hoge1オブジェクトを操作する処理が複数行ある場合なんて特にそうですしね。
うん、そんな気がしてきた!そうすると、returnの位置は臨機応変ってことになりますね。
・・・ってことで、結局今まで通りでいいのかな?w
[ エンドポイントを複数用意する方法 ]
エンドポイントを複数用意するには、WCFサービスのコンフィギュレーションに endpoint セクションを複数用意し、address 属性にそれぞれ重複しないように適当な値を設定します。また、必須ではありませんが、各 endpoint セクションの name 属性にてエンドポイントのコンフィギュレーション名を設定しておくと、WCF クライアントアプリケーションのコンフィギュレーションにもそのコンフィギュレーション名が反映され、クライアントで利用しやすくなります。 ( 用意しない場合、クライアントのコンフィギュレーションでは、 "バインディングセット名_コントラクト名" という命名規則に、連番が振られた名称が設定されます。 )
エンドポイントを複数用意する場合、WCF クライアントアプリケーションのサービスプロキシクラスでは、どのエンドポイントを利用するかを明示的に指定する必要があります。これは、サービスプロキ シクラスのコンストラクタで、エンドポイントコンフィギュレーション名を指定します。
[ 例 ]
例えば、一つのWCFサービスでSOAP1.1とSOAP1.2の両方に対応させたい場合、以下のように設定します。
<services>
<service behaviorConfiguration="MyServiceBehavior" name="MyService">
<endpoint name="MyServiceEndpointSoap1.2" binding="customBinding" bindingConfiguration="MyServiceBindingSoap1.2" contract="IMyService" address="soap1.2" />
<endpoint name="MyServiceEndpointSoap1.1" binding="customBinding" bindingConfiguration="MyServiceBindingSoap1.1" contract="IMyService" address="soap1.1" />
</service>
</services>
<bindings>
<customBinding>
<binding name="MyServiceBindingSoap1.2" >
<mtomMessageEncoding messageVersion="Soap12" />
<httpTransport />
</binding>
<binding name="MyServiceBindingSoap1.1" >
<mtomMessageEncoding messageVersion="Soap11" />
<httpTransport />
</binding>
</customBinding>
</bindings>
このWCFサービスのクライアントアプリケーションでは、以下のように、どのエンドポイントを利用するかを明示的に指定する必要があります。
// SOAP1.2 が適用されるエンドポイントを利用する場合
using (MyServiceClient client = new MyServiceClient("MyServiceEndpointSoap1.2"))
{
client.MyOperation1();
}
[ おまけ ]
セルフホストで設定をハードコーディングする場合の例を以下に示します。
僕が一番楽しみにしてたのは、天野さんのセッション「JavaScriptの現在と未来」だったんですが、正直驚きました。
僕は基本的にどのセッションも前のほうの席に座るようにしていて、特に天野さんのセッションは絶対最前列で聴こうと思ってたんですが・・・、開場の5分位前にセッション会場に行ったらもうかなりの行列ですよ。
で、開場して資料を受け取ろうと思ったら資料も品切れ^^; まぁ、天野さんはたぶん後日自身のブログで資料を公開してくれるだろうなと思ってたので資料はさっさと諦め、とにかく前列の方で空いてる席はないかと急ぎ足で前列に向かいました。そしたらなんとか右側の前から3列目くらいをゲットε=(。・д・。)
んで、肝心の内容ですが、かなり面白かったです。「JavaScriptとは豚が空を飛べる言語だ」って言われた時は皆目見当付かずだったのですが、順を追って聴けばなるほど、プロトタイプベースのOOPLならば、既存のコンストラクタ ( クラスベースOOPLで言うトコのクラスのような位置づけ ) そのものも自由に拡張できると言うことを指してたんですね。
あと、プレゼン資料もJavaScript製ということで、FireBugのコンソールから go(50); って命令したらスライドが50枚分早送り。これがまたかっこいい!いいもん見せてもらいましたw
.NET関連のセッションでは、WPFのセッションやASP.NET AJAX 1.0のセッションなどに出席しました。WPFは以前ちょろっと手を出そうと思ってたのですが、結局余裕がなくってやめちゃったんですよね。。。で、実は今までWPFにはそれほど興味を持ってもいなかったのですが、WPFに興味持っちゃいました^^; 早速Microsoft Expression Blend のβ版をインストールして遊んでみたいと思いますw
ASP.NET AJAX 1.0のセッションは、VSUG アカデミースペシャルの時と同じく、MSの鈴木さんがスピーカーを勤めていらっしゃいました。VSUGアカデミースペシャルの時と同じような内容なのかなぁと思いきや、今回は主にWeb標準にフォーカスを当てたセッションとなっており、とても楽しめました。
あと、ASP.NET AJAX 1.0について聴きたいことがあったので、セッション終了後に質問させて頂きました。その際名刺を頂いたのですが、僕はあいにく名刺を持参していなくて交換できなかったです。。。大変失礼なことをしてしまった。。。
ちなみに質問の内容は、「UpdatePanelのコンテナ内に含めるUIコントロールは、必要最低限にした方がいいのか?」という質問で、答えはYes。やはり、無駄にコントロールを格納すると、パフォーマンスダウン、下手すると描写のちらつきに繋がるそうです。横着して画面全体を一つのUpdatePanelに格納するなんてのはやめましょうということです。
ここからは僕の意見ですが、あくまでも「横着して」やるのは避けようということで、画面遷移なんかをAjaxで実現するには、全てを格納する必要があるでしょう。その場合も、個々のコントロールに対してUpdatePanelを用意することを怠らないようにしましょう。 ( UpdatePanelは入れ子にできます。ただし、ScriptManagerは1画面に1個だけ用意します。 )
何はともあれ、無事出席できてよかった~。おかげで今週休出になりそうだけど^^;
「.NET開発者への道 ~ Web開発編」を2月5日(月)に開始
これに伴って、ASP.NET Developer Centerがリニューアルされました。
ASP.NET Developer Center
さらに、MSDNフォーラムにASP.NETのフォーラムが開設されました。
ASP.NET - MSDNフォーラム
[ 情報元 ]
Hiroyasu Kitagawa's Blog : .NET開発者への道 ~ Web開発編
「.NET開発者への道~Web開発編」が本日からスタート。
[ WCFにおける例外 ]
WCFにおけるアプリケーション固有のエラーは、全てFaultException クラスまたはFaultException ジェネリック クラスとしてクライアントに渡されます。
サービスコントラクトを開発する際は、これらの例外を明示的にスローすることで、クライアントに例外を通知します。
それ以外の例外がスローされた場合、その例外の情報はクライアントには隠蔽され、代わりにFaultExceptionが渡されます。 ( 詳しくは後述 )
[ FaultException クラス ]
FaultException クラス (System.ServiceModel) を明示的にスローすることで、例外メッセージや例外コード ( 例外の原因を識別するためのコード ) をクライアントに通知することができます。
例外メッセージは、コンストラクタの引数 FaultReason にて指定します。このFaultReasonには、直接文字列を指定することも、FaultReason クラスを指定することもできます。FaultReason クラスを使用すると、クライアントのカルチャに応じたメッセージを渡すことができます。 ( 詳しくは後述 )
引数 FaultReason に指定された例外メッセージは、通常の例外と同じく、Message プロパティから取得することができます。また、Reason プロパティからFaultReason型のオブジェクトとして取得することもできます。
例外コードは、コンストラクタの引数 FaultCode にて指定します。このFaultCodeの型は、FaultCode クラス です。 ( 詳しくは後述 )
引数 FaultCode に指定された例外コードは、Code プロパティからFaultCode型のオブジェクトとして取得することができます。
[ FaultReason クラス ]
FaultReason クラス (System.ServiceModel) には、各カルチャ用のメッセージを用意して格納することが可能です。以下の例のように、複数のカルチャ用にメッセージを用意してやることで、例外を受け取るクライアントでは、クライアントのカルチャに応じたメッセージをMessageプロパティから取得することができます。
サービスコントラクト// using System;
// using System.Collections.Generic;
// using System.Globarization;
// using System.ServiceModel;
public void MyOperation1()
{
FaultReasonText reasonTextJA = new FaultReasonText("ほげ ふが ぴよ", new CultureInfo("ja-JP"));
FaultReasonText reasonTextEN = new FaultReasonText("hoge fuga piyo", new CultureInfo("en-US"));
FaultReasonText[] translations = new FaultReasonText[] { reasonTextJA, reasonTextEN };
FaultReason reason = new FaultReason(translations);
FaultException ex = new FaultException(reason);
throw ex;
}
こうやってスローされたFaultExceptionは、クライアントでは、カレントカルチャに従って適切なメッセージを提供します。以下の例のようにカレントカルチャを明示的に指定してみると簡単に確認できます。
クライアント// using System;
// using System.Globarization;
// using System.ServiceModel;
// using System.Threading.Thread;
using (MyServiceClient myServiceClient = new MyServiceClient())
{
try
{
myServiceClient.MyOperation1();
}
catch (FaultException ex)
{
Thread.CurrentThread.CurrentCulture = new CultureInfo("ja-JP")
Console.WriteLine(ex.Message); // ほげ ふが ぴよ
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US")
Console.WriteLine(ex.Message); // hoge fuga piyo
}
}
[ FaultCode クラス ]
FaultCode クラス (System.ServiceModel) は、例外の原因を識別するためのコードを表します。
このクラスのインスタンスは、FaultCode クラスの静的メソッドを使用して取得するか、コンストラクタを使用して取得することができます。
・静的メソッド
例外の原因が要求側 ( クライアント ) または受信側 ( サーバー ) にあることを示す例外コードを生成する場合、FaultCode クラスの静的メソッドを使用します。
FaultCode クラスの静的メソッドには、次の2種類があります。
FaultCode.CreateSenderFaultCode メソッド
例外の原因が要求側 ( クライアント ) にあることを示すFaultCodeオブジェクトを生成します。このメソッドによって生成されたFaultCodeオブジェクトのIsSenderFault プロパティは、trueを返します。
FaultCode.CreateReceiverFaultCode メソッド
例外の原因が受信側 ( サーバー ) にあることを示すFaultCodeオブジェクトを生成します。このメソッドによって生成されたFaultCodeオブジェクトのIsReceiverFault プロパティ は、trueを返します。
これらの静的メソッドは、引数にサブコードを ( FaultCode型、または文字列で ) 指定することができます。特に必要ない場合は、nullを指定します。
・コンストラクタ
独自のコードを作成する場合、FaultCode クラスのコンストラクタを使用します。
コンストラクタを使用する場合、引数 name に独自のコードを文字列で指定します。
コンストラクタによって生成されたFaultCodeオブジェクトのIsSenderFault プロパティとIsReceiverFault プロパティは、falseを返します。
コンストラクタの場合も、サブコードを指定することが可能です。特に必要ない場合は、サブコードを引数として取らないコンストラクタを使用するか、引数 subCode にnullを指定します。
[ FaultException ジェネリック クラス ]
FaultException ジェネリック クラス (System.ServiceModel) はFaultException クラスの派生クラスです。このクラスを明示的にスローすることで、例外メッセージや例外コードに加え、例外の詳細をクライアントに通知することができます。
FaultException ジェネリック クラスのジェネリック型パラメータ TDetail に指定された型は、Detail プロパティの型になります。クライアントは、このDetail プロパティを通じて例外の詳細を知ることができます。ただしそのためには、以下の例のようにFaultException ジェネリック クラスをスローする可能性のあるオペレーションコントラクトに、FaultContractAttribute クラス (System.ServiceModel) を属性として付加し、引数 detailType を指定する必要があります。
[ServiceContract]
public interface IMyService
{
[OperationContract]
[FaultContract(typeof(MyDetail))]
void MyOperation1();
}
ジェネリック型パラメータ TDetail には、ユーザー定義の型を指定することもできます。その場合、ユーザー定義の型は以下の例のようにデータコントラクトとして宣言する必要があります。
// using System;
// using System.ServiceModel;
[DataContract]
public class MyDetail
{
private string message;
[DataMember]
public string Message
{
get
{
return this.message;
}
set
{
this.message = value;
}
}
public MyDetail(string message)
{
this.message = message;
}
}
これらを適切に行うことで、クライアント側でもFaultException ジェネリック クラスを適切に利用することが可能になります。
なお、ジェネリック型パラメータ TDetail にExceptionDetail クラス (System.ServiceModel) を指定したり、Exception クラスまたはその派生クラスを指定することはしてはいけません。WCFサービス内で発生した例外情報を直接クライアントに公開することはセキュリティ的によろしくないですし、スタックトレースなどの例外情報はクライアント側には不要のはずです。FaultException クラスのコンストラクタにInnerExceptionを指定することができないのも、同様の理由からでしょう。
[ WCFでは例外情報を無闇に公開しない ]
WCFでは通常、セキュリティの観点から、FaultException クラスまたはFaultException ジェネリック クラス以外のハンドルされなかった例外の情報は、クライアントに一切渡さず、代わりに、ハンドルされなかった例外が発生したことを示すFaultException 例外オブジェクトを渡します。
ただし、これではデバッグ時に不便である場合があるため、例外の情報をクライアントに渡すように設定することもできます。
プロジェクトを、クラスライブラリではなく Webサービスとして作成した場合の初期設定では、例外の情報をクライアントに渡すように設定されているので注意します。
[ system.serviceModel ] - [ behaviors ] - [ serviceBehaviors ] - [ behavior ] - [ serviceDebug ] セクションのincludeExceptionDetailInFaults属性が、ハンドルされなかった例外の情報をクライアントに渡すように設定するための属性です。
この属性にtrueが設定されている場合、FaultException クラスまたはFaultException ジェネリック クラス以外のハンドルされなかった例外はFaultException ジェネリック クラスとしてスローされます。この時、ジェネリック型パラメータ TDetail はExceptionDetail クラス (System.ServiceModel) となります。クライアントでは、FaultException ジェネリック クラスのDetailプロパティにて取得したExceptionDetailオブジェクトから、ハンドルされなかった例外の情報を詳しく調べることができます。
以下に、設定例を示します。
<system.serviceModel>
<services>
<service name="MyService" behaviorConfiguration="MyService_ServiceBehaviors">
<endpoint contract="IMyService" binding="wsHttpBinding"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="MyService_ServiceBehaviors" >
<serviceDebug includeExceptionDetailInFaults="true" />
<serviceMetadata httpGetEnabled="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
[ 例外の通信 ]
WCFサービス内でハンドルされなかった例外は、SOAPメッセージに変換され、クライアントへと渡されます。
この時、ハンドルされなかった例外がFaultException クラス、またはFaultException ジェネリック クラスなら、以下の情報がSOAPメッセージの中に記述されます。
- Reason プロパティ
- Code プロパティ
- Action プロパティ
- Detail プロパティ ( FaultException ジェネリック クラスの場合 )
ハンドルされなかった例外が、FaultException クラス、またはFaultException ジェネリック クラスでない場合、serviceDebug セクションのincludeExceptionDetailInFaults 属性の値に応じて次のような処理が行われます。
ハンドルされなかった例外情報はSOAPメッセージの中には記述されません。代わりに、ハンドルされなかった例外が発生したことを示すFaultException例外オブジェクトが生成・スローされ、その例外情報がSOAPメッセージの中に記述されます。
・trueの場合
ハンドルされなかった例外情報を元にFaultException<ExceptionDetail>例外オブジェクトが生成・スローされ、その例外情報がSOAPメッセージの中に記述されます。
こうやってできあがったSOAPメッセージは、クライアントへと渡されます。
このSOAPメッセージを受け取ったクライアントは、SOAPメッセージを解析し、新たにFaultException クラスまたはFaultException ジェネリック クラスを生成し、スローします。
つまり、クライアントでハンドルしたFaultException クラスの例外オブジェクトまたはFaultException ジェネリック クラスの例外オブジェクトは、クライアントで生成・スローされたことになります。よって、StackTraceプロパティなどに格納されている情報はクライアントでスローされた時の情報であり、サーバーの例外情報ではありません。先にも述べたように、サーバーから渡される例外情報は、Reason プロパティ, Code プロパティ, Action プロパティ, Detail プロパティのみです。
[ まとめ ]
WCFの例外処理において、以下の3点には特に注意する必要があります。
- FaultException ジェネリック クラスをスローする可能性のあるオペレーションコントラクトでは、FaultContractAttribute属性を付加します。
- FaultException ジェネリック クラスの型引数にExceptionDetail クラスやException クラスを指定して、WCFサービス内で発生した例外の詳細をそのまま公開してはいけません。
- 運用時にはserviceDebug セクションのincludeExceptionDetailInFaults 属性の値はfalseにします。