Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Custom session state management in ASP.NET 1.1

4.48/5 (10 votes)
10 Apr 2006MIT7 min read 1   1.6K  
This article discusses a custom session state management mechanism that replaces ASP.NET 1.1 SessionStateModule.

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.

System.Web internals - before surgery

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:

  1. Create a wrapper class for the standard SessionStateModule that allows access to its hidden members.
  2. Create an implementation of the IStateClientManager, keeping in mind that Visual Studio cannot access interface declaration in System.Web (it's declared as internal).
  3. 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.

C#
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 );
            
            //
            // Same for other event handlers...
            //
        }
    }

    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);
    }

    //
    // Same for other event handlers...
    //
}

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:

C#
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.

C#
public class StateClientManagerImp
{
    public IAsyncResult BeginGetImp(string id, AsyncCallback cb, object state)
    {        // Implementation...
    }

    public IAsyncResult BeginGetExclusiveImp(string id, 
                        AsyncCallback cb, object state)
    {        // Implementation...
    }

    //void ConfigInit(SessionStateSectionHandler.Config config, 
    //                         SessionOnEndTarget onEndTarget);
    public void ConfigInitImp(object config, object onEndTarget)
    {        // Implementation...
    }

    public void DisposeImp()
    {        // Implementation...
    }

    //SessionStateItem EndGet(IAsyncResult ar);
    public object EndGetImp(IAsyncResult ar)
    {        // Implementation...
    }

    //SessionStateItem EndGetExclusive(IAsyncResult ar);
    public object EndGetExclusiveImp(IAsyncResult ar)
    {        // Implementation...
    }

    public void ReleaseExclusiveImp(string id, int lockCookie)
    {        // Implementation...
    }

    public void RemoveImp(string id, int lockCookie)
    {        // Implementation...
    }

    public void ResetTimeoutImp(string id)
    {        // Implementation...
    }

    //void Set(string id, SessionStateItem item, bool inStorage);
    public void SetImp(string id, object item, bool inStorage)
    {        // Implementation...
    }

    public void SetStateModuleImp(SessionStateModule module)
    {        // Implementation...
    }
}

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.
C#
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;

        // Define a new type in given module
        _typeBuilder = modBld.DefineType( name, TypeAttributes.Public);

        // Inherit given iface and implementation
        _typeBuilder.AddInterfaceImplementation(ifaceType);
        _typeBuilder.SetParent( impType );

        // Get iface methods
        MethodInfo[] ifaceMethods = ifaceType.GetMethods(
            BindingFlags.Instance | BindingFlags.Public);

        // Walk through iface methods and generate
        // correspondent implementation methods
        foreach( MethodInfo ifaceMethod in ifaceMethods)
        {
            ImplementIfaceMethod( ifaceMethod );
        }

        // Create the type
        return _typeBuilder.CreateType();
    }

    private void ImplementIfaceMethod( MethodInfo ifaceMethodInfo )
    {
        // This method assumes that:
        // - the number of parameters in iface and imp methods always match;
        // - implementation method name = iface method name + "Imp" suffix
        // - the number of parameters cannot exceed 3 (see _ldargCodes)

        // Convert ParameterInfo array to Type array
        ParameterInfo [] paramInfos = ifaceMethodInfo.GetParameters();
        Type [] paramTypes = new Type[ paramInfos.Length ];
        int paramIndex = 0;
        foreach( ParameterInfo paramInfo in paramInfos)
        {
            paramTypes[paramIndex] = paramInfo.ParameterType;
            paramIndex++;
        }

        // Define a new iface implementation method
        MethodBuilder methodBld = _typeBuilder.DefineMethod(
            ifaceMethodInfo.Name,
            MethodAttributes.Public | MethodAttributes.Virtual,
            ifaceMethodInfo.ReturnType,
            paramTypes );

        // Get "...Imp" method info
        MethodInfo impMethodInfo = _impType.GetMethod(
            ifaceMethodInfo.Name + "Imp",
            BindingFlags.Instance | BindingFlags.Public);

        // Generate code
        ILGenerator methodBldIL = methodBld.GetILGenerator();
        methodBldIL.Emit(OpCodes.Ldarg_0);

        // Walk through parameter list and generate corresponding ldarg
        for ( int index = 0; index < paramTypes.Length; index++)
        {
            methodBldIL.Emit( _ldargCodes[index + 1] );
        }

        // Generate call and graceful return
        methodBldIL.EmitCall(OpCodes.Call, impMethodInfo, null);
        methodBldIL.Emit(OpCodes.Ret);

        // Mark this method as iface implementation
        _typeBuilder.DefineMethodOverride(methodBld, ifaceMethodInfo );
    }
}

