However, even if you use
[SessionState(System.Web.SessionState.SessionStateBehavior.Disabled)]on your controller, it
turns out database calls are still made!
Deep inside the built in SessionStateModule class, it determines if it should load session state. Even if it does not load session state, it makes a call to ResetItemTimeout(). This then turns around and updates the session timeout value in the database.
Example of the problem
You create an 'Image' controller and that controller has actions to return the actual images to the client. Inside that action, you of course manage client caching, create thumbnails, pull images from databases, etc. The url to access this might be /Image/SomeFile.jpg or /Image/StandardThumbNail/SomeFile.jpg. (Note many people do things like this, as well as have controllers that return optimized/compressed scripts, stylesheets, etc)
Now, suppose you have a basic web page that has 20 images on it. Imagine you have optimized this page such that it only requires one database call. You turn on SQL Profiler, load up this webpage and are shocked to find 21 database calls! Why? One for the page and one for each image as it 'resets' the session timeout on each image request.
This is probably not considered a bug, as the asp.net team does not want a session to timeout even though the user is accessing controllers with the session disabled. But, if you think about most session scenarios - what we store in them is temporary. They expire in whatever time we setup (20 minutes, 1 hour, etc). So, if a user does NOT visit a page that requires the session inside our session time - perhaps it is ok the session has timed out? In MVC3 pages, you may only end up using session state as the 'tempdataprovider' for your modelstate in validation. In that case, you only use the session for the NEXT request - so it's OK if a timeout occurred.
Fixing the problem
How do we fix this? I have come up with several ways to solve this.
1) Change the behavior of the sql session provider's ResetItemTimeout to not reset the timeout if the session is disabled. Each developer would have to decide if this works for them, based on how the session was used in the site. I think for many cases, this would be valid.
2) Do #1 and add a new attribute of [EnableUpdateSessionTimeout]. This would go on controllers where you want the session timeout updated even though session state is disabled.
3) Do the inverse of #2. Add a new attribute of [DisableUpdateSessionTimeout]. This would go on any controller that you did not want to update session timeout and change the behavior of #1 - to always update it unless this is set.
I think #2 would be best if designing from the ground up - but it changes the default behavior of what asp.net always has done. For that reason, I think #3 would be best. This would require you to consider which controllers could be modified without breaking any existing code. That is an easy decision you say.....however, guess what? Did you know that EVERY request that comes into your application - even for static image files, scripts, etc that are delivered by IIS make this same call?? Crazy, huh? IIS indicates no session is necessary, just as we do in our controller - but the session provider still makes the call to the ResetItemTimeout. This translates into this call being made for EVERY FILE you serve no matter what! If you average 500 visitors a day and each visitor goes to 3 pages, where each page has 20 assets (images, scripts, etc) - that alone would translate to 30,000 calls to this stored procedure on any given day. Worse yet, you probably didn't need it to be called. Ultimately, if you want to get rid of all these unnecessary calls - we have to implement it like #2 so the default behavior is to eliminate the call (when session is disabled) unless [EnableUpdateSessionTimeout] is used. I will discuss both implementations below, lets start with #3.
Well, this actually could be a really easy fix - but many of the things we need to extend or access are marked as internal! Yuck. If the built in SqlSessionStateStore was not marked internal - we could just extend and change this. But, as it is we have to copy and paste the code for this into our own project! Why Microsoft, why?
So, I took the source code from the default providers and put that in a project. Next, I found that the property I needed to read in HttpContext was also internal (why oh why!?), so I had to use reflection to access it (cached the reflection information for speed). With this in place, I can successfully avoid the database call in ResetItemTimeout. Ugh...Copy and paste 2000 lines of code what could have been a simple override of an already virtual method....
Next, we need the [DisableUpdateSessionTimeout] attribute. The way I envisioned this to work was to put something in HttpContext.Items indicating you wanted to skip the session timeout database call. This should be a simple solution, but it is not. The issue is that the controller factory determines the session state and it does so before it even instantiates the controller! So, no attributes, controller methods or anything have a chance to run any code. This means you have to do this in a new ControllerFactory. This new controller factory will be pretty simple and inherit from the DefaultControllerFactory, so should not be many issues. The attribute is trivial:
And the new controller factory is very easy (since I inherited from the default one)
Next update ResetItemTimeout to use this new option:
Finally, update your web.config to use our new sql provider and register the new controller factory in your global.asax:
Now we are finished. Put [SessionState(System.Web.SessionState.SessionStateBehavior.Disabled)] on your controller, simply add the new [DisableUpdateSessionTimeout] to stop the database call from being made. In my example above, you go from having 21 database calls down to only 1. However, as discussed above, requests for static files still make this call. So, to change the above implementation to be like #2, is easy. Just reverse our logic and create a new attribute. Here are the changes:
Normally, he solution to this problem would not have been much code. However, it actually turns out to be much more code because Microsoft decided to make some methods and classes internal. The solution attached is 2300+ lines of code. If the classes were not marked internal, the solution would be less than 100 lines! (Most of it was from the default provider implementation source). Download source here Download source here.