C#と諸々

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

2007/09/16 22:40
最近、セッションステートの上手い利用方法を模索してます。
CodeZine に「ASP.NETのセッションをタイプセーフに取り扱うクラスの作成」という記事が載ってましたが、この方法は僕的にちょっと馴染めなかったもので。
以下、現時点での考えをメモしときます。


セッションステートに保存するオブジェクトは、セッションステートで管理されることを目的として定義されたクラスだけにします。
だから、セッションステートの項目キーはクラスの完全修飾名でいいと考えています。
文字列とかドメインオブジェクト、データセットなんかを直接保存したりはしません。


まず、クラスの完全修飾名を使ってセッションステートを利用するための補助を行う静的クラスを定義しておきます。
項目が見つからなかったり、セッションステートが使えない時 ( IRequiresSessionState インターフェイスを実装していない HTTP ハンドラからの呼び出し等 ) には例外を発生させるようにもします。

SessionStateAdapter(T) クラス
using System;
using System.Web;
using System.Web.SessionState;

/// <summary>
/// 型の完全修飾名を項目キーとしてセッションステートを利用するアダプターです。
/// </summary>
/// <typeparam name="T">セッションステートに保存するインスタンスの型。この型の完全修飾名がセッションステートの項目キーになります。</typeparam>
public static class SessionStateAdapter<T>
{
    /// <summary>
    /// ジェネリックパラメータ T のインスタンスを現在のセッションステートから取得します。
    /// </summary>
    /// <returns>ジェネリックパラメータ T のインスタンス</returns>
    /// <exception cref="SessionItemNotFoundException">指定した項目がセッションステート内に存在しません。</exception>
    /// <exception cref="System.InvalidOperationException">セッションステートが利用できません。</exception>
    public static T GetItem()
    {
        HttpSessionState currentSession = SessionStateAdapter<T>.GetSessionState();

        object result = currentSession[typeof(T).FullName];
        if ((result == null) || !(result is T))
        {
            throw new SessionItemNotFoundException(typeof(T).FullName);
        }
        return (T)result;
    }

    /// <summary>
    /// ジェネリックパラメータ T のインスタンスを現在のセッションステートに設定します。
    /// </summary>
    /// <param name="target">ジェネリックパラメータ T のインスタンス。</param>
    /// <exception cref="System.InvalidOperationException">セッションステートが利用できません。</exception>
    /// <exception cref="System.InvalidOperationException">セッションステートが読み取り専用です。</exception>
    /// <exception cref="System.ArgumentNullException">引数 target が null です。</exception>
    public static void SetItem(T target)
    {
        if (target == null)
        {
            throw new ArgumentNullException("target");
        }
        HttpSessionState currentSession = SessionStateAdapter<T>.GetSessionState();
        if (currentSession.IsReadOnly)
        {
            throw SessionStateAdapter<T>.CreateExceptionForSessionStateIsReadOnly();
        }
        currentSession[typeof(T).FullName] = target;
    }

    /// <summary>
    /// ジェネリックパラメータ T のインスタンスを現在のセッションステートから削除します。
    /// </summary>
    /// <exception cref="System.InvalidOperationException">セッションステートが利用できません。</exception>
    /// <exception cref="System.InvalidOperationException">セッションステートが読み取り専用です。</exception>
    public static void RemoveItem()
    {
        HttpSessionState currentSession = SessionStateAdapter<T>.GetSessionState();
        if (currentSession.IsReadOnly)
        {
            throw SessionStateAdapter<T>.CreateExceptionForSessionStateIsReadOnly();
        }
        currentSession.Remove(typeof(T).FullName);
    }

    /// <summary>
    /// ジェネリックパラメータ T のインスタンスが現在のセッションステートに存在するかどうかを表す値を返します。
    /// </summary>
    /// <returns>ジェネリックパラメータ T のインスタンスが現在のセッションステートに存在するかどうかを表す値。</returns>
    /// <exception cref="System.InvalidOperationException">セッションステートが利用できません。</exception>
    public static bool ItemIsExists()
    {
        HttpSessionState currentSession = SessionStateAdapter<T>.GetSessionState();
        return (currentSession[typeof(T).FullName] is T);
    }

