📜 ⬆️ ⬇️

Sessions in ASP.NET or how to create your own provider



ASP.NET offers many options for working with out-of-box sessions:

But no matter how many options out of the box, they can not fully respond to the tasks that confront the developer. In this article, we will look at how to implement our own ASP.NET (MVC) session state session storage provider.

SQL Server will act as session storage. We will work with the database through EntityFramework .
')

Table of contents


  1. Why am I writing this article?
  2. The reasons for the implementation of its own provider

  3. Who and how sessions are managed in ASP.NET

  4. Session Provider Implementation

  5. Testing provider
  6. Conclusion


Why am I writing this article?


For a long time I was developing php sites. At some point, I decided to learn something new and chose ASP.NET for this. Through the prism of the php session, authorization, membership and roles in ASP.NET caused me a lot of questions and dissatisfaction from misunderstanding how this works, and what was understandable was annoying because it didn't work well enough.

Now, several years later, I decided to create a series of articles not just to explain how it works, but to show that in ASP.NET there is nothing that could not be changed and done in its own way.

The reasons for the implementation of its own provider


Using unsupported storage


Microsoft products are quite expensive and this is a fact, and free versions have a number of limitations. Thus, you may want to use another database in a free edition, such as MySQL or PostgreSQL .

On the other hand, you may want to use Key-Value storage to improve the performance of a distributed application. Or maybe you simply have already purchased licenses for products of other companies.

Custom database table schema


This reason is the most frequent.

Standard features are good, but the more complex an application becomes, the more innovative solutions it requires for its work.

Example:
The site requires that you make it possible to close the session (to do forced exit - logout) for specific users. The standard database schema for SQL Server does not support this functionality, since sessions do not store user membership information.

Who and how sessions are managed in ASP.NET


SessionStateModule is responsible for processing the state (sessions), it is the default handler. If you wish, you can implement your own http-module that is responsible for handling sessions.

The SessionStateModule object interacts with the session provider, invoking certain provider methods during its work. Which session provider to use the module determines based on the configuration of the web application. Any session provider must inherit from the SessionStateStoreProviderBase class, which defines the necessary methods for SessionStateModule .

Scheme of work sessions


Below is a brief scheme of calling the provider methods in order to better understand how sessions work in ASP.NET (clickable).



Fig. Sequence of calling methods for working with sessions

First, the SessionStateModule determines the session mode for this page (ASP.NET WebForms) or the controller (ASP.NET MVC).

If the page attribute is set:
<% @ Page EnableSessionState = "true | false | ReadOnly"%>
(or the SessionState attribute for for ASP.NET MVC)

That work with the session will occur in Read Only mode (read only), which slightly improves the overall performance. Otherwise, the SessionStateModule module requests exclusive access and blocks the contents of the session. Blocking is removed only in the final stage of the request.

Why do we need session locks?


ASP.NET applications are multi-threaded applications, and ajax technologies are very popular. It may be a situation that several threads will access the session of the same user at once, in order to avoid conflicts, overwrite stored values ​​or retrieve outdated values, locks are used.

Locks only occur when a session is accessed by the same user from multiple threads.

The thread that accessed the session resources first gets exclusive access for the duration of the request. The remaining threads are waiting until the resources are released, or until a short timeout occurs.

We can implement a provider without blocking support, if we have reasons to believe that there will be no conflicts between threads, or they will not lead to significant consequences.

Session Provider Implementation


Creating a table to store session data


I will use the following table structure to store session state data:



It supports locks. I also added the UserId field, for my needs, to store information about the user who owns the session (for example, to force the user to logout from the admin panel).

SessionIdA unique string label not generated by us. This random number, encoded into a string composed of Latin letters and numbers, reaches a maximum of 24 characters in length.
CreatedTime to create a session.
ExpiresThe time when the session expires.
LookdateThe moment when the session was blocked.
LookidSession lock number.
LookedIs there currently a lock.
ItemcontentThe contents of the session in serialized form.
UseridThe user Id to which the session belongs (my guest has id = 1)


SQL query to create the above table (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 


Creating EntityFramework Data Model


I want to save myself from writing SQL queries manually and save time, so I will use ADO.NET EntityFramework . At the same time I will lose a little bit in the performance of the code, compared with the manual creation of SQL queries.

To do this, I will use the ADO.NET Entity Data Model wizard to create the model I need.


Fig. Select the ADO.NET Entity Data Model wizard to create a data model

I called the created entity DbSession . After that, I will use the code generation templates to create the necessary class and context for interaction with the database. The context manages the collection of entities from the database.


Fig. Select a menu to apply code generation patterns

I like the DbContext API , which is available from version 4.1 of EntityFramework , and that’s what I’ll choose.


Fig. Selecting DbContext as a code generation template

Done, now I have a context named CommonEntities and a DbSession class. You can begin to implement the provider.

Provider implementation


First we need to create a class that will be inherited from the base class SessionStateStoreProviderBase .

 using QueryHunter.WebDomain.Layouts.Session; public class SessionStateProvider : SessionStateStoreProviderBase { // ... } 


Next, you need to implement a number of methods, which are best described by the documentation or the code below with comments:

 /// <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 } 


Configuration setting


After we have implemented the provider, it is necessary to register it in the configuration. To do this, add the code below to the <system.web> section:



At the same time, CustomSessionStateProvider.Infrastructure.SessionProvider.SessionStateProvider is the full class name of our provider, including the namespace. You will most likely have yours.

Testing provider


In order to demonstrate the work of the sessions, I created an empty ASP.NET MVC 3 application, where I created a HomeController controller and defined a number of actions that map and record various elements in the session, including a list and a custom class object.

 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(); } } } 


I will not give the contents of the views ( View ), at the end of the article there is a link to the source codes. Instead, I will give the result that I received in the browser, sequentially invoking controller actions.



Conclusion


In conclusion, I just want to add that you do not need to be afraid to deviate from the standards, often your own solutions allow you to create more productive and flexible applications.

In the next article I will look at how to create your own membership mechanism in ASP.NET.
Thank you for your attention, enjoy your weekend!

PS Source codes are available at the link: CustomSessionStateStoreProvider.zip

useful links


Writing your Session Store Provider ASP.NET using Redis by Kirill Muzykova ( kmuzykov )
Implementing session state storage provider (msdn)
Sample session state storage provider (msdn)
MySQL Provider for Harry Kimpel

Source: https://habr.com/ru/post/141056/


All Articles