views:

115

answers:

3

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 :

  1. It's a kind of God object with far too many concerns.
  2. It's loosely typed and relies too much on XML manipulation / XPath.
  3. 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 :

  1. 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).
  2. 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
  3. 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?
A: 

Have you tried getting this to work without using NInject? You can start by loading it once per request (and perhaps cache it in the Request.Items) and persist it on each change instead of at the end of each request. This will probably not be fast because it is a big document, but perhaps performance is good enough.

Steven
Persisting it on each change is not really an option. This is what we are trying to get away from - having the caller explicitly save the changes back (write the value to the session). I've come to the opinion what it really wrong is the way we do the redirects. By using the overloaded version that does not terminate the page execution, everything works fine. We need to handle the fact the page continues to execute and renders output to the client, but that's ok... if a little repetitive. I'll probably put the redirect / check for allowing to render into a base class derived from page.
Michael Smith
Ending the request with a `ThreadAbortException` is just default behavior and pretty logical, because it saves you from making very sure your page does not execute any unexpected actions. However, I can understand this makes it very hard for you.
Steven
+1  A: 

I think you're on the right track. Here's some thoughts I had:

  • in addition to the strongly typed wrapper you have, I'd suggest a facade for accessing the context object that returns your wrapper, something like an IContextProvider. that way you can introduce it piece-meal, and then when it's fully integrated, you can refactor the provider without breaking the things that use it. I can't tell, but you might have already done this. it'll also be easier to change your persistence mechanism if you choose to. if you can do this, I would suggest once you get all the dependencies isolated from the context object, change it to not persist as XML. the SessionState will store a binary object much faster, and you can always serialize to XML if you need to do transforms.

  • I don't think that Ninject is the correct mechanism for what you're trying to do. it's difficult to signal end of the request in Ninject, since garbage collection can't be depended on. have you considered using an IHttpModule instead? you can use the AcquireRequestState and ReleaseRequestState or EndRequest to handle getting/setting the context in Session. only allow the app to get to the context object through the facade.

  • if you're on a webfarm, you're probably using a database for your Session storage anyway, so putting your context into a DB won't be much different.

dave thieben
Thanks Dave. You're absolutely right about the problems using NInject and the solution. :-) We realised that we were trying to use the OnePerRequest behaviour to manage the state and we didn't need to. We separated out the retrieval of the state (xml document) from session from access to the state for the rest of the app. Our context wrapper continues to provide access to the state for the rest of the application but the interaction with session state (get and put) is handled by an http module. Works exactly as we want...
Michael Smith
A: 

Firstly, while it's good to demonstrate you've put in the work, (and I and others may not have replied if it wasn't clear how much you're interested in a resolution)... that's a massive wall of text! Here's a +1 on your way to investing in a bonus for a complete response that talks about the Ninject ASP.NET extensions and how they apply to each individual element of your issue. Having said that, hopefully someone will come along with a real resolution for you.

Even though it's [very] 2.0 specific, Nate's Cache and Collect Post is required reading. While it seems you're pretty au fait with the tradeoffs involved and have debugged deep in, the article is well worth a few reads.

I'd also consider moving to V2 of Ninject - a lot of this stuff has been revised significantly. It's not magically going to work, but represents a mature rewrite based on a lot of learning from V1. Have you read the (V1 or) V2 unit tests for Ninject? They'll show you the low level tools at your disposal in order to realise your goals.

Bottom line for me is that you need to work out a strategy for your state management independent of DI, and then by all means use the container/DI system as a part of the implementation.

Ruben Bartelink
Maybe also http://feedproxy.google.com/~r/Devlicious/~3/5r-9KJKuQMs/must-i-release-everything-when-using-windsor.aspx
Ruben Bartelink