C#と諸々

C#がメインで他もまぁ諸々なブログです
おかしなこと書いてたら指摘してくれると嬉しいです(´・∀・`)
つーかコメント欲しい(´・ω・`)

2008/10/15 00:13
以前業務エラーについての記事を書いてから、もう 1 年半以上が経ちます。
今でもこの記事にコメントを頂くことがあり大変有難いのですが、実は、現在はまたもや異なる見解、というか捉え方をしています。(何度もすみません…)

といっても、「業務エラーは戻り値で表すべき」という考えになったというわけではありません。
以前は業務エラーは「戻り値」で表すべきか「例外」で表すべきかについて悩んでいましたが、今の僕は、そもそも業務エラーを表すのはこのどちらでもないと考えています。

まず、
  • 戻り値は、メソッドが結果を返すためのものです
  • 例外は、メソッドが例外的事象を通知するためのものです
このことは以前の記事にも書いてあります。
そして、これが全てです。
戻り値も例外も、業務エラーを表すためのものではないのです。


これだけじゃイマイチ伝わらないかもしれませんね。
ではここから、ドメイン駆動設計 (DDD) という設計思想を例に取ってみます。
書籍「ドメイン駆動」によると、DDD プロジェクトの典型的なレイヤ分割では、次の 4 つのレイヤに分割されるとしています。(直接的にはあまり深く語られていないので、以下は僕なりの解釈です。)

[ User Interface レイヤ ]
その名の通り UI に関するレイヤです。MVC における View に相当します。
テキストボックスの入力値を取得したり設定したり、ラベルの表示・非表示を切り替えたり、といった直接的な UI 操作のみを行います。

[ Application レイヤ ]
アプリケーションとしての動作に関するレイヤです。MVC における Controller に相当します。
User Interface レイヤ、Domain レイヤ、Infrastructure レイヤを操作します。アプリケーションとしての中核を担います。
例えば、図書館の貸出管理システムにおいて、貸出画面で複数の図書を一度に貸出できるようにするのか、一冊ずつしか貸出できないようにするのかは、アプリケーションの仕様に左右されます。一度に複数の貸出ができるようにするために、図書オブジェクトを一時的にコレクションに格納するといったロジックは、「アプリケーションとしての動作」のためのものです。(*1)

[ Domain レイヤ ]
ドメインモデルに関するレイヤです。MVC における Model に相当します。
例えば、貸出期限や貸出可能図書数などは図書館の貸出規定として決められていることです。貸出可能図書数を超えてしまう場合は貸出できないようにするといったロジックは、アプリケーションとしての動作とは無関係なドメインロジック (ビジネスロジック) です。

[ Infrastructure レイヤ ]
データアクセスや周辺機器の制御などに関するレイヤです。


DDD において、業務エラーを表すのは Application レイヤだと僕は考えます。なぜなら、業務エラーは「アプリケーションとしての動作」の一種だからです。
Application レイヤが色々な手段で「業務エラーにすべき状況の検出」をし、「業務エラーとしてユーザーに通知」するわけです。

業務エラーにすべき状況を検出する手段には、例えば次のような手段があります。(*2)
  • UI への入力値の妥当性を検証しその結果を見て検出
  • ドメインモデルの検証用メソッドを実行した結果を見て検出
  • ドメインモデルの状態プロパティを見て検出
  • ドメインモデルのメソッドを実行した結果例外が発生するかを見て検出
  • データアクセス時に例外が発生するかを見て検出

繰り返しますが、これらはあくまでも検出するための手段 (*3) であり、業務エラーそのものではありません。
業務エラーの検出後は、User Interface レイヤに対して、業務エラーをユーザーに通知するように指示を出します。この指示はメソッド呼び出し等 (*4) で行います。


ということで、最初に書きました通り、例外も戻り値も業務エラーを表すために使うものではないと主張します。特に、「本来例外を投げるべきところ」で「業務エラーは戻り値で表さなきゃいけないから戻り値にする」という考えには賛同できません。

今回、例として DDD を選びましたが、必ず DDD を適用しろとか、必ず Application レイヤで業務エラーを表せと言っているわけではありませんので注意してください。
また、ここで僕が述べたことは、他の設計思想においても当てはまるかもしれませんし、当てはまらないかもしれません。



*1:これは場合によっては Domain レイヤで行った方が適切だったりするかもですが、良い例が浮かばなかったので…
*2:これは決してボトムアップ設計を示唆しているわけではありません。
*3:"検出" を "選定" と置き換えた方が伝わりやすいかもしれませんね。
*4:まだ触ったことないですが ASP.NET MVC なら ViewData プロパティを使用するんですかね。
スポンサーサイト



タグ: .NET C# 例外処理
2007/11/14 01:38

PowerShell の例外処理
PowerShell で try - catch - finally を実現

以前、この 2 つの記事で PowerShell の例外処理に関することを書きました。
で、一つ重大なことを見落としていました。Write-Error コマンドレットの存在です。
実は、Write-Error コマンドレットで出力した例外は trap ブロックで捕捉することができないようです。だから、try - catch -finally を実現する関数も、Write-Error コマンドレットの前では無力です。

ErrorActionPreference 変数 ( あるいは -errorAction 共通パラメータ ) の設定は反映されます。というより、trap ブロックが無力であるため完全にこれに従って動作します。
ErrorActionPreference 変数に "Stop" が設定されている場合、 Write-Error コマンドレットによって発生した例外を trap ステートメントにて捕捉でき ( るように見え ) ます。でもこれは、厳密には Write-Error コマンドレットによって発生した例外ではありません。ActionPreferenceStopException という別の例外です。残念ながら、この例外から本来発生した例外を取得することはできないようです。



