ほぼすべての ASP.NET アプリケーションは、ユーザーのセッションのデータを追跡する必要があります。 ASP.NET は、セッション状態値を保存するための HttpSessionState クラスを提供します。各 HTTP 要求の HttpSessionState クラスのインスタンスには、静的な HttpContext.Current.Session プロパティを使用してアプリケーション全体からアクセスできます。 Page または UserControl の Session プロパティを使用すると、すべての Page および UserControl で同じインスタンスへのアクセスが簡単になります。
HttpSessionState クラスは、キーと値のペアのコレクションを提供します。キーは String 型で、値は Object 型です。これは、Session が非常に柔軟であり、ほぼあらゆる種類のデータを Session に保存できることを意味します。
ただし、この柔軟性にはコストがかかります (ただし、常に問題はあります)。コストは、アプリケーションにバグが入り込みやすくなるということです。導入される可能性のあるバグの多くは、単体テストでは検出されず、おそらくいかなる形式の構造化テストでも検出されません。これらのバグは、多くの場合、アプリケーションが運用環境にデプロイされたときにのみ表面化します。それらが表面化した場合、それがどのように発生したのかを特定し、バグを再現することが不可能ではないにしても、多くの場合非常に困難です。これは、修理に非常に費用がかかることを意味します。
この記事では、この種のバグを防ぐための戦略を紹介します。これは、HttpSessionState クラスによって提供される (あらゆるアプリケーションの要件を満たすことができる) 非常に自由なインターフェイスを、特定のアプリケーション専用に構築された適切に設計され制御されたインターフェイスでラップするという点で、Facade と呼ばれるデザイン パターンを使用します。デザイン パターンやファサード パターンに詳しくない場合は、インターネットで「ファサード デザイン パターン」を簡単に検索すると、多くの背景情報が得られます。ただし、この記事を理解するためにデザイン パターンを理解する必要はありません。
この記事に示されているサンプル コードは C# で記述されていますが、概念はどの .NET 言語にも適用できます。
何が問題ですか?
この記事のこのセクションでは、ファサードを使用せずに HttpSessionState クラスに直接アクセスする場合の問題について説明します。発生する可能性のあるバグの種類について説明します。
以下に、セッション状態変数にアクセスするために記述された一般的なコードを示します。
// セッション変数を保存します
Session["何らかの文字列"] = anyOldObject;
// セッション変数を読み取ります
DateTime startDate = (DateTime)Session["StartDate"];
この問題は、HttpSessionState が提供する柔軟なインターフェイスから発生します。キーは単なる文字列であり、値は厳密に型指定されていません。
文字列リテラルをキーとして使用する
文字列リテラルがキーとして使用される場合、キーの文字列値はコンパイラによってチェックされません。単純な入力ミスによって、新しいセッション値が簡単に作成されてしまいます。
セッション["受信"] = 27;...
セッション["受信"] = 32;
上記のコードでは、2 つの個別のセッション値が保存されています。
このようなバグのほとんどは単体テストによって特定されますが、常にそうとは限りません。値が期待どおりに変化していないことが必ずしも明らかであるとは限りません。
この種のバグは定数を使用することで回避できます:
private const string selected = "received";...
セッション[受信] = 27;...
セッション[受信] = 32;
型チェックなし
セッション変数に格納されている値の型チェックは行われません。コンパイラは、格納されている内容が正しいかどうかをチェックできません。
次のコードを考えてみましょう:
Session["maxValue"] = 27;...
int maxValue = (int)Session["maxValue"];
他の場所では、次のコードを使用して値を更新します。
セッション["最大値"] = 56.7;
「maxValue」セッション変数を maxValue int 変数に読み取るコードが再度実行されると、InvalidCastException がスローされます。
このようなバグのほとんどは単体テストによって特定されますが、常にそうとは限りません。
意図せずにキーを再利用する
各ページでセッション キーの定数を定義した場合でも、ページ間で意図せずに同じキーを使用する可能性があります。次の例を考えてみましょう:
1 ページのコード:
private const string edit = "edit";...
セッション[編集] = true;
最初のページの後に表示される 2 ページ目のコード:
private const string edit = "edit";...
if ((bool)セッション[編集])
{
...
}
3 番目の無関係なページのコード:
private const string edit = "edit";...
セッション[編集] = false;
何らかの理由で 2 ページ目が表示される前に 3 ページ目が表示された場合、値が期待どおりにならない可能性があります。コードはおそらく正常に実行されますが、結果は間違ったものになるでしょう。
通常、このバグはテストでは検出されません。バグが現れるのは、ユーザーがページ ナビゲーションの特定の組み合わせを実行したとき (または新しいブラウザ ウィンドウを開いたとき) のみです。
最悪の場合、バグが現れたことに誰も気づかず、データを意図しない値に変更してしまう可能性があります。
キーを意図せず再利用 - 再び
上の例では、同じデータ型がセッション変数に格納されています。格納されるものの型チェックがないため、互換性のないデータ型の問題も発生する可能性があります。
1 ページのコード:
Session["FollowUp"] = "true";
2 ページ目のコード:
Session["FollowUp"] = 1;
3 ページ目のコード:
Session["FollowUp"] = true;
バグが現れると、InvalidCastException がスローされます。
通常、このバグはテストでは検出されません。バグが現れるのは、ユーザーがページ ナビゲーションの特定の組み合わせを実行したとき (または新しいブラウザ ウィンドウを開いたとき) のみです。
私たちに何ができるでしょうか?
最初のクイックフィックス
まずできる最も簡単な事は、セッション キーに文字列リテラルを決して使用しないようにすることです。常に定数を使用して、単純な入力ミスを避けてください。
private const string limit = "limit";...
セッション[制限] = 27;...
セッション[制限] = 32;
ただし、定数がローカル (ページ レベルなど) で定義されている場合、同じキーを意図せずに再利用する可能性があります。
より良いクイックフィックス
各ページで定数を定義するのではなく、すべてのセッション キー定数を 1 つの場所にグループ化し、Intellisense に表示されるドキュメントを提供します。ドキュメントには、セッション変数が何に使用されるかを明確に示す必要があります。たとえば、セッション キー専用のクラスを定義します。
public static class SessionKeys
{
/// <概要>
/// 最大...
/// </概要>
public const string Limit = "制限";
...
セッション[SessionKeys.Limit] = 27
;
新しいセッション変数が必要な場合、すでに使用されている名前を選択すると、定数を SessionKeys クラスに追加するときにわかります。現在どのように使用されているかを確認し、別のキーを使用する必要があるかどうかを判断できます。
ただし、データ型の一貫性はまだ保証されていません。
より良い方法 - ファサードの使用
HttpSessionState には、アプリケーション内の 1 つの静的クラス (ファサード) 内からのみアクセスしてください。ページまたはコントロールのコード内から Session プロパティに直接アクセスすることはできません。また、ファサード内以外から HttpContext.Current.Session に直接アクセスすることもできません
。すべてのセッション変数は、ファサード クラスのプロパティとして公開されます。
これには、すべてのセッション キーに単一のクラスを使用するのと同じ利点に加えて、次の利点があります。
セッション変数に入力される内容の厳密な型指定。
セッション変数が使用されるコードでキャストする必要はありません。
プロパティ セッターのすべての利点は、(単に型を入力するだけではなく) セッション変数に何が入力されるかを検証することです。
セッション変数にアクセスする際のプロパティ ゲッターのすべての利点。たとえば、変数に初めてアクセスしたときに変数を初期化します。
セッションファサードクラスの例
以下は、MyApplication というアプリケーションのセッション ファサードを実装するクラスの例です。
崩壊
/// <概要>
/// MyApplicationSession は、ASP.NET Session オブジェクトにファサードを提供します。
/// セッション変数へのすべてのアクセスは、このクラスを介して行う必要があります。
/// </概要>
パブリック静的クラス MyApplicationSession
{
# 領域のプライベート定数
//------------------------------------------------ ---------------------
private const string userAuthorisation = "ユーザー認証";
private const string TeamManagementState = "TeamManagementState";
private const string startDate = "開始日";
private const string endDate = "EndDate";
//------------------------------------------------ ---------------------
# endregion
# リージョン Public Properties
//------------------------------------------------ ---------------------
/// <概要>
/// ユーザー名は、現在のユーザーのドメイン名とユーザー名です。
/// </概要>
パブリック静的文字列ユーザー名
{
get { return HttpContext.Current.User.Identity.Name; }
}
/// <概要>
/// UserAuthorisation には、次の権限情報が含まれます。
/// 現在のユーザー。
/// </概要>
public static UserAuthorisation UserAuthorisation
{
得る
{
ユーザー認証 userAuth
= (ユーザー認証)HttpContext.Current.Session[ユーザー認証];
// UserAuthorisation の有効期限が切れているかどうかを確認します
もし (
userAuth == null ||
(userAuth.Created.AddMinutes(
MyApplication.Settings.Caching.AuthorisationCache.CacheExpiryMinutes))
< 日時.現在
)
{
userAuth = UserAuthorisation.GetUserAuthorisation(ユーザー名);
ユーザー認証 = ユーザー認証;
ユーザー認証を返します
。
}
プライベートセット
{
HttpContext.Current.Session[userAuthorisation] = 値;
}
}
/// <概要>
/// TeamManagementState は、現在の状態を保存するために使用されます。
/// TeamManagement.aspx ページ。
/// </概要>
パブリック静的 TeamManagementState TeamManagementState
{
得る
{
return (TeamManagementState)HttpContext.Current.Session[teamManagementState];
}
セット
{
HttpContext.Current.Session[チーム管理状態] = 値;
}
}
/// <概要>
/// StartDate は、レコードのフィルタリングに使用される最も古い日付です。
/// </概要>
public static DateTime StartDate
{
得る
{
if (HttpContext.Current.Session[startDate] == null)
DateTime.MinValue を返します。
それ以外
return (DateTime)HttpContext.Current.Session[startDate];
}
セット
{
HttpContext.Current.Session[開始日] = 値;
}
}
/// <概要>
/// EndDate は、レコードのフィルタリングに使用される最新の日付です。
/// </概要>
public static DateTime EndDate
{
得る
{
if (HttpContext.Current.Session[endDate] == null)
DateTime.MaxValue を返します。
それ以外
return (DateTime)HttpContext.Current.Session[endDate];
}
セット
{
HttpContext.Current.Session[endDate] = 値;
}
}
//------------------------------------------------ ---------------------
# 終了領域
}
このクラスは、値が明示的に格納されていない場合にデフォルト値を提供できるプロパティ ゲッターの使用法を示します。たとえば、StartDate プロパティには、デフォルトとして DateTime.MinValue が提供されます。
UserAuthorisation プロパティのプロパティ ゲッターは、UserAuthorisation クラス インスタンスの単純なキャッシュを提供し、セッション変数内のインスタンスが最新の状態に保たれるようにします。このプロパティはプライベート セッターの使用も示しているため、セッション変数の値はファサード クラスの制御下でのみ設定できます。
Username プロパティは、かつてはセッション変数として保存されていたかもしれないが、現在はこの方法では保存されていない値を示します。
次のコードは、ファサードを通じてセッション変数にアクセスする方法を示しています。このコードではキャストを行う必要がないことに注意してください。
// セッション変数を保存します
MyApplicationSession.StartDate = DateTime.Today.AddDays(-1);
// セッション変数を読み取ります
DateTime startDate = MyApplicationSession.StartDate;
追加の特典
ファサード デザイン パターンの追加の利点は、内部実装がアプリケーションの残りの部分から隠蔽されることです。おそらく将来的には、組み込みの ASP.NET HttpSessionState クラス以外の、セッション状態を実装する別のメカニズムを使用することを決定するかもしれません。ファサードの内部を変更するだけで済みます。アプリケーションの残りの部分は何も変更する必要はありません。
まとめ
HttpSessionState のファサードを使用すると、セッション変数にアクセスするためのより堅牢な方法が提供されます。これは実装が非常に簡単なテクニックですが、大きなメリットがあります。