If you have an ASP.NET application, have you noticed that out-of-the-box your .aspx requests are serialized? That is to say, if you request page A.aspx and navigate to B.aspx before A.aspx finishes loading, the B.aspx won't begin until A.aspx completes.
Why is this, you may ask?
The answer is session locking. The ASP.NET framework protects you from simultaneous access to session state by two HTTP request handing threads by blocking subsequent requests until the first one relinquishes a lock. This is the default behavior, but it can be overriden (sort of) by using the enableSessionState page attribute:
-
enableSessionState="true" is the default behavior, and acquires a writer lock on session data. No other readers or writers can gain access while this lock is held.
-
enableSessionState="ReadOnly" acquires a reader lock on session data. Multiple readers are allowed simultaneously, but no writer can gain access if any reader holds a lock. Unfortunately, any changes you make to session are local to the request and not visible to other requests that look at the session object. No error is thrown if you try to modify a "read only" session, so this behavior is not obvious at first blush.
-
enableSessionState="false" doesn't acquire any lock. The HttpContext.Session property ends up being null, and your page will have no access to session data.
This is a sad state of affairs. If you want two user requests to execute in parallel, where both need read/write access to the session, you're out of luck. I wish the ASP.NET framework provided a way for me to manage synchronization to the session data myself as I do in the Java servlet/JSP world, perhaps by using something like a Session.SyncRoot property, but sadly there is absolutely no way to do this. I've looked at the framework code, and in ASP.NET 2.0 there are tons of assumptions all over the place about exclusive write access to the session.
What are the alternative solutions?
I can think of two solutions off the top of my head:
-
Create a static singleton (accessible by all sessions) that maps session IDs to string-to-object dictionaries, i.e. IDictionary<string, IDictionary<string, object>>. The static singleton needs to carefully synchronize access to the data structure.
-
Create a custom session data store that avoids locking.
Option 1 feels like a hack, as it bypasses all of the built-in session infrastructure and has an obvious drawback of not being automatically cleaned up when sessions terminate. Calling a cleanup method from a Session_End event is possible, but certainly not elegant. Here's an implementation:
/// <summary>
/// Stores session-specific values in application state so the values are available
/// immediately, even on pages that do not have access to the session object (e.g. enableSessionState=false).
/// <para/>
/// If you use this class, make sure <c>RemoveAllItems</c> is called when sessions end
/// to ensure memory is reclaimed.
/// <para/>
/// All methods on this class are thread-safe.
/// </summary>
public static class ApplicationSessionStateStore
{
private static bool _enabled;
/// <summary>
/// Enables this class for use. This is a safety check. By invoking this method,
/// the caller commits to calling <c>RemoveAllItems</c> when sessions terminate.
/// </summary>
public static bool IsEnabled
{
get { return _enabled; }
set { _enabled = value; }
}
public static void AssertEnabled()
{
if (!_enabled)
{
throw new InvalidOperationException("Use of ApplicationSessionStateStore is not enabled. See 'IsEnabled' property documentation for proper usage.");
}
}
public static T GetItem<T>(string sessionId, string key) where T : class
{
return GetItem<T>(sessionId, key, false);
}
public static T GetItem<T>(string key) where T : class
{
return GetItem<T>(HttpContext.Current.Session.SessionID, key, false);
}
public static T GetAndRemoveItem<T>(string sessionId, string key) where T : class
{
return GetItem<T>(sessionId, key, true);
}
public static T GetAndRemoveItem<T>(string key) where T : class
{
return GetItem<T>(HttpContext.Current.Session.SessionID, key, true);
}
public static void SetItem(string key, object value)
{
SetItem(HttpContext.Current.Session.SessionID, key, value);
}
public static void SetItem(string sessionId, string key, object value)
{
AssertEnabled();
HttpContext context = HttpContext.Current;
HttpApplicationState appState = context.Application;
IDictionary<string, object> sessionDic;
appState.Lock();
try
{
sessionDic = (IDictionary<string, object>)appState.Get(sessionId);
if (sessionDic == null)
{
sessionDic = new Dictionary<string, object>();
appState.Set(sessionId, sessionDic);
}
}
finally
{
appState.UnLock();
}
lock (sessionDic)
{
sessionDic[key] = value;
}
}
public static void RemoveAllItems()
{
RemoveAllItems(HttpContext.Current.Session.SessionID);
}
public static void RemoveAllItems(string sessionId)
{
HttpContext.Current.Application.Remove(sessionId);
}
private static T GetItem<T>(string sessionId, string key, bool removeItem) where T : class
{
AssertEnabled();
HttpContext context = HttpContext.Current;
HttpApplicationState appState = context.Application;
IDictionary<string, object> sessionDic = (IDictionary<string, object>)appState.Get(sessionId);
if (sessionDic == null)
{
return null;
}
else
{
object value;
lock (sessionDic)
{
if (sessionDic.TryGetValue(key, out value))
{
if (removeItem)
{
sessionDic.Remove(key);
}
}
}
return value as T;
}
}
}
This implementation requires some special handing in global.asax:
protected void Application_Start(Object sender, EventArgs e)
{
ApplicationSessionStateStore.IsEnabled = true;
}
protected void Session_End(Object sender, EventArgs e)
{
ApplicationSessionStateStore.RemoveAllItems();
}
Option 2 is actually surprisingly complex. When you implement your own session store, it's not like you're subclassing HttpSession. Oh no, that would be too simple. You need to subclass a "store" that provides "store data" that HttpSession encapsulates. So the custom store must avoid locking requests in the places the native store engages in its reader/writer lock magic, and it must also return a "store data" object whose instance methods are thread-safe. To further complicate matters, the native implementation of the in-process session store is internal and cannot be directly subclassed by your code, so you have to use reflection to instantiate it and the decorator pattern to subclass it. Here are all of the steps:
-
Define a MultiThreadedSesionStateStore that subclasses SessionStateStoreProviderBase.
-
In the constructor, instantiate the native in-proc store System.Web.SessionState.InProcSessionStateStore using Activator.CreateInstance.
-
In all the methods that must be defined, invoke the appropriate method on the native store using reflection to find the appropriate MethodInfo and calling Invoke. The key insight is to call the native store's GetItem method when the multi-threaded store's GetItemExclusive method is called. In essense, you're faking the native store to treat all session acquisitions as read-only acquisitions so the exclusive writer lock is never held. Since the session store data object returned is not a copy (the framework makes a copy of read-only session data elsewhere) it's possible to modify the data that should be treated as read-only.
-
In all methods that return SessionStateStoreData, wrap the store data using a custom subclass of SessionStateStoreData. In the subclass, wrap the inner SessionStateStoreData.Items collection in a custom ISessionStateItemCollection.
-
Implement the custom ISessionStateItemCollection class so that all methods lock the wrapped ISessionStateItemCollection's SyncRoot object for reads and writes. Importantly, when defining the Keys property return a custom NameObjectCollectionBase.KeysCollection instance that is a snapshot of the keys at that point in time. You'll need to use reflection to create a new instance of this class, as its construtor is internal. This involves defining a subclass NameValueCollectionBase.
-
In all methods that accept a SessionStateStoreData object, unwrap the decorator before passing the object to the wrapped session store implementation.
If you don't go through with steps 4 - 6, the session store data object will not be thread safe, and concurrent user requests may end up corrupting this object. With all steps implemented, reading and writing data to the session, e.g. Session[key] = value, becomes thread-safe. One important word of caution, though: If you're placing objects in session state, you must perform locking yourself or ensure the instance methods and properties of the shared object are also thread-safe.
If the 6 steps of this approach seem a bit daunting (I deduced all of this by using the .NET reflector and verifying through inspection that all ways in which the session data could be accessed were done in a thread-safe way), it's also possible to write a session store implementation completely from scratch, but that means you'd have to code up the session timeout logic too.
Why do you need to support concurrent user requests anyway?
There are several reasons why you won't want to run user requests serially. If you want to allow users to quickly navigate through your app, even when database queries may slow things down a bit, for example, allowing concurrent requests works wonders. It's also fantastic when multiple dynamic requests may occur in parallel on a page, such as the generation of dynamic charts.
The approach works best in code that periodically checks to ensrue a request is still active, since if a user is bouncing around the app you don't want to spend too much time executing queries and rendering HTML that will never see the light of day. It's as simple as:
if (!context.Response.IsClientConnected)
{
context.Response.End();
}