Write-Error コマンドレットのヘルプを見ると、 「オブジェクトをエラー パイプラインに書き込みます。」 と書いてあります。
私なりに調べてみたところ、この記述は半分正しく半分間違っています。
Write-Error コマンドレットに限らず、 例外に ErrorActionPreference 変数の "Continue" が適用されると、最終的には例外が通知されるもののそれ以降の処理が続行されます。これを実現するための仕組みがエラーパイプラインです。
先ほど書いたように、Write-Error コマンドレットは ErrorActionPreference 変数の設定に完全に従います。Write-Error コマンドレットによって発生した例外がエラーパイプライン渡されるのは、あくまでも ErrorActionPreference 変数が "Continue" に設定されている場合のみです。
( なお、コマンドレット内で発生した例外の一部も、Write-Error と同様の動作をします。具体的には、エラー通知の実装が、例外のスローではなく Cmdlet クラスの WriteError メソッドを使用している場合です。 )



エラーパイプラインは、エラー情報を蓄積しながら処理を続行させるための仕組みです。ここからは、エラーパイプラインについて見て行きましょう。

エラーパイプラインは通常のパイプラインとは別モノです。例えば以下のコードを実行すると、"Hoge" だけがパイプラインで渡されていることが確認できます。 ( $ErrorActionPreference 変数は "Continue" ( 既定 ) で試してください。 )

&{ Write-Error "Error1"; "Hoge1"; } | %{ "$_ がパイプラインで渡されました。"; };


出力は以下の通りです。

Write-Error "Error1"; "Hoge1";  : Error1
発生場所 行:1 文字:2
+ &{ <<<<  Write-Error "Error1"; "Hoge1"; } | %{ "$_ がパイプラインで渡されました。"; };
Hoge1 がパイプラインで渡されました。

Error1 の情報が出力された後、 "Hoge1 がパイプラインで渡されました。" と出力されていることから、エラーパイプラインと通常のパイプラインが別モノだということがわかります。

では、以下のコードを実行するとどうなるでしょうか。

&{ Write-Error "Error1"; "Hoge1"; Write-Error "Error2"; "Hoge2"; } | %{ "$_ がパイプラインで渡されました。"; };


このコードを実行すると、以下のような出力が得られます。

Write-Error "Error1"; "Hoge1"; Write-Error "Error2"; "Hoge2";  : Error1
発生場所 行:1 文字:2
+ &{ <<<<  Write-Error "Error1"; "Hoge1"; Write-Error "Error2"; "Hoge2"; } | %{ "$_ がパイプラインで渡されました。"; };
Hoge1 がパイプラインで渡されました。
Write-Error "Error1"; "Hoge1"; Write-Error "Error2"; "Hoge2";  : Error2
発生場所 行:1 文字:2
+ &{ <<<<  Write-Error "Error1"; "Hoge1"; Write-Error "Error2"; "Hoge2"; } | %{ "$_ がパイプラインで渡されました。"; };
Hoge2 がパイプラインで渡されました。

どうも、エラーパイプラインと通常のパイプラインは別モノではあるものの、一連の順序は保持されているようです。
もしかすると、エラーパイプラインを渡っている例外もパイプラインを渡っているオブジェクトも、どこかで識別用のマークが付けられているだけで、一つのパイプラインを渡っているのかもしれません。まぁ、実際どうなっているのかまでは私にはわかりません。



エラーパイプラインにある例外を通常のパイプラインに移動させることもできます。それには、2>&1 という演算子を使います。
以下のコードを実行すると、エラーパイプラインにある例外が通常のパイプラインに移動することが確認できます。

(&{ Write-Error "Error1"; "Hoge1"; Write-Error "Error2"; "Hoge2"; } | %{ "$_ ok1"; }) 2>&1 | %{ "$_ ok2";};


出力は以下のようになります。

Error1 ok2
Hoge1 ok1 ok2
Error2 ok2
Hoge2 ok1 ok2

Error1 と Error2 には "ok2" だけが付加されています。つまり、2>&1 演算子によって通常のパイプラインに移動されたわけです。
ここでは結果をわかりやすくするために例外を文字列に変換しましたので、例外情報が赤文字で書き出されてはいません。通常のパイプラインに移動されても、文字列に変換したりせず例外がそのまま出力された場合は、赤文字で出力されます。
( 余談ですが、PS オブジェクトに対して、Add-Member コマンドレットを使って "writeErrorStream" というノートプロパティを追加し値を true に設定すると、そのオブジェクトの出力時に出力が赤文字になります。例外が赤文字で表示されるのはこの為です。 )



エラーパイプラインはこんなところでしょうか。ちなみに通常のパイプラインからエラーパイプラインに移動させる 1>&2 演算子というものもあるようですが、使おうとすると「まだサポートされていない」と言われます。次期バージョンである PowerShell 2.0 ではサポートされるのかもしれません。
2007/11/07 23:52
以前 PowerShell の例外処理 という記事で書いた try - catch - finally もどきを進化させてみました。
今回は、以下のように C# の try - catch - finally に近い書き方ができます。

try {
    # 処理
} catch ([例外の型]) {
    param($ex)
    # 例外処理
} finally {
    # 後処理
}



各ブロックの開始の "{" の前と各ブロックの終了の "}" の後ろは、上記のように、改行せずに記述する必要があります。また例外の型は、上記のように、必ず "()" で囲む必要があります。

catch または finally は、省略可能です。例外の型も省略可能です。
# 追記 ( 2007/11/12 )
catch ブロック内では、break ステートメントを使用して例外を再スローすることができます。当然、任意の例外を throw ステートメントでスローすることもできます。
# 追記ここまで


try {
    # 処理
} catch {
    # 例外処理
}




