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



    ASP.NET offers many options for working with sessions out of the box:
    • Storing session information in server memory, inside an ASP.NET process
    • Storing session information on a state server
    • Storing session information in a SQL Server database in a predefined schema

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

    Session storage will be SQL Server . We will work with the database through EntityFramework .


    Table of contents


    1. Why am I writing this article?
    2. Reasons for implementing your own provider

    3. Who and how to manage sessions 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 sites in php . At some point, I decided to learn something new and chose ASP.NET for this. Through the prism of php sessions, authorization, membership, and roles in ASP.NET caused me many questions and dissatisfaction from not understanding how this works, but what was clear was annoying because it did not work well.

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

    Reasons for implementing your own provider


    Using unsupported storage


    Microsoft products are quite expensive and it 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 might want to use Key-Value storage to improve the performance of a distributed application. Or maybe you just have licenses for products of other companies already purchased.

    Custom database table schema


    This reason is the most common.

    Standard features are good, but the more complex the application becomes, the more non-standard solutions it requires for its work.

    Example:
    On a site it is required to make it possible to close sessions (do forced logout) for specific users. The standard database schema for SQL Server does not support this functionality because sessions do not store user membership information.

    Who and how to manage sessions in ASP.NET


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

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

    Session Scheme


    The following is a brief outline of calling provider methods in order to better understand how sessions work in ASP.NET (clickable). Fig. The sequence of calling methods for working with sessions First, SessionStateModule determine the session mode for this page (ASP.NET WebForms) or controller (ASP.NET MVC). If the attribute is set for the page: <% @ Page EnableSessionState = "true | false | ReadOnly"%> (or the SessionState attribute for for ASP.NET MVC) then the session will work in Read Only mode (read-only), which is somewhat improves overall performance. Otherwise, the SessionStateModule module











    requests exclusive access and blocks session content. The lock is released only at the final stage of the query.

    Why do I need session locks?


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

    Locks occur only when accessing a session of the same user from several threads.

    The thread that accessed session resources first gets exclusive access for the duration of the request. The rest of the threads wait until the resources are free, 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).

    SessionessionUnique string label, not generated by us. This random number encoded in a string, consisting of letters of the Latin alphabet and numbers, reaches a maximum of 24 characters in length .
    CreatedSession creation time.
    ExpiresThe time when the session expires.
    LookdateThe moment when the session was blocked.
    LookidSession lock number.
    LookedIs there a lock in the moment?
    ItemcontentSession content in serialized form.
    UseridId of the user the session belongs to (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 an EntityFramework Data Model


    I want to save myself from writing SQL queries manually and save time, so I will use ADO.NET EntityFramework . However, I’ll lose a bit in code performance compared to manually creating SQL queries.

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


    Fig. Choosing the ADO.NET Entity Data Model Wizard to Create a Data Model

    I named the created DbSession entity . After that, I will use the code generation templates in order to create the necessary class and context for interacting with the database. A context manages a collection of entities from a database.


    Fig. Choosing a Menu for Using Code Generation Templates

    I like the DbContext API , which has been available since version 4.1 of EntityFramework , which I will choose.


    Fig. Choosing DbContext as the code generation template.

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

    Provider Implementation


    First, we need to create a class that will inherit from the base class SessionStateStoreProviderBase .

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


    Next, you need to implement a number of methods, which the documentation or the code below with comments will better talk about :

    /// 
    /// Наш собственный провайдер.
    /// 
    public class SessionStateProvider : SessionStateStoreProviderBase
    {
        CommonEntities _dataContext;
        int _timeout;
        /// 
        /// Инициализация провайдера, читаем конфигурацию, устаналиваем переменные...
        /// 
        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();
        }
        /// 
        /// Получаем сессию для режима "только для чтения" без необходимости блокировки.
        /// 
        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);
        }
        /// 
        /// Получаем сессию в режиме эксклюзивного доступа с необходимостью блокировки.
        /// 
        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);
        }
        /// 
        /// Обобщенный вспомогательный метод для получения доступа к сессии в базе данных.
        /// Используется как GetItem, так и GetItemExclusive.
        /// 
        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;
        }
        /// 
        /// Удаляем блокировку сессии, освобождаем ее для других потоков.
        /// 
        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();
        }
        /// 
        /// Сохраняем состояние сессии и снимаем блокировку.
        /// 
        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();
            }
        }
        /// 
        /// Удаляет запись о состоянии сессии.
        /// 
        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();
        }
        /// 
        /// Сбрасывает счетчик жизни сессии.
        /// 
        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();
        }
        /// 
        /// Создается новый объект, который будет использоваться для хранения состояния сессии в течении запроса.
        /// Мы можем установить в него некоторые предопределенные значения, которые нам понадобятся.
        /// 
        public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout)
        {
            var data = new SessionStateStoreData(new SessionStateItemCollection(),
                                                    SessionStateUtility.GetSessionStaticObjects(context),
                                                    timeout);
            data.Items["UserId"] = 1;
            return data;
        }
        /// 
        /// Создание пустой записи о новой сессии в хранилище сессий.
        /// 
        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 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 it.

    Testing provider


    In order to demonstrate the work of the sessions, I created an empty ASP.NET MVC 3 application, where I created the HomeController and defined a series of actions that display and write various elements to the session, including a list and a user class object.

    namespace CustomSessionStateProvider.Controllers
    {
        public class HomeController : Controller
        {
            //
            // GET: /Home/
            public ActionResult Index()
            {
                return View();
            }
            // Установка сессии
            public ActionResult SetToSession()
            {
                Session["Foo"] = new List() {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 is a link to the source codes. Instead, I will give the result that I got in the browser, sequentially invoking controller actions.



    Conclusion


    In conclusion, I just want to add that you don’t 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 ASP.NET membership mechanism.
    Thank you for your attention, have a nice weekend!

    PS Source codes are available at: CustomSessionStateStoreProvider.zip

    useful links


    Writing your Session Store Provider ASP.NET uses Redis from Cyril Muzykova ( kmuzykov )
    Implementation of session state store provider (msdn)
    Example session state store provider (msdn)
    provider for MySQL from Harry Kimpel

    Also popular now: