This is quite a lengthy post, so bear with me. I'm not sure whether it is primarily about ASP.NET Session State behaviour, NInject, application design, or refactoring. Read on and then you can decide... :-)
Background
First, a bit of background. We are working on trying to refactor a large webshop into a more maintainable , structured design. The webshop is currently running on .NET 3.5, but the design is more of a hangover from the classic ASP days. Obviously we cannot tackle everything in one go, so many of the features / technologies / approaches have to be taken as a given. With that in mind...
The app maintains everything to do with the current session (user profile, cart, session choices, etc.) in a context object which is simply a large XML document that gets serialized to and deserialized from the Session as a string. The XML format is also important because the rendering is done via XSLT.
This has led to a number of problems :
- It's a kind of God object with far too many concerns.
- It's loosely typed and relies too much on XML manipulation / XPath.
- There is no standard way / pattern for retrieving the session xml document or for writing it back. We have a horrible mixture of methods that take the document in as a parameter, modify it and return it, methods that retrieve it themselves, modify it and save it back to session, etc, etc. This has lead to a lot of hard to trace bugs, over-use of serializing /deserializing from the Session, etc.
Our Solution
What we have done is try to introduce a strongly -typed wrapper around the xml document, which breaks it up into different concerns and to manage the lifecycle transparently to the rest of the app.
What we are aiming for is the following workflow:
Beginning of the request, we populate the session document from the xml string stored in the session.
The rest of the app interacts with it only through the strongly typed wrapper. The whole app uses the same instance and does not have to worry about when to retrieve or save the state back to session.
- At the end of the request, the underlying xml document is serialized back to the Session.
Since we are using NInject(v1) as the IOC of choice, we decided to use this to manage the lifecycle of our context object. The context object was wrapped with the OnePerRequest attribute and the dispose method was hooked up to a method that would save the xml document back to Session as a string.
It doesn't work...
We soon encountered a problem that the NInject OnePerRequest module didn't appear to have access to SessionState. The first thing we tried was a hack that we would keep the Session object in a variable to make sure we could still write to it. This appeared to work on a development machine but it became obvious it didn't when moving to out of process state.
It still doesn't work...
We tried inheriting from the OnePerRequest behaviour / module, and adding the IRequiresSessionState marker interface (OnePerRequestRequiresSessionState). However, this was not enough as the method which NInject uses to release references and clean up gets hooked up to the EndRequest method. Session is available in EndRequest but it has already been serialized to the out of process state server so changing something now is not reflected when the session string is retrieved at the beginning of the next request. We then decided to change the even t to hook up to. We ditched EndRequest and hooked up our OnePerRequestRequiresSessionState "release all" method to the PostRequestHandlerExecute event, which is BEFORE the session data gets serialized out of process.
It works... then it doesn't...
This seemed to work. On a single server and on a web farm. Then we noticed weird behaviour. There seemed to be two different versions of the context and you would randomly switch between them. Add something to the cart, it's not there. Go to browse to another product and the previous product would show up in the cart.
After some tracing, we discovered the culprit: Response.Redirect. Sprinkled throughout the site in literally hundreds of places is Response.Redirect(url);. With this version of the redirect, the execution of the page is stopped immediately. This means that PostRequestHandlerExecute is not fired and the current version of the Context object is not thrown away by NInject... and everything falls apart. New versions are not created properly, etc. EndRequest is fired which is why the normal NInject OnePerRequest module works fine with it, just not our bastardized version that tries to use session state.
Of course, there is an override to Response.Redirect where you can pass a boolean value in to tell it whether to terminate the existing page or continue to execute - Response.Redirect(url,false). Continuing obviously fires our event and everything works but... it continues to execute the rest of the page! This means executing everything that comes after the call to Redirect and we have absolutely no idea what that means (since the existing site expects it to stop).
What next?
So, any suggestions on what to do? So far we've discussed :
- Abstracting our redirect behaviour and going through a central method that controls the redirect (perhaps hacking out a way to call the PostRequestHandlerExecute even t or maybe a custom Redirect event that our NInject module can also subscribe to and clean up).
- Seeing if there is a way we can force the Session object to save in EndRequest if it hasn't been saved previously in PostRequestHandlerExecute, and do the ninject clean up in EndRequest
- Remove our dependency on Session completely and use another storage mechanism: DB, document DB, distributed HashTable, etc. Any advice? Suggestions we haven't thought of? Things you've tried that have / haven't worked?