![](https://habrastorage.org/storage2/00b/c6d/200/00bc6d2004d3fa3ec3c1d5864c46b23f.png)
ASP 。
- ASP.NETプロセス内のサーバーメモリにセッション情報を保存する
- 状態サーバーにセッション情報を保存する
- 事前定義されたスキーマでSQL Serverデータベースにセッション情報を保存する
しかし、すぐに使えるオプションがいくつあっても、開発者が直面するタスクに完全に答えることはできません。 この記事では、 ASP.NET (MVC)用の独自のセッション状態プロバイダーを実装する方法について説明します 。
セッションストレージはSQL Serverになります 。 EntityFrameworkを介してデータベースを操作します。
目次
なぜこの記事を書いているのですか?
長い間、私はphpでサイトを開発していました。 ある時点で、私は何か新しいことを学ぶことにし、そのためにASP.NETを選択しました。 phpセッションのプリズムを通して、ASP.NETの承認、メンバーシップ、および役割は、それがどのように機能するかを理解していないことから多くの質問と不満を引き起こしました。
数年後、その仕組みを説明するだけでなく、ASP.NETには独自の方法で変更および実行できないものがないことを示すために、一連の記事を作成することにしました。
独自のプロバイダーを実装する理由
サポートされていないストレージを使用する
マイクロソフト製品は非常に高価であり、事実であり、無料版には多くの制限があります。 したがって、 MySQLやPostgreSQLなどの別のデータベースを無料版で使用することをお勧めします 。
一方、 Key-Valueストレージを使用して、分散アプリケーションのパフォーマンスを向上させることができます。 または、購入した他社の製品のライセンスを持っているだけかもしれません。
カスタムデータベーステーブルスキーマ
この理由は最も一般的です。
標準機能は優れていますが、アプリケーションが複雑になるほど、その作業に必要な非標準ソリューションが増えます。
例:
サイトには、特定のユーザーのセッションを閉じる(強制ログアウトする)機能が必要です。 セッションにはユーザーメンバーシップ情報が保存されないため、SQL Serverの標準データベーススキーマはこの機能をサポートしません。
ASP.NETでセッションを管理する人と方法
SessionStateModuleは状態(セッション)の処理を担当し、デフォルトのハンドラーです。 必要に応じて、セッションの処理を担当する独自のhttp-moduleを実装できます。
SessionStateModuleオブジェクトは、操作中に特定のプロバイダーメソッドを呼び出すことにより、セッションプロバイダーと対話します。 モジュールを使用するセッションプロバイダーは、Webアプリケーションの構成に基づいて決定します。 すべてのセッションプロバイダーは、 SessionStateStoreProviderBaseクラスから継承する必要があります。このクラスは、 SessionStateModuleに必要なメソッドを定義します。
セッションスキーム
以下は、ASP.NETでセッションがどのように機能するかをよりよく理解するための、プロバイダーメソッドの呼び出しの概要です(クリック可能)。
![](https://habrastorage.org/storage2/14e/fd5/e3a/14efd5e3a25d0cfb92fe01050046256a.png)
図 セッションを操作するためのメソッド呼び出しのシーケンス
まず、 SessionStateModuleは、このページ(ASP.NET WebForms)またはコントローラー(ASP.NET MVC)のセッションモードを決定します。
ページに属性が設定されている場合:
<%@ Page EnableSessionState = "true | false | ReadOnly"%>
(またはASP.NET MVCのSessionState属性)
セッションでのその動作は、 読み取り専用モード(読み取り専用)で発生し、全体的なパフォーマンスがわずかに向上します。 それ以外の場合、 SessionStateModuleモジュールは排他的アクセスを要求し、セッションのコンテンツをブロックします。 ロックは、クエリの最終段階でのみ解放されます。
セッションロックが必要なのはなぜですか?
ASP.NETアプリケーションはマルチスレッドアプリケーションであり、 Ajaxテクノロジーは非常に人気があります。 競合を避けたり、保存されている値を上書きしたり、古い値を取得したりするために、複数のスレッドが同時に同じユーザーのセッションを使用するという状況が発生する場合があります。
ロックは、複数のスレッドから同じユーザーのセッションにアクセスする場合にのみ発生します。
セッションリソースにアクセスしたスレッドは、要求の期間中に最初に排他的アクセスを取得します。 残りのスレッドは、リソースが解放されるか、短いタイムアウトが発生するまで待機します。
スレッド間に競合がない、または重大な結果をもたらさないと信じる理由がある場合、サポートをブロックせずにプロバイダーを実装できます。
セッションプロバイダーの実装
セッションデータを保存するテーブルの作成
次のテーブル構造を使用して、セッション状態データを保存します。
![](https://habrastorage.org/storage2/b66/c19/465/b66c194651952b3ecca72dda2de7d918.png)
ブロックをサポートします。 また、必要に応じて、セッションを所有するユーザーに関する情報を保存するためにUserIdフィールドを追加しました(たとえば、ユーザーに管理パネルから強制的にログアウトさせるため)。
セッション | 独自の文字列ラベル。当社が生成したものではありません。 ラテンアルファベットの文字と数字で構成される文字列にエンコードされたこの乱数は、 最大 24文字の長さに達します。 |
作成しました | セッション作成時間。 |
期限切れ | セッションが期限切れになる時間。 |
ルックデート | セッションがブロックされた瞬間。 |
Lookid | セッションロック番号。 |
見た | 現時点でロックはありますか? |
アイテムの内容 | 直列化された形式のセッションコンテンツ。 |
ユーザーID | セッションが属するユーザーのID(ゲストのIDは1です) |
上記のテーブルを作成するSQLクエリ(SQL Server):
CREATE TABLE [dbo].[Sessions] ( [SessionId] varchar(24) COLLATE Cyrillic_General_CI_AS NOT NULL, [Created] smalldatetime NOT NULL, [Expires] smalldatetime NOT NULL, [LockDate] smalldatetime NOT NULL, [LockId] int NOT NULL, [Locked] bit CONSTRAINT [DF_Sessions_Locked] DEFAULT 0 NOT NULL, [ItemContent] varbinary(max) NULL, [UserId] int NOT NULL, CONSTRAINT [PK_Sessions] PRIMARY KEY CLUSTERED ([SessionId]) ) ON [PRIMARY] GO
EntityFrameworkデータモデルの作成
SQLクエリを手動で記述しないようにして時間を節約したいので、ADO.NET EntityFrameworkを使用します。 ただし、SQLクエリを手動で作成する場合と比較して、コードのパフォーマンスが少し低下します。
これを行うには、 ADO.NET Entity Data Modelウィザードを使用して、必要なモデルを作成します。
![](https://habrastorage.org/storage2/628/20a/aa5/62820aaa58c77247cd3c8db0c0183516.png)
図 データモデルを作成するためのADO.NETエンティティデータモデルウィザードの選択
作成したエンティティにDbSessionという名前を付けました。 その後、データベースとやり取りするために必要なクラスとコンテキストを作成するために、コード生成テンプレートを使用します。 コンテキストは、データベースからのエンティティのコレクションを管理します。
![](https://habrastorage.org/storage2/210/283/14a/21028314a4122323c05a0fbea9e4b7d7.png)
図 コード生成パターンを適用するためのメニュー選択
私が選択するEntityFrameworkの4.1バージョンから利用できるDbContext APIが好きです。
![](https://habrastorage.org/storage2/473/cb3/24f/473cb324fc275dd934ae04a17ecd3261.png)
図 コード生成テンプレートとしてDbContextを選択する
これで、 CommonEntitiesと呼ばれるコンテキストとDbSessionクラスができました。 プロバイダーの実装を開始できます。
プロバイダーの実装
最初に、基本クラスSessionStateStoreProviderBaseから継承するクラスを作成する必要があります。
using QueryHunter.WebDomain.Layouts.Session; public class SessionStateProvider : SessionStateStoreProviderBase { // ... }
次に、いくつかのメソッドを実装する必要があります。これらのメソッドについては、 ドキュメントまたはコメント付きの以下のコードで詳しく説明します。
/// <summary> /// . /// </summary> public class SessionStateProvider : SessionStateStoreProviderBase { CommonEntities _dataContext; int _timeout; /// <summary> /// , , ... /// </summary> public override void Initialize(string name, NameValueCollection config) { if (config == null) throw new ArgumentNullException("config"); base.Initialize(name, config); var applicationName = System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath; var configuration = WebConfigurationManager.OpenWebConfiguration(applicationName); var configSection = (SessionStateSection)configuration.GetSection("system.web/sessionState"); _timeout = (int)configSection.Timeout.TotalMinutes; // , EntityFramework . // Dependency Injection . _dataContext = new CommonEntities(); } public override void Dispose() { _dataContext.Dispose(); } /// <summary> /// " " . /// </summary> public override SessionStateStoreData GetItem(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions) { return GetSessionItem(context, id, false, out locked, out lockAge, out lockId, out actions); } /// <summary> /// . /// </summary> public override SessionStateStoreData GetItemExclusive(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions) { return GetSessionItem(context, id, true, out locked, out lockAge, out lockId, out actions); } /// <summary> /// . /// GetItem, GetItemExclusive. /// </summary> private SessionStateStoreData GetSessionItem(HttpContext context, string id, bool exclusive, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actions) { locked = false; lockAge = new TimeSpan(); lockId = null; actions = 0; var sessionItem = _dataContext.DbSessions.Find(id); // if (sessionItem == null) return null; // , if (sessionItem.Locked) { locked = true; lockAge = DateTime.UtcNow - sessionItem.LockDate; lockId = sessionItem.LockId; return null; } // , if (DateTime.UtcNow > sessionItem.Expires) { _dataContext.Entry(sessionItem).State = EntityState.Deleted; _dataContext.SaveChanges(); return null; } // , . if (exclusive) { sessionItem.LockId += 1; sessionItem.Locked = true; sessionItem.LockDate = DateTime.UtcNow; _dataContext.SaveChanges(); } locked = exclusive; lockAge = DateTime.UtcNow - sessionItem.LockDate; lockId = sessionItem.LockId; var data = (sessionItem.ItemContent == null) ? CreateNewStoreData(context, _timeout) : Deserialize(context, sessionItem.ItemContent, _timeout); data.Items["UserId"] = sessionItem.UserId; return data; } /// <summary> /// , . /// </summary> public override void ReleaseItemExclusive(HttpContext context, string id, object lockId) { var sessionItem = _dataContext.DbSessions.Find(id); if (sessionItem.LockId != (int)lockId) return; sessionItem.Locked = false; sessionItem.Expires = DateTime.UtcNow.AddMinutes(_timeout); _dataContext.SaveChanges(); } /// <summary> /// . /// </summary> public override void SetAndReleaseItemExclusive(HttpContext context, string id, SessionStateStoreData item, object lockId, bool newItem) { var intLockId = lockId == null ? 0 : (int)lockId; var userId = (int)item.Items["UserId"]; var data = ((SessionStateItemCollection)item.Items); data.Remove("UserId"); // var itemContent = Serialize(data); // , . if (newItem) { var session = new DbSession { SessionId = id, UserId = userId, Created = DateTime.UtcNow, Expires = DateTime.UtcNow.AddMinutes(_timeout), LockDate = DateTime.UtcNow, Locked = false, ItemContent = itemContent, LockId = 0, }; _dataContext.DbSessions.Add(session); _dataContext.SaveChanges(); return; } // , , // . var state = _dataContext.DbSessions.Find(id); if (state.LockId == (int)lockId) { state.UserId = userId; state.ItemContent = itemContent; state.Expires = DateTime.UtcNow.AddMinutes(_timeout); state.Locked = false; _dataContext.SaveChanges(); } } /// <summary> /// . /// </summary> public override void RemoveItem(HttpContext context, string id, object lockId, SessionStateStoreData item) { var state = _dataContext.DbSessions.Find(id); if (state.LockId != (int)lockId) return; _dataContext.Entry(state).State = EntityState.Deleted; _dataContext.SaveChanges(); } /// <summary> /// . /// </summary> public override void ResetItemTimeout(HttpContext context, string id) { var sessionItem = _dataContext.DbSessions.Find(id); if (sessionItem == null) return; sessionItem.Expires = DateTime.UtcNow.AddMinutes(_timeout); _dataContext.SaveChanges(); } /// <summary> /// , . /// , . /// </summary> public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout) { var data = new SessionStateStoreData(new SessionStateItemCollection(), SessionStateUtility.GetSessionStaticObjects(context), timeout); data.Items["UserId"] = 1; return data; } /// <summary> /// . /// </summary> public override void CreateUninitializedItem(HttpContext context, string id, int timeout) { var session = new DbSession { SessionId = id, UserId = 1, Created = DateTime.UtcNow, Expires = DateTime.UtcNow.AddMinutes(timeout), LockDate = DateTime.UtcNow, Locked = false, ItemContent = null, LockId = 0, }; _dataContext.DbSessions.Add(session); _dataContext.SaveChanges(); } #region public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback) { return false; } public override void EndRequest(HttpContext context) { } public override void InitializeRequest(HttpContext context) { } #endregion #region private byte[] Serialize(SessionStateItemCollection items) { var ms = new MemoryStream(); var writer = new BinaryWriter(ms); if (items != null) items.Serialize(writer); writer.Close(); return ms.ToArray(); } private SessionStateStoreData Deserialize(HttpContext context, Byte[] serializedItems, int timeout) { var ms = new MemoryStream(serializedItems); var sessionItems = new SessionStateItemCollection(); if (ms.Length > 0) { var reader = new BinaryReader(ms); sessionItems = SessionStateItemCollection.Deserialize(reader); } return new SessionStateStoreData(sessionItems, SessionStateUtility.GetSessionStaticObjects(context), timeout); } #endregion }
構成設定
プロバイダーを実装したら、構成に登録する必要があります。 これを行うには、次のコードを<system.web>セクションに追加します。
![](https://habrastorage.org/storage2/0a6/216/d16/0a6216d163a384bc3586cc0a594004d8.png)
同時に、 CustomSessionStateProvider.Infrastructure.SessionProvider.SessionStateProviderは、名前空間を含むプロバイダーの完全なクラス名です。 あなたはおそらくそれを持っているでしょう。
テストプロバイダー
セッションの動作を実証するために、空のASP.NET MVC 3アプリケーションを作成し、 HomeControllerを作成し、 リストやユーザークラスオブジェクトを含むさまざまな要素を表示してセッションに書き込む一連のアクションを定義しました。
namespace CustomSessionStateProvider.Controllers { public class HomeController : Controller { // // GET: /Home/ public ActionResult Index() { return View(); } // public ActionResult SetToSession() { Session["Foo"] = new List<int>() {1, 2, 3, 4, 5}; Session["Boo"] = new SomeClass(50); return View(); } // public ActionResult ViewSession() { return View(); } } // . [Serializable] public class SomeClass { readonly int _value; public SomeClass(int value) { _value = value; } public override string ToString() { return "value = " + _value.ToString(); } } }
ビューの内容は説明しませんが、記事の最後にソースコードへのリンクがあります。 代わりに、ブラウザで取得した結果を表示し、コントローラーアクションを順番に呼び出します。
![](https://habrastorage.org/storage2/99a/529/85e/99a52985e2de0177896b934ee8b4ca44.png)
おわりに
結論として、標準から逸脱することを恐れる必要はないことを付け加えます。多くの場合、独自のソリューションにより、より生産的で柔軟なアプリケーションを作成できます。
次の記事では、独自のASP.NETメンバーシップメカニズムを作成する方法について説明します。
見てくれてありがとう、良い週末を!
PSソースコードは、 CustomSessionStateStoreProvider.zipで入手できます。
便利なリンク
Kiris MuzykovによるRedisを使用したセッションストアプロバイダーASP.NETの記述( kmuzykov )
セッションステートストアプロバイダー (msdn)の実装
セッションステートストレージプロバイダー (msdn)の例
ハリー・キンペルによるMySQLプロバイダー