    /// <summary>
    /// 現在のセッションステートを取得します。
    /// </summary>
    /// <returns>現在のセッションステート。</returns>
    /// <exception cref="System.InvalidOperationException">セッションステートが利用できません。</exception>
    private static HttpSessionState GetSessionState()
    {
        HttpContext currentContext = HttpContext.Current;
        HttpSessionState currentSession;
        try
        {
            currentSession = currentContext.Session;
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException("セッションステートが利用できません。", ex);
        }
        if (currentSession == null)
        {
            throw new InvalidOperationException("セッションステートが利用できません。");
        }

        return currentSession;
    }

    /// <summary>
    /// セッションステートが読み取り専用であることを示す例外を生成します。
    /// </summary>
    /// <returns>セッションステートが読み取り専用であることを示す例外。</returns>
    private static Exception CreateExceptionForSessionStateIsReadOnly()
    {
        return new InvalidOperationException("セッションステートが読み取り専用です。");
    }
}



セッションステート内に項目が見つからなかった場合にスローされる SessionItemNotFoundException クラスは以下のコードになります。
追加情報として項目のキーを持つことができるようにしてます。

SessionItemNotFoundException クラス
using System;
using System.Runtime.Serialization;
using System.Security.Permissions;

/// <summary>
/// セッションステート内に特定の項目が見つからないことを表す例外。
/// </summary>
[Serializable]
public class SessionItemNotFoundException : Exception
{
    #region Constructors

    /// <summary>
    /// SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
    /// </summary>
    public SessionItemNotFoundException()
        : this(null)
    {
    }

    /// <summary>
    /// この例外の原因である項目のキーを指定して、SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="key">例外の原因となった項目キー。</param>
    public SessionItemNotFoundException(string key)
        : this(key, "指定した項目がセッションステート内に存在しません。")
    {
    }

    /// <summary>
    /// エラー メッセージ、およびこの例外の原因である項目のキーを指定して、SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="key">例外の原因となった項目キー。</param>
    /// <param name="message">エラーを説明するメッセージ。</param>
    public SessionItemNotFoundException(string key, string message)
        : this(key, message, null)
    {
    }

    /// <summary>
    /// エラー メッセージ、項目キー、およびこの例外の原因である内部例外への参照を使用して、SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="key">例外の原因となった項目キー。</param>
    /// <param name="message">例外の原因を説明するエラー メッセージ。</param>
    /// <param name="innerException">現在の例外の原因である例外。内部例外が指定されていない場合は、null 参照 (Visual Basic の場合は Nothing)。</param>
    public SessionItemNotFoundException(string key, string message, Exception innerException)
        : base(message, innerException)
    {
        this._key = key;
    }

    /// <summary>
    /// シリアル化したデータを使用して、SessionItemNotFoundException クラスの新しいインスタンスを初期化します。
    /// </summary>
    /// <param name="context">転送元または転送先に関するコンテキスト情報を含んでいる System.Runtime.Serialization.StreamingContext。</param>
    /// <param name="info">スローされている例外に関するシリアル化済みオブジェクト データを保持している System.Runtime.Serialization.SerializationInfo。</param>
    /// <exception cref="System.Runtime.Serialization.SerializationException">クラス名が null であるか、または System.Exception.HResult が 0 です。</exception>
    /// <exception cref="System.ArgumentNullException">info パラメータが null です。</exception>
    protected SessionItemNotFoundException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
        this._key = info.GetString("key");
    }

    #endregion

    #region Fields

    /// <summary>
    /// 例外の原因となった項目キーを取得します。
    /// </summary>
    private readonly string _key;

    #endregion

    #region Properties

    /// <summary>
    /// 例外の原因となった項目キーを取得します。
    /// </summary>
    public string Key
    {
        get
        {
            return this._key;
        }
    }

    #endregion

    #region Methods

    /// <summary>
    /// パラメータ名と追加の例外情報を使用して System.Runtime.Serialization.SerializationInfo オブジェクトを設定します。
    /// </summary>
    /// <param name="context">転送元または転送先に関するコンテキスト情報。</param>
    /// <param name="info">シリアル化されたオブジェクト データを保持するオブジェクト。</param>
    /// <exception cref="System.ArgumentNullException">info オブジェクトが null 参照 (Visual Basic の場合は Nothing) です。</exception>
    [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        if (info == null)
        {
            throw new ArgumentNullException("info");
        }
        base.GetObjectData(info, context);

        // 追加の例外情報がある場合、ここでパラメータ名と例外情報を info に追加します。
        info.AddValue("key", this._key, typeof(string));
    }

    #endregion
}