As the result, we have the following implementation for, say, BeginGet() method:

MSIL
.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
  // Code size       10 (0xa)
  .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
} // end of method StateClientManager::BeginGet

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".

C#
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) );

        // You may want to save this generated assembly for
        // testing (ildasm-ing and reflector-ing) purposes.
        // asmBuilder.Save("StateHijack.StateClientManager.dll");

        return retVal;
    }

    private void InitializeUnderlyingObjects()
    {
        if ( _webAsm == null )
        {
            _webAsm = Assembly.GetAssembly(
                typeof(System.Web.SessionState.SessionStateModule));

            // Generate our custom StateClientManager class
            _mgrType = CreateStateClientManagerType();
        }

        if (_origModuleWrapper == null)
        {
            // Create an instance of the original SessionStateModule
            _origModuleWrapper = new SessionStateModuleWrapper();
            _origModuleWrapper.InnerObject =
                new System.Web.SessionState.SessionStateModule();

            // Create an instance of the newly generated StateClientManager class
            _mgr = Activator.CreateInstance(_mgrType);
        }
    }

    public void Init(HttpApplication app)
    {
        lock (this)
        {
            InitializeUnderlyingObjects();

            // Mimic original SessionStateModule.Init  behavior
            ConfigWrapper config = new ConfigWrapper();
            config.InnerObject = HttpContext.GetAppConfig("system.web/sessionState");
            if (config.InnerObject == null)
            {
                config.Ctor();
            }

            InitModuleFromConfig(app, config.InnerObject, true);
            
            // For OutOfProc and SQLServer, this call checks
            // HttpRuntime.HasAspNetHostingPermission(
            //       AspNetHostingPermissionLevel.Medium);
            if (!_origModuleWrapper.CheckTrustLevel(config.InnerObject))
            {
                _origModuleWrapper.s_trustLevelInsufficient = true;
            }

            _origModuleWrapper.s_config = config.InnerObject;
        } // lock

        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;

        // Mimic original SessionStateModule.InitModuleFromConfig behavior
        if (config._mode == SessionStateMode.Off)
        {
            return;
        }

        if (config._isCookieless)
        {
            // Cookieless functionality requires adding this handler
            app.BeginRequest += new EventHandler( this.OnBeginRequest );

            // Add session id to the path
            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;
        }

        // Add event handlers
        app.AddOnAcquireRequestStateAsync(
            new BeginEventHandler(this.BeginAcquireState),
            new EndEventHandler(this.EndAcquireState));
        app.ReleaseRequestState +=(new EventHandler(this.OnReleaseState));
        app.EndRequest +=(new EventHandler(this.OnEndRequest));

        // Instead of analyzing config and choosing
        // between InProc, OutOfProc, SQL etc,
        // "patch" original SessionStateModule object, make _mgr point to
        // our instance of StateClientManager. We could even provide wrappers
        // for standard state client managers (InProc, OutOfProc, SqlServer)
        // and create correspondent objects here
        // (see that switch() statement in the
        // original SessionStateModule.InitModuleFromConfig), but:
        // - that would require some hacking
        //   on sessionState section handler, since
        //   "custom" sessionState managers are not supported in 1.1;
        // - we do not have a goal to come up with a "better ASP.NET";
        // - ASP.NET team has already done some part of the job in .NET 2.0

        _origModuleWrapper._mgr = _mgr;

        if (configInit)
        {
            // For the sake of consistency, call IStateClientManager.SetStateModule,
            // but it does not do anything anyways, see comments within
            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:

System.Web internals - after the surgery

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:

XML
<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.

  1. 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.
  2. 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.
  3. 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.

License

This article, along with any associated source code and files, is licensed under The MIT License