で、これらを実現するための関数がこちらです。

function global:try
{
    $currentArgIndex = 0;
    $tryBlock = $args[$currentArgIndex];
    $currentArgIndex++;
    if ($tryBlock -isnot [System.Management.Automation.ScriptBlock])
    {
        throw New-Object "ArgumentException" @("try ブロックの指定が不正です。");
    }
    if ("catch" -eq $args[$currentArgIndex])
    {
        $currentArgIndex++;
        if ($args[$currentArgIndex] -is [Type])
        {
            $targetExceptionType = $args[$currentArgIndex];
            $currentArgIndex++;
        }
        $catchBlock = $args[$currentArgIndex];
        $currentArgIndex++;
        if ($catchBlock -isnot [System.Management.Automation.ScriptBlock])
        {
            throw New-Object "ArgumentException" @("catch ブロックの指定が不正です。");
        }
    }
    if ("finally" -eq $args[$currentArgIndex])
    {
        $currentArgIndex++;
        $finallyBlock = $args[$currentArgIndex];
        $currentArgIndex++;
        if ($finallyBlock -isnot [System.Management.Automation.ScriptBlock])
        {
            throw New-Object "ArgumentException" @("finally ブロックの指定が不正です。");;
        }
    }
    if (($() -eq $catchBlock) -and ($() -eq $finallyBlock))
    {
        throw New-Object "ArgumentException" @("catch ブロックまたは finally ブロックを指定してください。");
    }
   
   
    &{
        $requireFinally = ($() -ne $finallyBlock);
        &{
            &$tryBlock;
            trap
            {
                if ($() -eq $catchBlock)
                {
                    break;
                }
                $ex = $_.Exception;
                if (($() -ne $targetExceptionType) -and (!$targetExceptionType.IsAssignableFrom($ex.GetType())))
                {
                    break;
                }
                &$catchBlock $ex;
                continue;
            }
        };
        if ($requireFinally)
        {
            $requireFinally = $False;
            &$finallyBlock;
        }
        trap
        {
            if ($requireFinally)
            {
                $requireFinally = $False;
                &$finallyBlock;
            }
            break;
        }
    };
}




以下のスクリプトを実行すると、動作が確認できます。

try {
    "try ブロック実行";
    throw New-Object "ArgumentException";
    "この文は出力されない";
} catch ([ArgumentException]) {
    param ($ex)
    "{0} がスローされたから catch ブロック実行" -f $ex.GetType().Name;
} finally {
    "finally ブロック実行";
}



出力は以下のようになります。

try ブロック実行
ArgumentException がスローされたから catch ブロック実行
finally ブロック実行



【 ダウンロード 】
自作の PowerShell 関数は、以下の記事からまとめてダウンロードできます。

YokoKen.PowerShell.Scripts

2007/08/13 02:45
前回の記事で defaultRedirect によってエラーページに遷移させるとセッションがクリアされると書きましたが、defaultRedirect によってエラーページに遷移させた場合でも、セッションがクリアされる時とクリアされない時があることが判明しました。また、この動作はセッションの格納方法によっても異なります。

セッションの格納方法別に挙動をまとめてみました。 ( SQL Server モードは未検証ですが。。。 )


[ In Process モード ]
未処理例外発生時の HTTP リクエスト処理より以前にセッションデータが格納されていた場合、セッションはクリアされません。
未処理例外発生時の HTTP リクエスト処理より以前にセッションデータが格納されていなかった場合、未処理例外発生時の HTTP リクエスト処理中に格納されたセッションは、未処理例外発生時の HTTP リクエスト処理が終了した時点でクリアされます。


[ State Server モード ]
未処理例外発生時の HTTP リクエスト処理中に格納されたセッションデータは、未処理例外発生時の HTTP リクエスト処理が終了すると無効化され、未処理例外発生時の HTTP リクエスト処理以前の状態が復元されます。 ( 新しくキーを生成した場合は削除され、既存のキーに対してなんらかの処理を行った場合は、その処理が全て取り消される。 )


[  SQL Server モード ]
検証してません。 ( State Server モードと同じ動作な予感。 )



どのみち、Global.asax の Error イベントハンドラでリダイレクトすればセッションは保持されます。Global.asax で ClearError メソッドを呼び出すことで例外が「処理」されるため、上記動作が作動しないということですね。


# この検証をしている時に ClearError メソッドを呼び忘れて検証していたため、「Global.asax でリダイレクトさせても同様の現象が発生する!」って内容の記事を30分程公開していたのは内緒 ^^; もしその時に記事を読んでしまった方がいらっしゃいましたら、本当にすみませんでした m( _ _;)m
2007/08/10 19:30
<customErrors mode="On" defaultRedirect="Error.aspx" />


Web.config で上記のようにして defaultRedirect を設定した場合、未処理の例外が発生したら自作エラーページである Error.aspx にリダイレクトされます。
僕の場合は、今まで Global.asax の Error イベントハンドラ ( Application_Error メソッド ) で自作エラーページへのリダイレクト処理を行っていました。なので気づかなかったんですが、Web.config に設定した defaultRedirect によってエラーページに遷移した場合、セッションがクリアされます。

Global.asax の Error イベントハンドラでリダイレクトさせるより defaultRedirect でリダイレクトさせた方がいいんじゃないか?と疑問に思って ( ※ ) 検証していたら、これに気づきました。いや、気づくまでしばらくハマってました。

今後も Global.asax の Error イベントハンドラでリダイレクトしていこうと心に決めましたね。


※ defaultRedirect でリダイレクトさせる場合、mode 属性に RemoteOnly 指定するだけで簡単に開発者向けのエラーページを表示するように切り替えられるからです。


