読者です 読者をやめる 読者になる 読者になる

miso_soup3 Blog

主に ASP.NET 関連について書いています。

ASP.NET Web API 2.1 その 2 ~Global Error Handling~

ASP.NET Web API

ASP.NET Web API 2.1 の目玉、Global Error Handling について調べました。

目次
  • 概要
    • 今までは不十分だった例外処理
    • ドキュメント
  • 使ってみる
    • 例外フィルター属性との関連
  • その他の情報
    • 破壊的変更は無し
    • 例外情報の取得
  • Web API における例外処理まとめ

概要

例外処理の方法が新たに用意されました。

ASP.NET Web API パイプライン内で発生した例外を全て捕捉し、ログ、レスポンスのカスタマイズを行えるようになりました。追加されたオブジェクトは2つ、Exception LoggersException Handlers になります。前者はログ、後者はレスポンスのカスタマイズ用になります。この2つのコードを記述しパイプラインに登録することで、例外処理を行います。Global Error Handling イコール Exception Loggers & Exception Handlers とみて良いと思います。

今までは不十分だった例外処理

今までは、例外処理は例外フィルター属性(Exception Filters)が用意されていました。ですがこれは、コントローラーのアクションメソッド内で発生した例外にのみ対処し、次のような場所で発生する例外には対応していません。

  • Controller のコンストラクタ
  • Message Handler
  • ルーティング
  • レスポンスのコンテンツのシリアライズ

etc...
Global Error Handling は、これらの場所で発生した例外にも対応します。例外フィルター属性との違いや棲み分けについては、後述します。

(補足:Message Handler にて 500 レベルのレスポンスをキャッチすることはできますが、発生した例外の詳細は把握できません。トレース機能は診断用であり、プロダクト環境では適していません。)

ドキュメント

使ってみる

コンストラクターで例外を発生させる ApiController を用意し、Global Error Handling を使って例外処理を行ってみます。

public class ValuesController : ApiController
{
	public ValuesController()
	{
		throw new Exception("コンストラクタで例外発生させる");
	}
		
	[Route("error/constructor")]
	public string Get()
	{
		return "hogehoge";
	}	
}

まずは、デフォルトの動作を確認。
Global Error Handling を使用しない場合、Get() メソッドを呼び出すと下のようなレスポンスが返ります。

HTTP/1.1 500 Internal Server Error
Content-Type: application/json
…