ここからが、肝心の「セッションステートで管理されることを目的として定義されたクラス」になります。

業務アプリケーションは大抵の場合、情報の検索だとか、情報の登録、物品の貸出等の複数の機能を持っています。
ここでは、これら一つ一つを "業務" と呼びます。(こうやって呼ぶのって一般的なのかな?)
僕は、画面にビジネスロジックをできるだけ持ち込まないために、ファサードパターンを適用して各業務毎にクラスを用意します。
例えば、ほげ業務には HogeBusiness クラス、ふが業務には FugaBusiness クラスを用意します。
そして、この業務クラスをセッションに保存しておき、複数画面に渡って業務を行います。
そこで、BusinessManager という名前の「セッションステートで管理されることを目的として定義されたクラス」を一つ用意し、そのクラスのフィールドで全ての業務を管理します。
BusinessManager は、Current という読み取り専用のスタティックプロパティを持ちます。
この Current プロパティは、セッションステートを使用してシングルトンパターンのような動きをします。


BusinessManager クラス
using System;

/// <summary>
/// 業務を管理します。
/// </summary>
[Serializable]
public class BusinessManager
{
    #region Static Members

    #region public static BusinessManager Current

    /// <summary>
    /// 現在のセッションにて管理されている BusinessManager オブジェクトを取得します。
    /// </summary>
    /// <exception cref="System.InvalidOperationException">セッションステートが利用できません。</exception>
    /// <exception cref="System.InvalidOperationException">セッションステートが読み取り専用です。</exception>
    public static BusinessManager Current
    {
        get
        {
            BusinessManager currentBusinessManager;
            bool isExists = SessionStateAdapter<BusinessManager>.ItemIsExists();
            if (isExists)
            {
                currentBusinessManager = SessionStateAdapter<BusinessManager>.GetItem();
            }
            else
            {
                currentBusinessManager = new BusinessManager();
                SessionStateAdapter<BusinessManager>.SetItem(currentBusinessManager);
            }
            return currentBusinessManager;
        }
    }

    #endregion

    #endregion

    #region Fields

    #region private HogeBusiness _hogeBusiness

    /// <summary>
    /// ほげ業務を取得または設定します。
    /// </summary>
    private HogeBusiness _hogeBusiness;

    #endregion

    #region private FugaBusiness _fugaBusiness

    /// <summary>
    /// ふが業務を取得または設定します。
    /// </summary>
    private FugaBusiness _fugaBusiness;

    #endregion

    #endregion

    #region Properties

    #region public HogeBusiness HogeBusiness

    /// <summary>
    /// ほげ業務を取得します。
    /// </summary>
    /// <exception cref="System.InvalidOperationException">ほげ業務が開始されていません。</exception>
    public HogeBusiness HogeBusiness
    {
        get
        {
            if (this._hogeBusiness == null)
            {
                throw this.CreateExceptionForNotStartedBusiness("ほげ");
            }
            return this._hogeBusiness;
        }
    }

    #endregion

    #region public FugaBusiness FugaBusiness