[ 関連記事 ]
未処理例外発生時のセッションの動作
タグ: .NET C# ASP.NET 例外処理
2007/07/28 20:25
PowerShell の例外処理は VB6 のような非構造化例外処理を採用している。
PowerShell では trap ブロックによって例外を捕捉する。
この例外処理の動作がけっこうややこしい ( 特に $ErrorActionPreference が関わってくる条件 ) ので、検証してまとめてみた。 ( 掲載したサンプルコードはそのまま実行可能。 )


[ throw ステートメント ]
PowerShell では、 throw ステートメントを使用して例外を明示的にスローすることができる。スローできるのは Exception 派生クラスのインスタンス、ErrorRecord クラス (System.Management.Automation) のインスタンス、文字列である。これ以外のオブジェクトをスローしようとした場合、ToString メソッドによって文字列に変換されてスローされる。
なお、文字列がスローされた場合は、その文字列を元に RuntimeException クラス (System.Management.Automation) のインスタンスが生成され、この RuntimeException がスローされる。


[ trap ブロック ]
trap ステートメントの直後に例外の型を指定しておくと、その型の例外のみハンドルされる。(型指定は省略可。)
trap ブロックは一つのスコープに対して複数用意することができるが、実際に有効になるのはその中の一つだけであり、先に定義された trap ブロックが使用される。trap ブロックはスコープ内のどこに配置してもよく、同一スコープ内の全ての処理に作用する。
例えば、以下のコードでは、どこで例外が発生しても、"aaa" と出力される。

function Sample1
{
  param ([Int32] $arg0)
 
  if ($arg0 -eq 1) { throw "いち"; }
  trap { "aaa"; }
  if ($arg0 -eq 2) { throw "に"; }
  trap { "bbb"; }
  if ($arg0 -eq 3) { throw "さん"; }
  trap { "ccc"; }
}

Sample1 1;
Sample1 2;
Sample1 3;



[ 捕捉した例外を取得 ]
trap ブロック内では &_ 変数にて、捕捉した例外にアクセスできる。ただし、$_ 変数に格納されているのは ErrorRecord オブジェクトであり、実際に発生した例外は ErrorRecord の Exception プロパティから取得する。つまり、 $_.Exception にて実際に発生した例外を取得できる。
例外を捕捉して新しく別の例外としてスローする例を以下に示す。

function Sample2
{
  throw (New-Object "System.ArgumentException");
  trap [System.ArgumentException]
  {
    $ex = $_.Exception;
    throw (New-Object "System.IO.FileNotFoundException" @("ファイルが見つからないよ", $ex));
  }
}

Sample2;



[ 例外の処理方法 ]
例外が発生すると、その例外を捕捉できる trap ブロックが同一スコープ内に存在するかどうか探される。存在した場合は、その trap ブロックに制御が移り、存在しない場合は上位スコープに例外がスローされる。
例外が上位スコープにスローされると、上位スコープで再び trap ブロックが存在するかどうか探される。これを繰り返し、最上位のスコープまで trap ブロックが存在しなかった場合、処理は中断され、例外がユーザーに通知される。


[ tarp ブロック用のステートメント ]
trap ブロック内では、以下の2つのステートメントを使用できる。

break ステートメント
スコープを抜ける。例外は上位に再スローする。

continue ステートメント
例外が発生したコードの次のコードに制御を移す。例外は握りつぶされる。


[ continue ステートメントについて ]
continue ステートメントが実行されると例外が発生したコードの次のコードに制御が移動すると書いたが、厳密には、「同一スコープ内での次のコードに制御を移動」となる。
以下のようなコードを例に挙げてみる。

function Sample3
{
  &{
    "ほげ";
    throw "throw1";
    "Hoge";
  };
  &{
    "ふが";
    throw "throw2";
    "Fuga";
  };
  trap
  {
    continue;
  }
}

Sample3


この Sample3 関数内には、trap ブロックを除いて2つのブロックが存在し、この2つのブロックの中には trap ブロックが含まれていない。
このコードを実行すると、まず1つ目のブロックで "ほげ" と出力した後例外が発生する。一つ目のブロック内には trap ブロックが存在しないので、例外は上位にスローされる。その結果、一つ上位のスコープにある trap ブロックによって例外がハンドルされる。trap ブロック内には continue ステートメントが記述されているので、処理を続行することになる。遷移先は「同一スコープ内での次のコード」なので、1つ目のブロック内の処理は中断され、2つ目のブロックに制御が移る。2つ目のブロックでも同様の動作が繰り返される。
結果として、以下の出力が得られる。

ほげ
ふが


[ $ErrorActionPreference 変数 ]
trap ブロック内で break ステートメントも continue ステートメントも実行されなかった場合、$ErrorActionPreference 変数の設定に従った動作が行われる。
ただし、trap ブロック内で新たに例外が発生した場合は除く。 ( 例えば捕捉した例外を元に新しく例外を生成してスローした場合等。 )
また、捕捉できる trap ブロックがどこにもない例外が発生した場合 ( 2007/11/13 追記:throw ステートメントでスローした例外は除く ) も、$ErrorActionPreference 変数の設定に従った動作が行われるが、一度でも trap ブロックに捕捉された例外に対しては動作しない。

$ErrorActionPreference 変数には ActionPreference 列挙体 ( System.Management.Automation ) の列挙値を設定できる。ActionPreference 列挙体には以下の4つの列挙値が用意されている。

SilentlyContinue ( サイレント続行 )
例外が発生したコードの次のコードに制御を移す。例外は握りつぶされる。
trap ブロック内で continue ステートメントが実行された場合と同様の挙動。

