ASP.NET MVC の ActionFilter でセッションの値を管理する

この記事は ASP.NET Advent Calendar 2014 - Qiita の 11 日目の記事です。なんか空いてたので登録してみました。昨日は KatsuYuzu さんの ASP.NET の customErrors、IISの httpErrors #aspnetjp - KatsuYuzuのブログ でした。明日もまだ空いているようなのでネタのある方はいかがでしょうか。

ActionFilter について

ActionFilter とは ASP.NET MVC で提供されている Attribute *1のひとつである、 System.Web.Mvc.ActionFilterAttribute のことです。

開発者はこの ActionFilterAttribute のサブクラスを実装することで、 Controller のアクションメソッドの処理前後に任意の処理を挟むことができます。また ActionFilterAttribute を使用することで、以下のようなことができます。

  • HttpContext にアクセスできる
  • Controller のアクションメソッド引数に任意の値を注入できる

HttpContext にアクセスできるということは、セッションにもアクセスできるということです((実際には HttpContext.Current を使えば別にここ以外でもセッションにアクセスできますが・・・))。つまりセッションから任意の値を取り出して Controller のメソッド引数にセットするなんてことができるわけで、これらを利用すれば Controller からセッション情報の管理処理を追い出すことができそうです。ユニットテストがはかどりますね。

実際にやってみます。

セッションに保持するモデルクラス

なんでもいいですが、簡単にこんなクラスを用意します。

    [Serializable]
    public class SessionModel
    {
        public string Value { get; set; }
    }

普通に Controller からセッションを操作すると、アクションメソッドの実装は以下のような感じになります。

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            // セッションから取得
            var model = (SessionModel)HttpContext.Session[sessionKey];
            if (model == null)
            {
                model = new SessionModel();
                HttpContext.Session[sessionKey] = model;
            }

            // なんか処理
            DoSomething(model);

            // セッションから削除
            HttpContext.Session.Remove(sessionKey);

            return View();
        }
    }

これを Attribute を使ってできるようにします。

Attribute

ActionFilterAttribute を継承した、こんな感じの Attribute を作ります。 Controller のアクションメソッドの引数に値を注入するには、 filterContext.ActionParameters に引数名を指定して値をセットします。セッションの値の取得・更新・削除については特筆すべき点はありませんね。

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
    public class SessionModelParamAttribute : ActionFilterAttribute
    {
        private static readonly string SessionKey = "hogehoge";

        /// <summary>
        /// セッションの値を設定する引数名。
        /// </summary>
        public string ParamName { get; set; }
        /// <summary>
        /// 処理完了後にセッションの値を破棄するかどうか。
        /// </summary
        public bool Destroy { get; set; }

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            // ここの処理はアクションメソッドの実行前に呼ばれる

            if (ParamName == null)
            {
                return;
            }

            // セッション値がセットされてなければ作成
            var model = (SessionModel)filterContext.HttpContext.Session[SessionKey];
            if (model == null)
            {
                model = new SessionModel();
                filterContext.HttpContext.Session[SessionKey] = model;
            }

            // アクションメソッドの引数にセッション値を設定
            filterContext.ActionParameters[ParamName] = model;
        }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            // ここの処理はアクションメソッドの実行後に呼ばれる

            if (Destroy)
            {
                // セッション値を破棄
                filterContext.HttpContext.Session.Remove(SessionKey);
            }
        }
    }

やらなくても動きますが、 Controller でセッション値を更新する場合は OnActionExecuted でセッションに値を再セットしたほうがいいかもしれません*2

Controller

こんな感じで Attribute を使えます。これで ParamName で指定した名前の引数にセッションの値がセットされた状態でメソッドが実行されます。 ParamName の指定を忘れるとリクエストパラメーターでモデルの値が指定できてしまうので注意しましょう。

    public class HomeController : Controller
    {
        [SessionModelParam(ParamName = "model", Destroy = true)]
        public ActionResult Index(SessionModel model) // model にセッションの値が入っている
        {
            // なんか処理
            DoSomething(model.Value);

            return View();
            // 処理完了後、セッションの値は Attribute で破棄される
        }
    }

すっきりしましたね。

今回は単純なサンプルですが、 Attribute の実装を工夫すればいろいろ柔軟な処理ができそうですね。

*1:Java でいうところのアノテーションですね

*2:Javaサーブレットでは明示的に HttpSession.setAttribute を呼ばないとクラスタリング環境でセッション値の更新が同期されないという問題がありましたが、 .NET でも同じなのかな?