Monday 8 June 2015

Session, Compression and Generics

Yeah, I am totally NOT sold for usage of Session in ASP.NET MVC applications but then there are certain cases where it makes sense (e.g. shopping cart). Basically it is a matter of making an informed decision after considering multiple points - user experience, application behavior across tabs, Storage pattern for session etc. Once you start using session, you have to also ensure that you are managing it well.

Eager developers can make your life tough on production environments. Mark it. We must assess the potential size of objects we will end up with in session once we start using it and plan to handle that capacity. One thing is for sure that each web server (in case of in-proc session which i am sure that you are not using) or session state provider (Redis, SQL Server, ASP.NET State Server, soon to be retired AppFabric cache) has limited memory and network bandwidth to dedicate and will need to scale up. The thing about Out-Of-Proc session state providers is that there will be a network call to persist or retrieve information from session and the size of data that you transfer per call will become one important factor in production scenarios.

There are some clever ways by which we can think of reducing the size of information over the wire. One such way is to compress the serialized object if it goes beyond a logical limit (which you would have found through Endurance tests). Internet has used this approach before too.

Our team wrote a wrapper over session to control what kind of session we use (HttpSession or something else). So when we started facing performance issues, we wrote a simple code to serialize the object using NewtoSoft.JSON and then compress the serialized string. Point to note is that we did not need to persist any private variables of the objects across requests and therefore JSON serialization was not an issue for us. If you are using private state of the object then there are some other ways to achieve compression too (e.g. binary serialize, make it base64 encoded and compress the string).

In my case, I created a custom class named CustomSessionObject which is a generic class and can store following pieces of information - if the object is compressed, compressed bytes when object is compressed, original object just in case we decide to not compress it. Here is what session Add/Get looked like:

public void AddItemToSession(string key, T value)
{
            CustomSessionObject sessionObj = new CustomSessionObject();
            sessionObj.Bytes = Compress(Newtonsoft.Json.JsonConvert.SerializeObject(value));
            Session[key] = sessionObj;
}

public T RetrieveItemFromSession(string key)
{
            var sessionObj = Session[key] as CustomSessionObject;
            var decompressedJson = Decompress(sessionObj.Bytes);
            return Newtonsoft.Json.JsonConvert.DeserializeObject(decompressedJson);
}

You can use any of the compression tools. I used .NET's in-build compression capabilities (using GZipStream).

Pro/Con: You should evaluate a critical breaking point where cost of sending data over the wire is more than the total cost of compressing data and sending compressed data over the wire. Once you have figured that out, you can extend the session management to do selective compression operations.

Salient point: Notice that i used CustomSessionObject instead of CustomSessionObject. That is because the user of session methods can store object of type T but may want to retrieve it as a type that is compatible in nature - when they do with the above code, they get unwanted results. Classic example: Store Collection and retrieve ICollection. While that would work in normal scenarios as the two objects can be casted into one another, it would not work in above scenario because CustomSessionObject> is not allowed to be casted into CustomSessionObject> or vice versa. Generics, I tell you!! 

Here is a simple test:

static T GetItem(object o)
{
            return (T)o;
}

Collection collInt = new Collection();
ICollection icollInt = GetItem>(collInt); // works
Console.WriteLine(icollInt == null);

var collInt2 = new CustomSessionObject>();
CustomSessionObject> icollInt2 = GetItem>>(collInt2); // exception here
Console.WriteLine(icollInt2 == null);