Stop ( 中断 )
スコープを抜ける。例外は上位に再スローする。
trap ブロック内で break ステートメントが実行された場合と同様の挙動。

Continue ( 続行 )
例外が発生したコードの次のコードに制御を移す。例外はユーザーに通知される。( 再スローされるわけではない。 )
この値が既定の設定。

Inquire ( 問い合わせ )
例外のメッセージを表示し、動作をユーザーに問い合わせる。選択できる動作は、続行, サイレント続行, 中断, 中断 ( 例外握りつぶし ) の4つ。


・trap ブロック内で break ステートメントも continue ステートメントも実行されなかった場合の例
trap ブロック内で break ステートメントも continue ステートメントも実行されなかった場合の例を以下に示す。

function Sample4
{
  &{
    throw (New-Object "System.InvalidOperationException" @());
    "ほげ";
    trap
    {
      "trap1";
    }
  };
  "ふが";
  trap
  {
    "trap2";
    break;
  }
}

$ErrorActionPreference = "SilentlyContinue";
Sample4;


このコードを実行すると、以下の出力が得られる。

trap1
ほげ
ふが

Sample4 関数内のスクリプトブロックは一行目で例外をスローしている。この例外を捕捉する trap ブロックが同一スコープ内にあるので、この trap ブロックに制御が移動する。しかし、break ステートメントも continue ステートメントも記述されていないので、trap ブロックを抜けた後は $ErrorActionPreference 変数に従った動作が行われる。
$ErrorActionPreference 変数には "SilentlyContinue" を設定しているので、例外は握りつぶされ、次の行の "ほげ" という文字列が出力され、続いて "ふが" という文字列が出力される。

"trap2" は出力されないことに注意する。trap ブロック内で break 及び continue が実行されなかった場合、その上位に trap ブロックがあろうがなかろうが、$ErrorActionPreference 変数に従った動作が行われる。その結果例外が握りつぶされたため、2つ目の trap ブロックは実行されていない。


・捕捉できる trap ブロックがどこにもない例外が発生した場合の例
捕捉できる trap ブロックがどこにもない例外が発生した場合の例を以下に示す。

function Sample5
{
  &{
    throw (New-Object "System.InvalidOperationException" @());
    "ほげ";
  };
  "ふが";
}

$ErrorActionPreference = "SilentlyContinue";
Sample5;


このコードを実行すると、以下の出力が得られる。

ほげ
ふが

Sample5 関数内のスクリプトブロックは一行目で例外をスローしている。この例外を捕捉する trap ブロックが、同一スコープ内にも上位スコープ内にも存在しないため、$ErrorActionPreference 変数に従った動作が行われる。
$ErrorActionPreference 変数には "SilentlyContinue" を設定しているので、例外は握りつぶされ、次の行の "ほげ" という文字列が出力され、続いて "ふが" という文字列が出力される。


[ テクニック ]
適当に考えた、テクニックというほどでもないテクニックを3つほど。
「もっとエレガントに書けるだろ!」って方は是非コメントでお教えください m( _ _ )m

try もどき
trap ブロックは配置されているスコープ内の全てに対して有効である。C# の try ブロック のように範囲を限定する特別な機能は備わっていない。
C# の try ブロック のように範囲を限定したい場合は以下のようなテクニックが使える。

function Technique1
{
  param ([int] $arg0)
  &{
    if ($arg0 -eq 1)
    {
      throw (New-Object "System.ArgumentException" @());
    }
    trap [System.ArgumentException]
    {
      "1つ目のブロックで発生した例外を捕捉";
      break;
    }
  };

  &{
    if ($arg0 -eq 2)
    {
      throw (New-Object "System.ArgumentException" @());
    }
    trap [System.ArgumentException]
    {
      "2つ目のブロックで発生した例外を捕捉";
      break;
    }
  };
}

Technique1 1;
Technique1 2;


つまり、trap ブロックが有効となる範囲を狭めるために、スクリプトブロックを作成し、& を使ってその場で実行させるということ。


catch ( 例外握りつぶし ) もどき
PowerShell では trap ブロックが C# の catch ブロックに相当するが、catch ブロックで例外を握りつぶすような機能が用意されていない。 ( continue ステートメントを使えば例外を握りつぶしせるが、そのまま次のコードに復帰してしまう。 )
C# の catch ブロックで例外を握りつぶすような感じの処理を行いたい場合は以下のようなテクニックが使える。

function Technique2
{
  &{
    &{
      "この次に例外が発生";
      throw "hoge";
      "例外は握りつぶされるが、ここは出力されない";
    };
    trap
    {
      continue;
    }
  };
  "例外が握りつぶされ、このコードが実行される";
}

Technique2


つまり、trap ブロックが置かれているスコープ内に、スクリプトブロックを一つだけ配置し、continue ステートメントによって例外を握りつぶす。こうすることによって、trap ブロックが一つ上位に配置されているため、スクリプトブロックの中の処理は中断される。


finally もどき
PowerShell には C# の finally ブロックのように確実な後処理を行うための特別な機能が用意されていない。
C# の finally ブロック のように範囲を限定したい場合は以下のようなテクニックが使える。

function Technique3
{
  param ([Boolean] $arg0)
 
  $finallyBlock =
    {
      "確実な後処理";
    };

  if ($arg0)
  {
    throw "すろー";
  }
  &$finallyBlock;

  trap
  {
    &$finallyBlock;
    break;
  }
}

Technique3 $False;
Technique3 $True;


つまり、確実な後処理をスクリプトブロックとして変数に格納しておき、正常終了時も、例外捕捉時もそれを呼び出すということ。

