Storing Objects in In-Memory 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.
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.
This article's approach should be 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. In case any of you need the code for this article, send an email to kalpesh@sarasennovations.com
I have now also implemented a framework for session less system, and migrated my systems to it, therefore the code takes a very different approach of storing the objects compared to the method i have shown in this article, I can conduct a short lecture for your team with appropriate working demos and hand in the source code post the lecture. Write to me if interested
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();
_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);
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.
Hi ,
ReplyDeletePlease share sample project.
where i need to use _Context.Session.InitObjectStore()
Thank you.
srinivas
thottempudimail@gmail.com
I like the solution and the caveats. Amazing that MS have not provided the way to co-ordinate interactions witha user.
ReplyDeleteWe 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)
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.
ReplyDeleteI located one reliable example of this fact through this blog website. I am mosting likely to use such information now.
ReplyDeleteTech World
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?
ReplyDeleteLOL again. No thank you. I will use session.
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.
DeleteHI there,
ReplyDeleteI'm currently working on a solution to temporarily store the choices of a visitor in ~11 lists under the context of an MVC Core web app exposed to relatively high concurrency (200 concurrent users) in a single server setup. I was thinking about using a single object to store/update/read all the choices made by the user but reading your post (which is absolutely spot on) I feel like given my scenario storing the ~11 values I need as session variables would be enough, since it'd avoid the caveats of object management in sessions. Let me know what you think if you happen to read this, your feedback would be greatly appreciated by this amateur. Regards
Hi, for a high concurrency app i wouldn't advice in-memory sessions (object or values) it increases the spec of the hardware and still provide unpredictable results. I have myself used the above solution only as an interim and now have moved to my own custom architected distributed cache stored in a db. It keeps server memory free, also your system will become compatible with server farms and therefore provide more resource to handle concurrent user requests per server. However the above is based on certain assumptions from my side, i can guide better if i know your case in full.
DeleteHi Kalpesh,
ReplyDeleteThanks explaining in detail how to overcome this very challenge that I am facing. Also thanks for all the helpful tips that you provided in your email to me.
Agreed that avoiding session variables in for the best, however as the legacy application I support has a reason to use session variables and the object that gets stored cannot be serialised, I was unsure how to migrate this to .NET CORE, hopefully this being an interim solution, I should be able to find a better way to deal with this and avoid session variables if possible, or keep it to the bare minimum.
I am Glad it helped, it is a necessary code while migrating !! i have now moved on and the architecture is devoid of sessions, this has significantly reduced the load on servers memory and we are able to scale easily, the architecture is now based on our own implementation of sql based sessions
Delete