Storing Objects in Session in Asp.Net Core

Since you are here, you already know the challenges of not being able to use Session in the same way as was possible in the days of .Net Framework.

Hence I will get straight to the point of how to do this - without trying to tell you why you shouldn't store object in the session, because if you have a use case for storing session, then you need to know how to do it right - and how not to do it as well.

However my approach should be either taken as an interim while you are migrating your old apps from Asp.Net Framework to Asp.Net Core - with a long term plan to move away from session objects all together, or you should use my recommended approach only if you are providing a single server solution or a box solution.

First: How not to do this.

Microsoft's official page recommends storing objects in a session as a serialised object - they are assuming we all need multiple servers for our projects. But whether we use multiple server or single server the solution for serialisation will not work.

This is what they have recommended in this link - they keep changing links so i am pasting the code as well.

public static class SessionExtensions { public static void Set<T>(this ISession session, string key, T value) { session.SetString(key, JsonConvert.SerializeObject(value)); } public static T Get<T>(this ISession session, string key) { var value = session.GetString(key); return value == null ? default(T) : JsonConvert.DeserializeObject<T>(value); } }

What are the problems here ?
A. Serialisation isn't same as object, true it will help in distributed server scenario but it comes with a caveat that they have failed to highlight - that it will work without any unpredictable failures only when the object is meant to be read and not to be written back.

B. If you were looking for a read-write object in the session, everytime you change the object that is read from the session after deserialisation - it needs to be written back to the session again by calling serialisation - and this alone can lead to multiple complexities as you will need to either keep track of the changes - or keep writing back to session after each change in any property. In one request to the server, you will have scenarios where the object is written back multiple times till the response is sent back.

C. For a read-write object in the session, even on a single server it will fail, as the actions of the user can trigger multiple rapid requests to the server and not more than often system will find itself in a situation where the object is being serialised or deserialised by one thread and being edited and then written back by another, the result is you will end up with overwriting the object state by threads - and even locks won't help you much since the object is not a real object but a temporary object created by deserialisation.

D. There are issues with serialising complex objects - it is not just a performance hit, it may even fail in certain scenario - especially if you have deeply nested objects that sometimes refer back to itself.


I have already pointed this out in my issue request on GitHub of Asp.Net Core - the issue id is: 18159


Now coming to the main part, i.e. how to do it right:

1. First implement this as a Cache object, create one item in IMemoryCache for each unique session.
2. Keep the cache in sliding expiration mode, so that each time it is read it revives the expiry time - thereby keeping the objects in cache as long as the session is active.
3. Second point alone is not enough, you will need to implement heartbeat technique - triggering the call to session every T minus 1 min or so from the javascript. (This we anyways used to do even to keep the session alive till the user is working on the browser, so it won't be any different - i won't cover this part of the code

//Implement this SessionExtensions
    public static class SessionExtensions
    {
        private static ObjectCache _Cache;
        private const string SESSION_CACHE_KEY = "$sessioncache";

        static SessionExtensions()
        {
            _Cache = new MemoryCache("sessionCache");
        }

        private static Dictionary<string, object> GetPrivateObjectStore(string cacheKey)
        {
            CacheItem ci = _Cache.GetCacheItem(cacheKey);

            if (ci != null)
            {
                return (Dictionary<string, object>)ci.Value;
            }

            return null;
        }

        public static void InitObjectStore(this ISession session)
        {
            Dictionary<string, object> sessionObjects = new Dictionary<string, object>();
            CacheItemPolicy policy = new CacheItemPolicy();

            //set sliding timer to session time + 1, so that cache do not expire before session does
            policy.SlidingExpiration = TimeSpan.FromMinutes(CoreConfig.SessionTimeoutMin + 1);

            CacheItem ci = new CacheItem(session.Id + SESSION_CACHE_KEY);
            ci.Value = sessionObjects;

            _Cache.Set(ci, policy);
        }

        public static void RemoveObjectStore(this ISession session)
        {
            string cacheKey = session.Id + SESSION_CACHE_KEY;

            Dictionary<string, object> objectStore = GetPrivateObjectStore(cacheKey);

            objectStore.Clear();

            //also remove the collection from cache
            _Cache.Remove(cacheKey);
        }

        public static Dictionary<string, object> GetAllObjectsKeyValuePair(this ISession session)
        {
            Dictionary<string, object> objectStore = GetPrivateObjectStore(session.Id + SESSION_CACHE_KEY);

            return objectStore;
        }

        public static void RemoveObject(this ISession session, string key)
        {
            Dictionary<string, object> objectStore = GetPrivateObjectStore(session.Id + SESSION_CACHE_KEY);

            if (objectStore != null)
            {
                objectStore.Remove(key);
            }
        }

        public static void SetObject(this ISession session, string key, object value)
        {
            Dictionary<string, object> objectStore = GetPrivateObjectStore(session.Id + SESSION_CACHE_KEY);

            if (objectStore == null)
            {
                //ensure object store is available so that existing legacy code doesn't break
                InitObjectStore(session);
            }

            if (value == null)
            {
                objectStore.Remove(key);
            }
            else
            {
                objectStore[key] = value;
            }
        }

        public static object GetObject(this ISession session, string key)
        {
            Dictionary<string, object> objectStore = GetPrivateObjectStore(session.Id + SESSION_CACHE_KEY);

            object result = null;

            if(objectStore != null)
            {
                result = objectStore[key];
            }

            return result;
        }
    }


//Now to use the session, just follow this:
1. Call this as the first thing for each unique session 
            _Context.Session.InitObjectStore();

2. Call this to clear the session 
            _Context.Session.RemoveObjectStore();
            _Context.Session.Clear();

3. Call this to remove only a particular key
            _Context.Session.RemoveObject(key);

           //incase you have some parts of the session stored as a string, call this too just in case
            _Context.Session.Remove(key);

4. Call this to get the object out - and this is a real object, you can change as much you want and no need to write it back again to session
            object sessionObject = _Context.Session.GetObject(key);

5. Call this to set the object
            _Context.Session.SetObject(key, sessionObject);

6. Call this to manage heartbeat
            _Context.Session.SetObject(SESSION_HEARTBEAT_PARAM, sessionObject);


You just need to pass the key for the object,  if you see the implementation you will notice that the Session Extension will make sure it goes in the current user MemoryCacheItem


Additional recommendations

Use this to get / set User ID of the signed in user - this will increase the speed if you want to validate if the user is signed in or not
         
            //set 
            _Context.Session.SetString(USER_ID_SESSION_PARAM, beUser.ID.ToString());

            //get
            string val = _Context.Session.GetString(USER_ID_SESSION_PARAM);

Make an object called SessionManager - so that all your code related to session read / write sits in one place.

Do not keep very high value for session time out - If you are implementing heartbeat technique, even 3 mins of session time out will be enough


Please leave comments if this helped you.

Comments

  1. Hi ,
    Please share sample project.
    where i need to use _Context.Session.InitObjectStore()
    Thank you.
    srinivas
    thottempudimail@gmail.com

    ReplyDelete
  2. I like the solution and the caveats. Amazing that MS have not provided the way to co-ordinate interactions witha user.
    We can do this for the whole app and for an object interaction through Dependency Injection:
    AddSingleton (scope of the whole app)
    AddScoped (scope of object interaction)
    Seems logical to add a new one within the langauge:
    AddSessionScope (object scope of the current user session)

    ReplyDelete
  3. thanks. after many interactions on their github, they finally agreed to update their documents, but they are still very stubborn to do anything about it in the framework.

    ReplyDelete
  4. I located one reliable example of this fact through this blog website. I am mosting likely to use such information now.
    Tech World

    ReplyDelete
  5. LOL, so we have to engineer a whole contraption of on memory objects kept alive by some underground js snippet which does silent http calls to our application to keep the session alive?

    LOL again. No thank you. I will use session.

    ReplyDelete
    Replies
    1. That is what this blog is about i.e. To be able to use sessions. Whether you want to use session as memory object or direct (depends on your use case) there is always a question of what should be the value of session timeout. a high value timeout leads to performance issues as the session is sitting on the server even though user has left the browser and that is what lead to js based snippet - so you can keep session timeout as low as 3 mins and still make it available to user as long as they are working, by keeping it alive using js, and this way as soon as they leave browser the session at the server end goes away in 3 mins. It is in-fact ingenious and widely practiced to give a performance boost and not sure what is to laugh about it. Anyways that is not even main part of this blog, the core part is to address how to use session in .net core to store objects.

      Delete

Post a Comment

Popular posts from this blog

Real Issues in Cryptocurrencies

How to give correct file ownerships and permissions to secure your app server