try - catch - finally もどき
これらを状況に応じてうまく組み合わせれば try - catch -finally が完全に再現できる。 ( でも可読性はあまりよくない。。。 )
まぁ、「catch ( 例外握りつぶし ) もどき」はあまり使うことないかな。


# 追記 ( 2007/11/07 )

C# の try -catch - finally に近い形式で記述する方法を記事にしました。

# 追記ここまで


[ 最後に ]
PowerShell の例外処理はこんなところかな。
ん、$Error 変数?なんのことやら。


# 追記 ( 2007/11/14 )

例外処理には、まだ他にも書くべきことがありました。 → エラーパイプライン

# 追記ここまで
2007/04/23 23:08
ASP.NET では、Global.asax で例外を一元的にハンドリングするためのエラー ハンドラを用意することができました。しかし、WCF では残念ながら Global.asax がサポートされていません。 ( ASP.NET 互換モードでも非サポート。 )

WCF では、例外を一元的にハンドリングするための機構が別の方法で提供されています。
Global.asax に比べて、手順がやや面倒となりますが、クライアントにフォールトメッセージを通知することを考慮した仕組みにはなっています。


【 IErrorHandler インターフェイス 】
例外を一元ハンドリングするのは、 IErrorHandler インターフェイス (System.ServiceModel.Dispatcher) を実装した独自のクラスとなります。
このインターフェイスが提供するメソッドは以下の2つです。

[ ProvideFault メソッド ]
ProvideFault メソッド は、サービス内部からスローされた例外をハンドリングします。このメソッド内でクライアントに渡すフォールトメッセージを生成することができます。
ロギングなどの処理はこのメソッドではなく、後述の HandleError メソッドで行います。

このメソッドのパラメータは以下の3つです。

error
型は Exception クラス (System) です。
このパラメータにより、サービス内部からスローされた例外を取得できます。

version
型は MessageVersion クラス (System.ServiceModel.Channels) です。
このパラメータにより、現在の SOAP 通信 における SOAP のバージョンを取得できます。

fault
型は Message クラス (System.ServiceModel.Channels) です。また、このパラメータは参照渡しとなっています。
サービス内部からスローされた例外が、 FaultException クラス (System.ServiceModel) であった場合、それを表わすフォールト メッセージがこのパラメータにより取得できます。サービス内部からスローされた例外が、 FaultException クラスでない場合、このパラメータは null となります。
この ProvideFault メソッド内で Message クラスを生成して、このパラメータに設定することができます。これにより、クライアントに渡すフォールトメッセージを指定することができるわけです。

・ProvideFault メソッドの実装例
FaultException 以外の、ハンドルされなかった例外が発生した場合に、予期せぬエラーが発生したという通知をクライアントに対して行うための実装例を以下に示します。

public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
    if (fault != null)
    {
        return;
    }

    FaultReasonText[] reasonTexts =
        new FaultReasonText[]
        {
            new FaultReasonText("予期せぬエラーが発生しました。", "ja-JP"),
            new FaultReasonText("An unexpected error occurred.", "en-US")
        };
    FaultReason reason = new FaultReason(reasonTexts);
    FaultCode code = FaultCode.CreateReceiverFaultCode("unexpected", "http://schemas.yokoken.net/Hoge");
    FaultException faultException = new FaultException(reason, code);
    MessageFault messageFault = faultException.CreateMessageFault();
    string action = OperationContext.Current.IncomingMessageHeaders.Action;
    fault = Message.CreateMessage(version, messageFault, action);
}


[ HandleError メソッド ]
HandleError メソッド は、サービス内部からスローされた例外と、通信時に発生した例外をハンドリングします。
サービス内部からスローされた例外は、ProvideFault メソッドが先にハンドリングします。その後、このメソッドがハンドリングします。ProvideFault メソッドでフォールト メッセージを生成した場合でも、HandleError メソッドでハンドリングされる例外は変わりません。つまり、サービス内部でスローされた例外がハンドリングされます。
ロギングや、MSMQ の有害メッセージ ( Poison Message ) の処理などはこのメソッドで行います。

このメソッドのパラメータは以下の1つです。

error
型は Exception クラス (System) です。
このパラメータにより、サービス内部からスローされた例外や、通信時に発生した例外を取得できます。

・戻り値について
このメソッドの戻り値は bool 型の値です。true を返した場合、例外はハンドルされたものとして扱われ、false を返した場合、例外はハンドルされなかったものとして扱われます。
MSMQ で有害メッセージを処理した等の場合は、ここで true を返します。何も処理しなかったり、ロギングのみを行う場合は false を返します。
( ただ、正直な話、これらの具体的な違いは不明です。色々試してみましたが、 true を返しても false を返しても、挙動の違いは見受けられませんでした。Reflector でコードを覗いてみると、一応特定の条件下において false が返された場合に、チャネルの中止処理を行うようになっているようです。 )

・HandleError メソッドの実装例
FaultException 以外の、ハンドルされなかった例外 ( 通信時の例外含む ) が発生した場合に、例外の情報をコンソールへ出力するための実装例を以下に示します。

public bool HandleError(Exception error)
{
    if (error is FaultException)
    {
        return false;
    }
    string errorType = error.GetType().FullName;
    string errorMessage = error.Message;
    string errorStackTrace = error.StackTrace;
    Console.WriteLine("[ Error ]\r\nType: {0}\r\nMessage: {1}\r\nStack Trace: {2}", errorType, errorMessage, errorStackTrace);
    return false;
}


【 エラー ハンドラの登録 】

[ ChannelDispatcher へ登録 ]
IErrorHandler 実装クラスを定義しただけでは、エラー ハンドラとして動作してくれません。IErrorHandler 実装クラスを登録する処理を実装する必要があります。
登録は、ChannelDispatcher クラス (System.ServiceModel.Dispatcher) のインスタンスの、ErrorHandlers プロパティ に対して行います。このプロパティに格納されているコレクションオブジェクトに対してIErrorHandler 実装クラスのインスタンスを追加してやります。

