Work Around Your ASP.Net Session Serialization Issues
OK. We're going to assume everyone here knows about ASP.Net Session state. We also will assume that you know there's 3 modes, InProc (default), State Server, and SQL. The latter two will be the topic of discussion here. The biggest difference between the InProc and out of process state management systems is that the objects placed in the Session are required to be serializable. This is not something too big a problem to deal with.
The situation that tends to happen however is that an application is built that can leverage the Session system and life is good, for a time that is. Then the time comes that you need to scale out your systems so you switch your state management to and out of process option and BAM! You find that your code has allowed non-serializable objects to "leak" into Session state. Now you have to track down all the code and objects that needs to change, deploy it, test it, then get back to your original job of actually scaling your system out.
So you see that these objects that have been playing along nicely in the InProc Session cannot be normally serialized directly. So the .Net runtime has a concept of a Serialization Surrogate which you would use in this situation. Basically its a class that can be registered with a serializer that can be used to serialize another object of a given type (even if that type is inherently not serializable). While it would be nice to be able to leverage this approach it turns out that there's no way to get your hands onto the serializer that is going to get used by the Session Manager (note: If anyone knows something I don't on this topic, CALL ME). Too bad. Hopefully they will add some new events to allow this in the future.
So how can we work around this?
Well the solution is to bascially take control of the serialization yourself before the state persistence operations actually run. Effectively you are to subvert the serializer to do your will ;)
To make this concrete, let's assume you have some class that's not serializable like this:
public class Class1
{
public String id;
public Class1(String id)
{
this.id = id;
}
[OnDeserializedAttribute()]
internal void RunThisMethod(StreamingContext context)
{
// Proves that serialization occurs. Put a breakpoint here :)
}
}
Now we can define a Surrogate for this (or any type for that matter). What's important here is that the Surrogate can be defined in any assembly. This means that this is a totally orthagonal operation to your existing code. Therefore the chance of regression is minor. You can do any operation you can write in code in this Surrogate that you need to in order to perform the serialization. So we have a Surrogate now that looks like this:
public class Class1Serializer : ISerializationSurrogate
{
#region ISerializationSurrogate Members
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
Class1 toBeSerialized = obj as Class1;
if (toBeSerialized == null) return;
info.AddValue("id", toBeSerialized.id);
}
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context,
ISurrogateSelector selector)
{
Class1 toBeDeserialized = obj as Class1;
if (toBeDeserialized == null) return null;
//Do your custom work here.
String id = info.GetString("id");
toBeDeserialized.id = id;
return toBeDeserialized;
}
#endregion
}
Now onto the "magic code" :)
You see, what we end up doing is creating a SurrogateSelector type and add our new custom Surrogate registered to handle serialization for whatever types we need like this:
SurrogateSelector ss = new SurrogateSelector();
ss.AddSurrogate(typeof(Class1), new StreamingContext(StreamingContextStates.All), new Class1Serializer());
Now when we go to perform serialization we can add in this Surrogate and the Surrogate type will be used instead to perform the work.
BinaryFormatter formatter = new BinaryFormatter();
formatter.SurrogateSelector = ss;
formatter.Serialize(buffer, item);
buffer.Position = 0;
Byte[] data = buffer.GetBuffer();
See how we now have our unserialized class boiled down to an array of Bytes? Well this data can be schleped off to and back from cold storage anytime you want now. What better way to deal with it than putting it into special named location in Session? Because the data is now in a serializable format, the Session Manager will have no trouble "serializing our serialized object"! :)
So to go a little but further you can easily accomplish this all together with some ASP.Net HttpApplication events to deal with this for you. Of course we want to deal with the deserialization as soon as safe and serialization as late as possible to make our state as consistent as possible. This can be handled like this:
void Application_PostAcquireRequestState(object sender, EventArgs e)
{
Byte[] data = Session["Context"] as Byte[];
if (data == null) return;
SurrogateSelector ss = new SurrogateSelector();
ss.AddSurrogate(typeof(Class1), new StreamingContext(StreamingContextStates.All), new Class1Serializer());
BinaryFormatter formatter = new BinaryFormatter();
formatter.SurrogateSelector = ss;
using (MemoryStream buffer = new MemoryStream(data))
{
buffer.Position = 0;
Class1 deserialized = (Class1)formatter.Deserialize(buffer);
this.Session["item"] = deserialized;
}
}
void Application_PostRequestHandlerExecute(object sender, EventArgs e)
{
Class1 item = this.Session["item"] as Class1;
if (item == null) return;
using (MemoryStream buffer = new MemoryStream())
{
SurrogateSelector ss = new SurrogateSelector();
ss.AddSurrogate(typeof (Class1), new StreamingContext(StreamingContextStates.All), new Class1Serializer());
BinaryFormatter formatter = new BinaryFormatter();
formatter.SurrogateSelector = ss;
formatter.Serialize(buffer, item);
buffer.Position = 0;
Byte[] data = buffer.GetBuffer();
Session["Context"] = data;
}
Session.Remove("item");
}
See how easy that was?
Now to be fair, I am assuming the Session State keys that are going to be used are well known in this example here. Of course that's not going to be the same situation in a generic bit of code for you. Those assumptions could be deadly. You should realize though that with a little bit of configuration (basically the target type to surrogate type mapping), a simple routine to iterate the Session collection, a generic custom type to hold the binary data and the type that you serialized, and a custom HttpModule, one could wrap this idea up into a very easy to implement and use package for your own applications. All you need to do is simply check the type definition of every class in Session state and check to see if the Type.IsSerializable property. If false, then add in the required surrogate to your own serializer. Package all this stuff up into a serialzable IDictionary making sure the map the keys between Session and the dictionary. Finally, remove the items from session and place your manually dealt with objects into a safe place. On the way back in just perform the reverse operation, take each item from the serialized dictionary, deserialize to the indicated type, and finally add it back into the Session. VoilĂ !
Jimmy Zimms is thinking that serialization options ASP.Net and WCF should be more open for extension. |