ASP.NET MVCのフィルターを触ってみた

__この記事は[ASP.NET Advent Calendar 2016](http://qiita.com/advent-calendar/2016/asp-net)の15日目の記事です。__

ASP.NET MVCのFilterについて簡単にまとめてみます。
# Filterの種類
## 承認フィルター
IAuthorizationFilterを実装
セキュリティ的にアクションを実行できるかの判断処理。
他のフィルターより前に実行される。
OnAuthorization・・・承認メソッド
ValidateAntiForgeryTokenAttributeが有名か。

## アクションフィルター
IActionFilterを実装
アクションの前後に行いたい処理。
OnActionExecuting・・・アクション前に呼び出されるメソッド
OnActionExecuted・・・アクション後に呼び出されるメソッド

## 結果フィルター
IResultFilterを実装
ActionResultオブジェクトの実行前後に行いたい処理。
OnResultExecuting・・・ActionResultオブジェクト実行前に呼び出されるメソッド
OnResultExecuted・・・ActionResultオブジェクト実行後に呼び出されるメソッド

## 例外フィルター
IExceptionFilterを実装
MVC内でスローされた未処理の例外がある場合に呼び出される処理。
OnException・・・例外発生時に呼び出されるメソッド

# MyFilterを作ってみた
ここからは独自実装の話になります。

通常、MVCを使ってUA判定でビューを切り替える場合はDisplayModesが使われると思いますが、
UA判定条件をカスタマイズしたかった為、Filterを使って実装してみました。

```csharp:デバイス種別を持った薄いControllerラッパー
public class MyControllerBase : Controller
{
    // デバイス種別
    public enum DeviceType : byte
    {
        // PC
        PC = 0,
        // スマートフォン
        SmartPhone = 1,
    }
    public DeviceType TargetDevice { get; set; }
}
```
```csharp:デバイス判定用ActionFilter属性クラス
public class DeviceJudgmentAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        // MyControllerBaseを継承しているControllerクラスであることが前提
        MvcControllerBase ctr = filterContext.Controller as MyControllerBase ;
        if (ctr != null)
        {
            // Requestを渡してデバイス種別を受け取る
            ctr.TargetDeviceType = GetDeviceType(filterContext.HttpContext.Request);
        }
        base.OnActionExecuting(filterContext);
    }
  
    // デバイス判定処理
    private DeviceType GetDeviceType(HttpRequestBase request)
    {
        // 判定Logicは省略
    }
}
```

あとは、このフィルターを`MyControllerBase`を継承したControllerの好きなところに
属性として指定してあげれば完了です。

`OnActionExecuting`でアクション実行前にデバイス種別が取得出来ているので、
あとはその後のロジックで有効に活用してあげましょう。

# アクションフィルターと承認フィルター
上記のアクションフィルターを実装してUA毎のページ切り分けを行っていたのですが、
実装後しばらくしてから問題があることに気付きました。

問題というのは __「例外フィルターで表示するエラーページをデバイス毎に切り分け」__ です。

通常であれば、アクション実行前にフィルターが動いてUA判定を行ってくれるのでいいのですが、
アクション実行前に動いてしまうものがあります。

そうです。 __「承認フィルター」__ です。

承認処理は全てのフィルターの最初に実行され、承認が通らなかった場合は例外をスローします。

スローされた例外を基本的に例外フィルターで拾ってエラーページを表示していたのですが、
アクションフィルター実行前なのでUA判定が行われておらず、期待したページが表示されなかったのです。

# ActionInvokerに無理矢理組み込む
ってことで、各フィルターを呼び出しているActionInvokerを無理矢理拡張しました。

```csharp:デバイス判定処理を追加したActionInvokerクラス
public class DeviceJudgmentActionInvoker : ControllerActionInvoker
{
    // アクションを呼び出す処理をオーバーライドし、デバイス判定処理を先に行う
    public override bool InvokeAction(ControllerContext controllerContext, string actionName)
    {
        // MyControllerBaseを継承しているControllerクラスであることが前提
        MvcControllerBase ctr = filterContext.Controller as MyControllerBase ;
        if (ctr != null)
        {
            // Requestを渡してデバイス種別を受け取る
            ctr.TargetDeviceType = GetDeviceType(filterContext.HttpContext.Request);
        }
        return base.InvokeAction(controllerContext, actionName);
    }

    // デバイス判定処理
    private DeviceType GetDeviceType(HttpRequestBase request)
    {
        // 判定Logicは省略
    }
}
```
```csharp:ControllerでActionInvokerの読み込み
public class MyController : MyControllerBase
{
    public MyController()
    {
        // コンストラクタ内でControllerの持つActionInvokerプロパティに、上記ActionInvokerインスタンスを設定
        ActionInvoker = new SetTargetDeviceActionInvoker();
    }
}
```

この実装で、承認フィルター前にデバイス判定を行うことが出来ました。

## ActionInvoker拡張の注意点
上記の方法はフィルター実装前での処理になりますので、
これらの処理の中で例外が発生した場合は例外フィルターで拾えません。