ServiceHostBase クラス (System.ServiceModel)ChannelDispatchers プロパティ には、ChannelDispatcherCollection クラス (System.ServiceModel.Dispatcher) のインスタンスが格納されています。このコレクションから、ChannelDispatcher オブジェクトを取得することができます。
このコレクション自体は、ChannelDispatcherBase クラス (System.ServiceModel.Channels) のコレクションとして定義されています。ChannelDispatcherBase クラスは、ChannelDispacher クラスの基本クラスですが、ErrorHandlers プロパティは定義されていません。つまり、ChannelDispacher クラスへのキャストが行えるか検証し、キャストを行う必要があります。 ( ただ、ChannelDispatcherBase クラスは抽象クラスであり、このクラスの派生クラスは ChannelDispacher クラスしかありません。つまり、事実上ChannelDispatcherCollection クラスは ChannelDispacher のコレクションです。しかし、それでもキャストができるかどうかの検証はしっかり行うことを推奨します。 )

[ 登録はサービス ビヘイビアで行う ]
登録処理は独自のビヘイビアで行います。ビヘイビアからではなく、サービス ホストを直接操作して登録処理を行っても、エラー ハンドラは動作してくれません。
ビヘイビアはサービス ビヘイビアまたはエンドポイント ビヘイビアとなります。
独自のビヘイビアを定義する方法については以下の記事を参照してください。

独自のビヘイビアを定義する
独自のビヘイビアを定義する 2


【 実装例 】
エラー ハンドラの実装例と、それを適用するためのサービス ビヘイビアの実装例を以下に示します。

エラー ハンドラ
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;

internal sealed class ErrorHandler : IErrorHandler
{
    public bool HandleError(Exception error)
    {
        if (error is FaultException)
        {
            return false;
        }
        string errorType = error.GetType().FullName;
        string errorMessage = error.Message;
        string errorStackTrace = error.StackTrace;
        Console.WriteLine("[ Error ]\r\nType: {0}\r\nMessage: {1}\r\nStack Trace: {2}", errorType, errorMessage, errorStackTrace);
        return false;
    }

    public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
    {
        if (fault != null)
        {
            return;
        }

        FaultReasonText[] reasonTexts =
            new FaultReasonText[]
            {
                new FaultReasonText("予期せぬエラーが発生しました。", "ja-JP"),
                new FaultReasonText("An unexpected error occurred.", "en-US")
            };
        FaultReason reason = new FaultReason(reasonTexts);
        FaultCode code = FaultCode.CreateReceiverFaultCode("unexpected", "http://schemas.yokoken.net/Hoge");
        FaultException faultException = new FaultException(reason, code);
        MessageFault messageFault = faultException.CreateMessageFault();
        string action = OperationContext.Current.IncomingMessageHeaders.Action;
        fault = Message.CreateMessage(version, messageFault, action);
    }
}


サービス ビヘイビア
using System;
using System.Collections.ObjectModel;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Description;

[AttributeUsage(AttributeTargets.Class)]
public class ErrorHandlerAttribute : Attribute, IServiceBehavior
{
    private IErrorHandler errorHandler;

    public ErrorHandlerAttribute()
    {
        this.errorHandler = new ErrorHandler();
    }

    public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
    {
    }

    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {
        foreach (ChannelDispatcherBase dispatcherBase in serviceHostBase.ChannelDispatchers)
        {
            ChannelDispatcher dispatcher = dispatcherBase as ChannelDispatcher;
            if (dispatcher == null)
            {
                continue;
            }
            dispatcher.ErrorHandlers.Add(this.errorHandler);
        }
    }

    public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {
    }
}


これで、サービス クラスに ErrorHandlerAttribute クラスを属性として付加すれば、サービスにエラー ハンドラが適用されます。
タグ: .NET C# WCF 例外処理
2007/02/25 04:29
まず最初に謝ります。ごめんなさい、以前 業務エラーの表現方法の考察 で書いたことを取り消させてください。

僕はやはり、

「業務エラーは例外で表現するべき」

と考えます。
では、その理由について語っていきます。


【 目次 】
1. 例外の利点
1-1. 例外時の処理コードを隔離できる
1-2. バグを検出しやすい
1-3. 詳細な情報を保持している

2. 業務エラーとしての例外
2-1. 一つのメソッドは一つの機能を表す
2-2. コスト

3. 注意点
3-1. 適切な実装の上で必要となるのは判別情報のみ
3-2. 業務エラーはExceptionクラスから派生

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はこのようなルールを推奨しようとしていました。

しかし、.NET Framework クラスライブラリ ( 以降、FCL ) 自身がこのルールを守れなかったのです。実は、FCLに用意されている例外の中には、ApplicationExceptionから派生しているクラスや、Exceptionクラスから直接派生しているクラスが紛れ込んでいます。FCLがルールを守れていない以上、このルールはもう台無しです。ApplicationExceptionから派生させる意味はなくなります。


【 4. まとめ 】
  • 業務エラーは例外としてスローし、上位で適切な処置を行う必要があります。
  • 業務エラーを例外で表すことにより、可読性・保守性の向上に繋がります。
  • 業務エラーを例外で表しても、適切な設計を行えばパフォーマンスに問題はありません。
  • 業務エラーを表す例外に対して適切な処理を行っている限り、型以外の例外情報は不要です。
  • 業務エラーを表す例外がハンドルされなかった場合、例外情報がとても重宝します。
  • 業務エラーを表す例外はException クラスから派生させます。