{"Message":"エラーが発生しました。",
"ExceptionMessage":"アクションメソッド内で例外発生させる",
"ExceptionType":"System.Exception","StackTrace":"   場所 WebApplication41.Controllers.ErrorController.GetHoge() 場所 c:\\ ….

この例外を補足しレスポンスをカスタマイズするには、該当部分(この場合は IHttpControllerActivator かな?)の拡張が必要になり非常~~に面倒です。

そこで、Global Error Handling の登場です。Exception Logger と Exception Handler を作成し、サービスに登録します。作成にはそれぞれ ExceptionLogger クラスと ExceptionHandler クラス(System.Web.Http.ExceptionHandling)が用意されているのでそれを継承します(IExceptionLogger, IExceptionHandler を実装しても OK)。作成場所はどこでも良いです。

MyExceptionLogger クラスを作成:

using System;
using System.Diagnostics;
using System.Web.Http.ExceptionHandling;

namespace WebApplication41
{
	public class MyExceptionLogger : ExceptionLogger
	{
		public override void Log(ExceptionLoggerContext context)
		{
			Debug.WriteLine(String.Format("★呼び出し {0} - {1}",
				System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName,
				System.Reflection.MethodBase.GetCurrentMethod().Name));

			Exception ex = context.Exception;
			// ログをとります
			Debug.WriteLine("例外発生");
			Debug.WriteLine("Method : " + context.Request.Method);
			Debug.WriteLine("Url : " + context.Request.RequestUri);
			Debug.WriteLine("Exception : " + context.Exception);
		}

		// 非同期の場合は LogAsync メソッドを override
	}
} 

MyExceptionHandler クラスを作成:

using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Web.Http.ExceptionHandling;
using System.Web.Http.Results;

namespace WebApplication41
{
	public class MyExceptionHandler : ExceptionHandler
	{
		public override void Handle(ExceptionHandlerContext context)
		{
			Debug.WriteLine(String.Format("★呼び出し {0} - {1}",
				System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName,
				System.Reflection.MethodBase.GetCurrentMethod().Name));

			HttpRequestMessage request = context.Request;
			Exception ex = context.Exception;

			//レスポンスの内容を指定
			context.Result = new ResponseMessageResult(
				request.CreateResponse(HttpStatusCode.InternalServerError, 
				"MyExceptionHandler で設定したレスポンス"));
		}

		//非同期の場合は HandleAsync メソッドを override
	}
}

Exception Handler では、引数の context の Result プロパティに返したいレスポンスをセットします。IHttpActionResult 型なので注意。

WebApiConfig.cs などで、HttpConfiguration の Services に作成したクラスを登録します。

using System.Web.Http;
using System.Web.Http.ExceptionHandling;

namespace WebApplication41
{
	public static class WebApiConfig
	{
		public static void Register(HttpConfiguration config)
		{
			// Web API configuration and services
			config.Services.Replace(typeof(IExceptionLogger), new MyExceptionLogger());
			config.Services.Replace(typeof(IExceptionHandler), new MyExceptionHandler());
…

これで準備は完了です。先ほどの Get() メソッドを呼び出すと、作成した MyExceptionLogger, MyExceptionHandler が呼び出されるようになります。
帰ってくるレスポンスは↓のようになります。

HTTP/1.1 500 Internal Server Error
Content-Type: application/json; 
…

"MyExceptionHandler で設定したレスポンス"

出力ウィンドウには、↓のように表示されました。Exception Loggers の後に Exception Handlers が実行されているのが分かります。

★呼び出し WebApplication41.MyExceptionLogger - Log
例外発生
Method : GET
Url : http://localhost:1130/error/constructor
Exception : System.InvalidOperationException: 型 'ValuesController' のコントローラーを作成するときにエラーが発生しました。コントローラーにパラメーターのないパブリック コンストラクターがあることを確認してください。 ---> System.Exception: コンストラクタで例外発生させる
   場所 WebApplication41.Controllers.ValuesController..ctor() ...省略
★呼び出し WebApplication41.MyExceptionHandler - Handle
例外フィルター属性との関連

アクションメソッド内で例外が発生した場合も、Exception Loggers と Exception Handlers は呼び出されます…が、次のような場合は Exception Handlers は呼び出されません。

  • 例外フィルター属性を併用し、フィルターの中で Response を指定している場合

Exception Loggers の方は必ず呼び出されます。この場合に返されるレスポンスは、例外フィルター属性内で設定したものになります。

例外フィルター属性と併用した場合の順番はこのようになります。

  • 1.アクションメソッド内で例外が発生
  • 2.Exception Loggers の呼び出し
  • 3.例外フィルター属性の呼び出し
    • Response が指定されている場合:指定された Response を返す
    • Response が指定されていない場合:Exception Handlers の呼び出し

実際に例外フィルター属性との併用を試してみます。先ほどのように、Exception Loggers と Exception Handlers を登録した上で、例外フィルター属性を適用させてみます。

ApiController :

using System;
using System.Web.Http;

namespace WebApplication41.Controllers
{
    public class ErrorController : ApiController
	{

		[Route("error/action")]
		[MyExceptionFilter] //例外フィルター属性適用
		public string GetHoge()
		{
			throw new Exception("アクションメソッド内で例外発生させる");
		}
	}
}

例外フィルター属性の作成:

using System;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Web.Http.Filters;

namespace WebApplication41
{
	public class MyExceptionFilterAttribute : ExceptionFilterAttribute
	{
		public override void OnException(HttpActionExecutedContext actionExecutedContext)
		{
			Debug.WriteLine(String.Format("★呼び出し {0} - {1}",
				System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName,
				System.Reflection.MethodBase.GetCurrentMethod().Name));

			var request = actionExecutedContext.Request;
			actionExecutedContext.Response = request.CreateResponse(
				HttpStatusCode.InternalServerError, "例外フィルターで設定したレスポンス");
		}
	}
}

この場合、レスポンスは以下のようになります。Exception Logger は呼び出されますが、例外フィルター属性内でレスポンスが指定されているので、Exception Handler は呼び出されません。

HTTP/1.1 500 Internal Server Error
Content-Type: application/json;
...

"例外フィルターで設定したレスポンス"


試しに、例外フィルター属性を↓のように変更し、レスポンスを指定しない場合を見てみます。

public override void OnException(HttpActionExecutedContext actionExecutedContext)
{
	Debug.WriteLine(String.Format("★呼び出し {0} - {1}",
		System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName,
		System.Reflection.MethodBase.GetCurrentMethod().Name));
	//actionExecutedContext.Response にレスポンスを指定しない
}

この場合返ってくるレスポンスは、Exception Handlers で指定したレスポンスになります。
ログは↓のようになります。

★呼び出し WebApplication41.MyExceptionLogger - Log
例外発生
Method : GET
Url : http://localhost:1130/error/action
Exception : System.Exception: アクションメソッド内で例外発生させる
   場所 WebApplication41.Controllers.ErrorController.GetHoge() ... 省略
★呼び出し WebApplication41.MyExceptionFilterAttribute - OnException
★呼び出し WebApplication41.MyExceptionHandler - Handle

例外フィルター属性の後に、Exceptiion Handlers が呼び出されていることがわかります。
一方の Exception Loggers は必ず呼び出されるので、例外フィルター属性をロギング代わりに使用していた場合、二重ログにならないように注意です。

そのほかの情報

破壊的変更は無し

今回はマイナーバージョンのリリースなので、既存のソースを ASP.NET Web API 2.1 に更新しても、例外フィルター属性の挙動や、アクションメソッド以外で例外が発生した場合の挙動等は変わりません。

例外情報の取得

例外の情報―Exception, Request, 発生した場所 etc… はコンテキストまとめられ、引数に渡されます。(ExceptionLoggerContext、ExceptionHandlerContext)
ドキュメント の後半あたりに、どのような情報が取得できるかの詳細が記載されています。

Exception Loggers と Exception Handlers の違い
  • Exception Loggers
    • 例外が発生した場合、必ず呼び出されます。
    • Exception Loggers は複数登録することができます。
  • Exception Handlers
    • 例外が発生した場合、レスポンスの内容を指定できる場合に呼び出されるみたい…(呼び出されないケースは、先述の例外フィルター属性と併用した時しか確認できていません。)
    • Excpetion Loggers と違って複数登録することができません。
ELMAH を使用したサンプル

Global Error Handling のサンプルとして、ASP.NET Web API に ELMAH を適用されたものが用意されています。Download Sample

ELMAH とは:
ASP.NET の未処理の例外をロギングしたりメール通知したりしてくれる便利なフレームワークです。
参考サイト:
ASP.NET MVCでELMAHを使う
ELMAHでASP.NETアプリの例外をログ

サンプルはすぐに実行できるようになっています。どこで例外が発生しようがすべてロギングされます。

f:id:miso_soup3:20140122013056p:plain

Global Error Handling を使うことで、ELMAH 等のようなサードパーティに例外情報を受け渡すことが可能になります。

Web API における例外処理まとめ

今回の更新で、ASP.NET Web API における例外処理の方法は、例外フィルター属性と Global Handle Error の2つになりました。
例外フィルター属性で行ってきた例外処理は、Global Handle Error でカバーできる可能性があります。が、無理に Global Handle Error にシフトする必要もないと思います。

棲み分けとしてはこんな感じです。

  • Exception Loggers
    • Web API 内で発生した例外をロギングする場合に使う
  • Exception Handlers
    • Web API 内で発生した例外を処理し、レスポンスをカスタマイズする場合に使う
  • 例外フィルター属性
    • 特定のアクションメソッド内で発生した例外のロギング、処理、レスポンスのカスタマイズ時に使う