    /// <summary>
    /// ふが業務を取得します。
    /// </summary>
    /// <exception cref="System.InvalidOperationException">ふが業務が開始されていません。</exception>
    public FugaBusiness FugaBusiness
    {
        get
        {
            if (this._fugaBusiness == null)
            {
                throw this.CreateExceptionForNotStartedBusiness("ふが");
            }
            return this._fugaBusiness;
        }
    }

    #endregion

    #endregion

    #region Constructors

    #region private BusinessManager()

    /// <summary>
    /// BusinessManager クラスの新しいインスタンスを初期化します。
    /// </summary>
    private BusinessManager()
    {
        this.InitializeFields();
    }

    #endregion

    #endregion

    #region Methods

    #region public void StartHogeBusiness()

    /// <summary>
    /// ほげ業務を開始します。
    /// </summary>
    public void StartHogeBusiness()
    {
        this._hogeBusiness = new HogeBusiness();
    }

    #endregion

    #region public void EndHogeBusiness()

    /// <summary>
    /// ほげ業務を終了します。
    /// </summary>
    /// <returns></returns>
    public void EndHogeBusiness()
    {
        this._hogeBusiness = null;
    }

    #endregion

    #region public void StartFugaBusiness()

    /// <summary>
    /// ふが業務を開始します。
    /// </summary>
    public void StartFugaBusiness()
    {
        this._fugaBusiness = new FugaBusiness();
    }

    #endregion

    #region public void EndFugaBusiness()

    /// <summary>
    /// ふが業務を終了します。
    /// </summary>
    /// <returns></returns>
    public void EndFugaBusiness()
    {
        this._fugaBusiness = null;
    }

    #endregion

    #region public void EndAllBusiness()

    /// <summary>
    /// 全ての業務を終了します。
    /// </summary>
    public void EndAllBusiness()
    {
        this.InitializeFields();
    }

    #endregion

    #region private void InitializeFields()

    /// <summary>
    /// フィールドを初期化します。
    /// </summary>
    private void InitializeFields()
    {
        this._hogeBusiness = null;
        this._fugaBusiness = null;
    }

    #endregion

    #region private void CreateExceptionForNotStartedBusiness(string businessName)

    /// <summary>
    /// 業務が開始されていないことを表す例外を生成します。
    /// </summary>
    /// <param name="businessName">業務名。</param>
    private Exception CreateExceptionForNotStartedBusiness(string businessName)
    {
        string message = string.Format("{0}業務は開始されていません。", businessName);
        return new InvalidOperationException(message);
    }

    #endregion

    #endregion
}



セッションには BusinessManager のインスタンスが一つだけ格納され、そのインスタンスの各フィールドで業務クラスが管理されます。
セッションで扱う情報は業務クラス以外にもありますので、それらにはまた別の「セッションステートで管理されることを目的として定義されたクラス」を用意します。
それらもまた、Current という読み取り専用のスタティックプロパティを定義し、セッションステートを使用したシングルトンパターンのように実装します。


とりあえず、今の考えはこんな感じです。
他にも、「セッションステートで管理されることを目的として定義されたクラス」を各業務ごとに用意するという方法も考えていて、それはまた少し異なる仕組みとなります ( シングルトンパターンではなくなります ) 。



// コード修正履歴
2007/09/17
・セッションステートが読み取り専用の時は、SessionStateAdapter<T>.SetItem(T) メソッド、SessionStateAdapter<T>.RemoveItem(T) メソッドが例外をスローするよう修正。

2008/12/14
・NotFoundSessionItemException クラスの名称を SessionItemNotFoundException に変更。
タグ: .NET C# ASP.NET











トラックバックURL↓
http://csharper.blog57.fc2.com/tb.php/160-ac303e4d

SessionStateAdapter クラス改良
以前、セッションステートの利用方法を模索中 という記事で掲載した SessionStateAdapter クラスをこちらの問題に対応させましたので、掲載します。...

2008.12.16 22:04 | C#と諸々