【 5. 参考文献 】

プログラミング Microsoft .NET Framework 第2版
この記事を書くに当たって、この書籍の例外に関するページが大変参考になりました。業務エラー・システムエラーという観点では特に書かれていませんが、是非一読してみることをお勧めします。


タグ: .NET C# 例外処理
2007/02/04 22:41

[ 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 属性の値に応じて次のような処理が行われます。

・falseの場合
ハンドルされなかった例外情報は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にします。
タグ: .NET C# WCF 例外処理
2006/12/06 15:53

2007/02/25 追記

この記事に書いたことは、現在の私の意見とは異なります。

現在は、業務エラーは例外で表すべきと私は考えています。
詳細はこちらの記事を参照してください。


以前書いた業務エラーの表現方法ついての考察。
経験不足が否めない。。。
つっこみしてくれる方大募集!w

【 業務エラーの定義 】
まず、Application Architecture for .NET第三章 ( 以下、AAfN ) と、書籍 「 Microsoft Visual Studio 2005 による Webアプリケーション構築技法 」 ( 以下、赤間本 ) での、業務エラーの定義に差異はないか?

[ AAfN ]
AAfNでは、例外を以下のように分類している。

技術上の例外
データベース接続の失敗など。

業務上の例外
外部キー制約の違反など。

[ 赤間本 ]
赤間本では、例外を以下のように分類している。

アプリケーション/システムエラー
( 例として、 ) データベース障害やネットワーク障害により、データベースの読み書きが行えなかった場合。

業務エラー
( 例として、 ) 希望された顧客IDがすでに他の顧客により利用されていたといった理由により、顧客情報が登録できなかった場合。

また、赤間本では、業務エラーとアプリケーション/システムエラーの切り分け方として、「業務設計の中で想定されていなければならないケースが業務エラー」というようなことも書かれている。

[ 比較 ]
AAfNは外部キー制約の違反、赤間本は一意キー制約の違反を例に挙げている。これらは、「他のユーザーが既に登録/編集済み」などが原因で発生する。
どちらもあまり詳細には書かれていないが、定義の差異はなさそうだ。
個人的には、「エンドユーザーに許容されている範囲内での、エンドユーザーの操作 ( 入力 ) 次第で発生しうる、正常系でないフローが業務エラー」で、「システム異常など、まさに例外的な状況がアプリケーション/システムエラー」だと解釈している。


【 戻り値と例外 】
業務エラー云々を抜きにして、戻り値と例外について分析してみる。

[ 戻り値 ]

与えられた入力 ( 引数 ) に操作を適用した結果生じる出力。
引数と違い、返せるオブジェクトは一つだけ。
固定の型。
戻り値は呼び出し元でしか受け取れない。

[ 例外 ]
操作の途中で、例外的な状況に陥った場合に、通常とは異なるフローで、その例外情報を上位に伝播する。
例外メッセージ、スタックトレース、例外の元となった例外など伝播される情報が多い。
例外そのものは、例外の分類に応じた型 ( Exception派生クラス ) となる。
例外は例外ハンドラで処理する。その際、捕らえる例外を限定することができる。
スローされた例外は、対応する例外ハンドラが現れるまで上位に伝播される。
特殊な情報伝播を行うため、コストが高い。


【 業務エラーの表現方法の分析 】

[ 業務エラーの内容 ]
どのような業務エラーが発生したのかを知る必要はあるか?
これは、「ある」と思う。
DBの制約に関する業務エラーを例に挙げると、「IDが重複 ( 一意キー制約に違反 ) しているのか?」、「削除されたアイテムを参照 ( 外部キー制約に違反 ) しているのか?」、「違反している場所はどこなのか?」といった情報は、どこを修正すればいいのかが明確になり、有益である。

・業務エラーを例外で表した場合
ExceptionのMessageプロパティに、どのような業務エラーが発生したのか記述することができる。

・業務エラーを戻り値で表した場合
戻り値の型がBoolean型だと、どのような業務エラーが発生したのかは不明となる。ただし、業務エラーが一つの理由でしか発生しない場合は、Boolean型でも明確となる。また、チェック系メソッドを用意して事前にチェックを行えるようにしておけば、業務エラーの内容を知ることができる。
また、戻り値の型を、Boolean型ではなくEnum型にすれば、業務エラーに応じた戻り値を返せる。

[ スタックトレース ]
業務エラーを例外で表した場合は、スタックトレースが利用できるが、スタックトレースは必要か?
これは、「不要」である。そもそも業務エラーは「設計時に想定されている状況」なのだから、スタックトレースなんて見る必要がない。

[ 上位での処理方法 ]
先に述べたように、戻り値は呼び出し元でしか受け取れない。例外は、対応する例外ハンドラが現れるまで上位に伝播される。
最上位に例外ハンドラを用意するだけで済むので、例外の方に分があるように思える。

【 考察 】
一見すると、戻り値の方が処理がめんどうで、例外に分があるように見える。
しかし、業務エラーの内容を伝えるのにExceptionのMessageプロパティを使うのは、あまり適 切ではないように思う。GUIに表示するメッセージを下位層で決めているようなものだ。業務エラーは文字通り「業務」上のエラーなので、表示・表示内容の 責任を持つのはGUIである。しかも、文字列なので、上位層の「プログラムコード」で業務エラーの詳細を判別できない。 ( 各業務エラーごとにExceptionクラスを定義したり、独自のプロパティなんかを用意してやればなんとかなりそうだけど。 )
そもそも、業務エラーは設計時に想定されている事象なので、例外的な状況ではない。 ( という話をどこかで聞いた。 )


【 結論 】
「業務エラーは戻り値で表現」です。
タグ: .NET C# 例外処理