Introduction
This article suggests a "brute force" approach to the problem of implementing custom session state management mechanism for ASP.NET 1.0/1.1. This issue was somewhat addressed by the ASP.NET team in .NET 2.0 Beta, but the developers who use older framework still have to re-invent their own wheel every time when they are not happy with any of the standard state management options. The article describes an implementation of a custom session state module that replaces the standard System.Web.SessionState.SessionStateModule
and uses custom storage media for session data. The implementation works closely with System.Web
internals and uses reflection for obtaining non-public structures and generating IL code on the fly.
Background
OutOfProc or SQL Server-based state management engine can be a good choice for many scenarios, but both of them have some drawbacks, which are out of the scope of this article. What if custom session state management mechanism is the only option for the customer?
ASP.NET gurus (see extremely useful "Understanding session state modes + FAQ" by Patrick Y. Ng) suggest only two solutions. The first approach is to replace session state module, and this task, according to Patrick, "can be quite tedious". I tend to agree: re-writing hundreds of lines of well-tested code that can be completely thrown out in the next .NET release, does not seem to be a very rewarding job. The second solution is to implement an additional HTTP module that co-exists with the standard session state module, subscribes to application events, and tries to handle (or better say, hijack) session state structures that have been already processed by the standard module. The major caveats of this approach are:
- sharing
HttpContext.Session
with another running session state module is not a straightforward process;
- session data won't be available immediately after
AcquireRequestState
, which is (maybe not a major, but still) a violation of ASP.NET request processing model;
- we actually need only some part of the standard framework functionality, the rest of it spends machine resources in vain.
An ideal hypothetical solution makes the maximum possible use of the existing SessionStateModule
code and totally eliminates InProc/OutOfProc/SQL Server state manager involvement. Let's see how close we can make it.
SessionStateModule - how it works
Standard SessionStateModule
is a class that implements the IHttpModule
interface and does the following:
- subscribes to specific ASP.NET application events;
- using HTTP request information, creates or retrieves session identifier;
- moves session state data from the storage to current HTTP context and back;
- performs some additional state maintenance tasks (state object creation, removal etc.).
All storage-related tasks are performed by an object that implements the IStateClientManager
interface. System.Web
comes with three types of state manager objects that handle three standard scenarios: InProc
, OutOfProc
, SqlServer
. The following diagram gives an idea about state management for the InProc case.
Adding a custom state management engine - a plan
From the above diagram, the most natural solution is obvious - replace InProcStateClientManager
with your own class and enjoy the freedom of custom IStateClientManager
implementation and the power of existing session state module functionality. But there are some problems here. ASP.NET creators do not want us to rely on their implementation details and they do not allow us to set the value of the _mgr
member, which points to the state client manager object. Moreover, they do not expose IStateClientManager
itself, and they hide all SessionStateModule
members, leaving us only Init()
and Dispose()
(this method tandem is supposed to be called by the ASP.NET framework).
In good old times of native code compilers, assembly languages, powerful debuggers and disassemblers, the only way to inject a new piece of functionality into existing binary was, hmm... well, a bit tricky: patch executable code and data in the EXE/DLL file or do some surgery on a running process. In the managed world we have a better way to deal with binaries: reflection. Let's summarize what we have to do:
- Create a wrapper class for the standard
SessionStateModule
that allows access to its hidden members.
- Create an implementation of the
IStateClientManager
, keeping in mind that Visual Studio cannot access interface declaration in System.Web
(it's declared as internal
).
- Build a custom
SessionStateModule
that, on initialization, creates an instance of this state client manager object, an instance of standard session state module, and tells the standard module that it should use our non-standard manager object. A custom module should also subscribe to application events and delegate them to the standard module.
The following sections describe these major steps in detail.
SessionStateModule wrapper class
Creating a wrapper class is easy. Just obtain field or member info once, and call GetValue
/SetValue
/Invoke
when needed.
public abstract class SystemWebClassWrapper
{
protected static Assembly _asmWeb = null;
protected object _o = null;
public SystemWebClassWrapper()
{
if ( _asmWeb == null )
{
_asmWeb = Assembly.GetAssembly(
typeof(System.Web.SessionState.SessionStateModule));
}
}
}
public class SessionStateModuleWrapper: SystemWebClassWrapper
{
private static Type _objType = null;
private static FieldInfo _mgrInfo = null;
private static MethodInfo _onBeginRequestInfo = null;
private static MethodInfo _beginAcquireStateInfo = null;
private static MethodInfo _endAcquireStateInfo = null;
private static MethodInfo _onReleaseStateInfo = null;
private static MethodInfo _onEndRequestInfo = null;
public SessionStateModuleWrapper()
{
if (_objType == null )
{
_objType = _asmWeb.GetType(
"System.Web.SessionState.SessionStateModule", true);
_mgrInfo = _objType.GetField(
"_mgr", BindingFlags.Instance | BindingFlags.NonPublic );
_onBeginRequestInfo = _objType.GetMethod(
"OnBeginRequest", BindingFlags.Instance | BindingFlags.NonPublic );
}
}
public System.Web.SessionState.SessionStateModule InnerObject
{
get { return (System.Web.SessionState.SessionStateModule)_o; }
set { _o = value; }
}
public object _mgr
{
get { return _mgrInfo.GetValue( _o ); }
set { _mgrInfo.SetValue( _o, value ); }
}
public void OnBeginRequest(object source, EventArgs eventArgs)
{
object [] methodParams = new object [2] { source, eventArgs };
_onBeginRequestInfo.Invoke( _o, methodParams);
}
}
You will see how we can use this wrapper later. Before that, let's figure out how to create an implementation of a hidden interface.
State client manager class - generate it on the fly
Let's create an implementation of IStateClientManager
, which is declared in System.Web
as:
internal interface IStateClientManager
{
IAsyncResult BeginGet(string id, AsyncCallback cb, object state);
IAsyncResult BeginGetExclusive(string id,
AsyncCallback cb, object state);
void ConfigInit(SessionStateSectionHandler.Config config,
SessionOnEndTarget onEndTarget);
void Dispose();
SessionStateItem EndGet(IAsyncResult ar);
SessionStateItem EndGetExclusive(IAsyncResult ar);
void ReleaseExclusive(string id, int lockCookie);
void Remove(string id, int lockCookie);
void ResetTimeout(string id);
void Set(string id, SessionStateItem item, bool inStorage);
void SetStateModule(SessionStateModule module);
}
Implementation class defined in our custom module assembly may look as follows. Please note that some StateClientManagerImp
signatures slightly differ from those that IStateClientManager
has. This is because correspondent data types are not exposed by System.Web
, so we have to pass them as object
.
public class StateClientManagerImp
{
public IAsyncResult BeginGetImp(string id, AsyncCallback cb, object state)
{
}
public IAsyncResult BeginGetExclusiveImp(string id,
AsyncCallback cb, object state)
{
}
public void ConfigInitImp(object config, object onEndTarget)
{
}
public void DisposeImp()
{
}
public object EndGetImp(IAsyncResult ar)
{
}
public object EndGetExclusiveImp(IAsyncResult ar)
{
}
public void ReleaseExclusiveImp(string id, int lockCookie)
{
}
public void RemoveImp(string id, int lockCookie)
{
}
public void ResetTimeoutImp(string id)
{
}
public void SetImp(string id, object item, bool inStorage)
{
}
public void SetStateModuleImp(SessionStateModule module)
{
}
}
Consider a utility class called StateClientManagerFactory
, that does the following:
- obtains
IStateClientManager
info from System.Web
;
- in a separate dynamic assembly, it defines a new class type
StateHijack.StateClientManager
that inherits from IStateClientManager
and from StateClientManagerImp
described above (which actually implements wannabe-interface methods with "Imp" suffix);
- walks through all interface methods and generates
StateHijack.StateClientManager
code: every StateHijack.StateClientManager
method must call the correspondent "Imp" method of the parent class StateClientManagerImp
.
public class StateClientManagerFactory
{
private static readonly OpCode[] _ldargCodes = new OpCode [4]
{
OpCodes.Ldarg_0,
OpCodes.Ldarg_1,
OpCodes.Ldarg_2,
OpCodes.Ldarg_3
};
private TypeBuilder _typeBuilder = null;
private Type _impType = null;
private Type _ifaceType = null;
public Type Create( string name,
ModuleBuilder modBld,
Type impType,
Type ifaceType )
{
_impType = impType;
_ifaceType = ifaceType;
_typeBuilder = modBld.DefineType( name, TypeAttributes.Public);
_typeBuilder.AddInterfaceImplementation(ifaceType);
_typeBuilder.SetParent( impType );
MethodInfo[] ifaceMethods = ifaceType.GetMethods(
BindingFlags.Instance | BindingFlags.Public);
foreach( MethodInfo ifaceMethod in ifaceMethods)
{
ImplementIfaceMethod( ifaceMethod );
}
return _typeBuilder.CreateType();
}
private void ImplementIfaceMethod( MethodInfo ifaceMethodInfo )
{
ParameterInfo [] paramInfos = ifaceMethodInfo.GetParameters();
Type [] paramTypes = new Type[ paramInfos.Length ];
int paramIndex = 0;
foreach( ParameterInfo paramInfo in paramInfos)
{
paramTypes[paramIndex] = paramInfo.ParameterType;
paramIndex++;
}
MethodBuilder methodBld = _typeBuilder.DefineMethod(
ifaceMethodInfo.Name,
MethodAttributes.Public | MethodAttributes.Virtual,
ifaceMethodInfo.ReturnType,
paramTypes );
MethodInfo impMethodInfo = _impType.GetMethod(
ifaceMethodInfo.Name + "Imp",
BindingFlags.Instance | BindingFlags.Public);
ILGenerator methodBldIL = methodBld.GetILGenerator();
methodBldIL.Emit(OpCodes.Ldarg_0);
for ( int index = 0; index < paramTypes.Length; index++)
{
methodBldIL.Emit( _ldargCodes[index + 1] );
}
methodBldIL.EmitCall(OpCodes.Call, impMethodInfo, null);
methodBldIL.Emit(OpCodes.Ret);
_typeBuilder.DefineMethodOverride(methodBld, ifaceMethodInfo );
}
}
As the result, we have the following implementation for, say, BeginGet()
method:
.method public virtual instance class [mscorlib]System.IAsyncResult BeginGet(
string A_1,
class [mscorlib]System.AsyncCallback A_2,
object A_3)
cil managed
{
.override [System.Web]System.Web.SessionState.IStateClientManager::BeginGet
.maxstack 4
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: ldarg.2
IL_0003: ldarg.3
IL_0004: call instance class [mscorlib]System.IAsyncResult
[StateMirror.SessionStateModule]StateMirror.StateClientManagerImp::BeginGetImp(
string,
class [mscorlib]System.AsyncCallback,
object)
IL_0009: ret
}
Custom SessionStateModule
We are almost done. Now we have to implement a custom SessionStateModule
that:
- creates an instance of the standard session state module class (using the wrapper class described above);
- creates an instance of the custom state client manager (that recently generated
StateClientManager
);
- mimics standard
SessionStateModule.Init()
behavior;
- makes the standard module object use the recently generated
StateClientManager
;
- subscribes to application events and calls the standard module's handlers.
With these tasks in mind, the code below is more or less self-explanatory. The "original" version of the initialization methods can be obtained with the help of Lutz Roeder's .NET Reflector tool. What we do here can be described as "a careful aggregation of the standard session state module with partial re-implementation".
public class SessionStateModule: IHttpModule
{
private static Assembly _webAsm = null;
private static Type _mgrType = null;
private object _mgr = null;
private SessionStateModuleWrapper _origModuleWrapper = null;
private Type CreateStateClientManagerType()
{
AppDomain curDomain = Thread.GetDomain();
AssemblyName asmName = new AssemblyName();
asmName.Name = "StateHijack.StateClientManager";
AssemblyBuilder asmBuilder = curDomain.DefineDynamicAssembly(
asmName,
AssemblyBuilderAccess.RunAndSave);
ModuleBuilder modBuilder = asmBuilder.DefineDynamicModule(
"StateClientManager",
"StateHijack.StateClientManager.dll");
StateClientManagerFactory mgrFactory =
new StateClientManagerFactory();
Type retVal = mgrFactory.Create(
"StateClientManager",
modBuilder,
typeof(StateClientManagerImp),
_webAsm.GetType("System.Web.SessionState" +
".IStateClientManager", true) );
return retVal;
}
private void InitializeUnderlyingObjects()
{
if ( _webAsm == null )
{
_webAsm = Assembly.GetAssembly(
typeof(System.Web.SessionState.SessionStateModule));
_mgrType = CreateStateClientManagerType();
}
if (_origModuleWrapper == null)
{
_origModuleWrapper = new SessionStateModuleWrapper();
_origModuleWrapper.InnerObject =
new System.Web.SessionState.SessionStateModule();
_mgr = Activator.CreateInstance(_mgrType);
}
}
public void Init(HttpApplication app)
{
lock (this)
{
InitializeUnderlyingObjects();
ConfigWrapper config = new ConfigWrapper();
config.InnerObject = HttpContext.GetAppConfig("system.web/sessionState");
if (config.InnerObject == null)
{
config.Ctor();
}
InitModuleFromConfig(app, config.InnerObject, true);
if (!_origModuleWrapper.CheckTrustLevel(config.InnerObject))
{
_origModuleWrapper.s_trustLevelInsufficient = true;
}
_origModuleWrapper.s_config = config.InnerObject;
}
if (_origModuleWrapper._mgr == null)
{
InitModuleFromConfig(app, _origModuleWrapper.s_config, false);
}
if (_origModuleWrapper.s_trustLevelInsufficient)
{
throw new HttpException("Session state need higher trust");
}
}
public void InitModuleFromConfig(
HttpApplication app,
object configObject,
bool configInit)
{
ConfigWrapper config = new ConfigWrapper();
config.InnerObject = configObject;
if (config._mode == SessionStateMode.Off)
{
return;
}
if (config._isCookieless)
{
app.BeginRequest += new EventHandler( this.OnBeginRequest );
HttpContextWrapper curContext = new HttpContextWrapper();
curContext.InnerObject = curContext.Current;
_origModuleWrapper.s_appPath =
curContext.InnerObject.Request.ApplicationPath;
if (_origModuleWrapper.s_appPath[
_origModuleWrapper.s_appPath.Length - 1] != '/')
{
_origModuleWrapper.s_appPath += "/";
}
_origModuleWrapper.s_iSessionId =
_origModuleWrapper.s_appPath.Length;
_origModuleWrapper.s_iRestOfPath =
_origModuleWrapper.s_iSessionId + 0x1a;
}
app.AddOnAcquireRequestStateAsync(
new BeginEventHandler(this.BeginAcquireState),
new EndEventHandler(this.EndAcquireState));
app.ReleaseRequestState +=(new EventHandler(this.OnReleaseState));
app.EndRequest +=(new EventHandler(this.OnEndRequest));
_origModuleWrapper._mgr = _mgr;
if (configInit)
{
MethodInfo setStateModuleInfo = _mgrType.GetMethod(
"SetStateModule",
BindingFlags.Instance | BindingFlags.Public );
object[] invokeParams = new object[1] { _origModuleWrapper.InnerObject };
setStateModuleInfo.Invoke( _mgr, invokeParams );
}
}
public void Dispose()
{
lock(this)
{
_mgr = null;
_origModuleWrapper = null;
}
_origModuleWrapper.InnerObject.Dispose();
}
private void OnBeginRequest(object source, EventArgs eventArgs)
{
_origModuleWrapper.OnBeginRequest( source, eventArgs );
}
private IAsyncResult BeginAcquireState(
object source, EventArgs e, AsyncCallback cb, object extraData)
{
return _origModuleWrapper.BeginAcquireState( source, e, cb, extraData );
}
private void EndAcquireState(IAsyncResult ar)
{
_origModuleWrapper.EndAcquireState( ar );
}
private void OnReleaseState(object source, EventArgs eventArgs)
{
_origModuleWrapper.OnReleaseState( source, eventArgs );
}
private void OnEndRequest(object source, EventArgs eventArgs)
{
_origModuleWrapper.OnEndRequest( source, eventArgs );
}
}
Putting it all together
The class diagram now looks as follows:
Just insert your module reference to the web.config file, and make sure it does not interfere with the original httpModules
setting in the machine.config:
<httpModules>
<add name="Session" type="StateHijack.SessionStateModule, StateHijack"/>
</httpModules>
Copy your custom module to the bin folder of the application, and enjoy the benefits of your custom session state management.
Source code and sample project
Just a few notes.
- State client manager implemented in the sample source code uses a temporary directory to store session data. One session state object - one *.ses file. This is just a sample implementation that helps to prove the concept. It's definitely not the right way to store session data in the real world deployment.
- There is a small number of additional wrapper classes in the project, they allow us to get access to other objects that
System.Web
does not fully expose. These objects include: SessionStateSectionHandler+Config
, HttpAsyncResult
, SessionStateItem
, and HttpContext
.
- The sample ASP.NET application is extremely simple: it increments the session variable called "
RefreshNum
" on every postback.
Looking further
All of the above was tested on .NET 1.1. The discussed approach should work for 2.0 as well, but I would definitely go with the session state store provider model suggested by the ASP.NET team.
A quick reflector-powered reconnaissance of .NET 1.0 shows that StateSessionModule
has slightly different Init()
and InitModuleFromConfig()
implementations, so in order to make things work on 1.0, you will probably have to mimic that 1.0 implementation. It doesn't seem to be a lot of work though. After all, this is the destiny of those who walk the thorn path of using undocumented APIs.
Building a reliable and scalable session state management solution
The approach discussed in this article has been used in product development. An open source version of the product is available here under the Files section. A commercial product description can be found here.