See Joel Program

November 10, 2008

Maintaining ASP.NET Session State in an Ajax Application

Filed under: ASP.NET AJAX — Joel Rumerman @ 9:33 pm

Recently, I’ve been working with applications that require very few postbacks. While this has been great from a usability perspective, after all the application is very ajaxy and responsive, it’s caused some unexpected problems. One problem that it caused is that ASP.NET Sessions were expiring even though the user was actively using the application.

The Sessions were expiring because the user wasn’t causing a postback (full or partial) and thus executing the ASP.NET page life cycle, which takes care of maintaining Session for us by implementing the IRequireSessionState interface.

One way I could have solved this problem is to rehydrate Session whenever an Ajax request was received (such as by marking the web method as requiring Session), but this is a bad programming practice for a few reasons. First, having Session attached to a request and response isn’t very RESTful and breaks at least a couple of design rules. Second, it’s a relatively expensive operation to pull Session from the session store, deserialize it into the Session object, use it, maybe modify it, and then serialize it back out to the Session store when the request is complete. As Session gets bigger, this operation gets more expensive and I really want the Ajax features of the application to fly. Finally, I might not own the REST endpoints that I’m using for my application’s functionality. In that case, I’m talking to a third party service (i.e. Amazon’s S3 service), and not communicating at all with my application, but I still need to keep Session alive. The user doesn’t know or care that I’m talking to Amazon. They just think they’re using my application.

What I really wanted to do was sometimes revalidate Session while the user interacted with the Ajaxy parts of the application without the user paying any perceivable performance penalty and not breaking too many rules of application design.

To do this, I created a mechanism to execute a fire and forget request to a custom Http Handler that takes care of revalidating Session for us.

[Download the code here]

First, let’s cover the the custom Http Handler:

using System.Web;
using System.Web.SessionState;

public class OutOfBandSessionValidator : IHttpHandler, IRequiresSessionState
{
    public bool IsReusable
    {
        get { return false; }
    }

    public void ProcessRequest(HttpContext context)
    { }
}

As you can see, out OutOfBandSessionValidator does almost nothing. The only thing really worth noting is that it implements the IRequiresSessionState interface that tells ASP.NET that when a request comes in, this handler requires that Session be retrieved and deserialized, and when the request completes, it serializes the Session object and stores it back in the Session store. This is the mechanism that will allow us to keep Session alive.

We wire up out custom handler just like any other Http Handler in the web.config like so:

<httpHandlers>
...
    <add verb="GET,HEAD" path="OutOfBandSessionValidator.ashx" type="OutOfBandSessionValidator"/>
...
</httpHandlers>

(This is IIS 6 style.)
We can now access our custom handler like so: http://localhost:1544/OutOfBandSessionValidator.ashx

Now that we’ve got our handler wired up, we need to create a mechanism to call it. For performance purposes, we want to call the handler in a way that doesn’t affect our application’s performance (perceived or real) and we don’t want to call it too often. Rather, we want to call it once a minute or something like that. We’re going to wrap this functionality in a custom component. In the custom component we’re going to reply on a Client EventPool event to talk to our component rather than call into it directly. This way, we don’t really need to have a link to the component.
First, for posterity, let’s redefine our Client EventPool. (For more information on the Client EventPool, see this post.)

_EventPool = function() {
    _EventPool.initializeBase(this);
};
_EventPool.prototype = {
    addEvent: function(name, handler) {
        this.get_events().addHandler(name, handler);
    },
    removeEvent: function(name, handler) {
        this.get_events().removeHandler(name, handler);
    },
    raiseEvent: function(name, source, args) {
        var handler = this.get_events().getHandler(name);
        if (handler) {
            handler(source, args);
        }
    }
};
_EventPool.registerClass("_EventPool", Sys.Component);
var $evp = $create(_EventPool, { "id": "EventPool" }, null, null, null);

Now we can define our OutOfBandSessionValidator component and a custom event args class:

RevalidateSessionArgs = function(ticks) {
    RevalidateSessionArgs.initializeBase(this);
    this._ticks = ticks;
};
RevalidateSessionArgs.prototype = {
    get_ticks: function() { return this._ticks; }
};
RevalidateSessionArgs.registerClass("RevalidateSessionArgs", Sys.EventArgs);

_OutOfBandSessionValidator = function() {
    _OutOfBandSessionValidator.initializeBase(this);
    this._revalidateSessionDelegate = null;
    this._lastTimeSessionWasValidated = null;
    this._delay = 60000; // default to 60 seconds.
};

_OutOfBandSessionValidator.prototype = {
    set_delay: function(value) { this._delay = value; },
    get_delay: function() { return this._delay; },

    initialize: function() {
        _OutOfBandSessionValidator.callBaseMethod(this, 'initialize');
        this._revalidateSessionDelegate = Function.createDelegate(this, this._revalidateSession);
        $evp.addEvent("RevalidateSession", this._revalidateSessionDelegate);
    },

    dispose: function() {
        if (this._revalidateSessionDelegate !== null) {
            $evp.removeEvent("RevalidateSession", this._revalidateSessionDelegate);
            this._revalidateSessionDelegate = null;
        }
        _OutOfBandSessionValidator.callBaseMethod(this, 'dispose');
    },

    _revalidateSession: function(sender, args) {
        Sys.Debug.trace("_revalidatingSession");
        var revalidate = false;
        if (this._lastTimeSessionWasValidated === null) {
            revalidate = true;
        }
        else if ((args.get_ticks() - this._lastTimeSessionWasValidated) > this._delay) {
            revalidate = true;
        }

        if (revalidate) {
            Sys.Debug.trace("Executing call to OutOfBandSessionValidator.ashx");
            this._lastTimeSessionWasValidated = args.get_ticks();
            var wRequest = new Sys.Net.WebRequest();
            wRequest.set_httpVerb("GET");
            wRequest.set_url("OutOfBandSessionValidator.ashx?d=" + args.get_ticks());

            var executor = new Sys.Net.XMLHttpExecutor();
            wRequest.set_executor(executor);
            executor.executeRequest();
        }
    }
};
_OutOfBandSessionValidator.registerClass("_OutOfBandSessionValidator", Sys.Component);
$create(_OutOfBandSessionValidator, { "id": "OutOfBandSessionValidator" }, null, null, null);

The _OutOfBandSessionValidator component is quite simple. In it's initialize method we wire up a delegate to the "RevalidateSession" event
initialize: function() {
    _OutOfBandSessionValidator.callBaseMethod(this, 'initialize');
    this._revalidateSessionDelegate = Function.createDelegate(this, this._revalidateSession);
    $evp.addEvent("RevalidateSession", this._revalidateSessionDelegate);
},


In the dispose method we clear out this event handler

dispose: function() {
    if (this._revalidateSessionDelegate !== null) {
        $evp.removeEvent("RevalidateSession", this._revalidateSessionDelegate);
        this._revalidateSessionDelegate = null;
    }
    _OutOfBandSessionValidator.callBaseMethod(this, 'dispose');
},


The other method, _revalidateSession, which is executed whenever the “RevalidateSession” event is raised, is responsible for calling our OutOfBandSessionValidator handler not more than whatever delay we set.

_revalidateSession: function(sender, args) {
    Sys.Debug.trace("_revalidatingSession");
    var revalidate = false;
    if (this._lastTimeSessionWasValidated === null) {
        revalidate = true;
    }
    else if ((args.get_ticks() - this._lastTimeSessionWasValidated) > this._delay) {
        revalidate = true;
    }

    if (revalidate) {
        Sys.Debug.trace("Executing call to OutOfBandSessionValidator.ashx");
        this._lastTimeSessionWasValidated = args.get_ticks();
        var wRequest = new Sys.Net.WebRequest();
        wRequest.set_httpVerb("GET");
        wRequest.set_url("OutOfBandSessionValidator.ashx?d=" + args.get_ticks()); 

        var executor = new Sys.Net.XMLHttpExecutor();
        wRequest.set_executor(executor);
        executor.executeRequest();
    }
}

In the _revaidateSession method, the args parameter is of type RevalidateSessionArgs. This custom arguments class contains one property, get_ticks(), which simply holds a ticks value. This value’s intended purpose is to hold the current ticks of the date/time when of when the event args were created. It then performs some checking to make sure that we haven’t initiated a request to OutOfBandSessionValidator.ashx with the delay and then sends a non-cacheable GET request to the OutOfBandSessionValidator.ashx handler.

To use the OutOfBandSessionValidator we raise the RevalidateSession event like so:

$evp.raiseEvent("RevalidateSession", this, new RevalidateSessionArgs(new Date().getTime()));

Now that we have a mechanism for calling the OutOfBandSessionValidator.ashx by raising an event, we can write a small test page that raises the event every 5 seconds. To really illustrate that our mechanism works, we’re going to set the ASP.NET Session timeout to 1 minute in the web.config file.

        <sessionState  mode="InProc" cookieless="true" timeout="1" />
    </system.web>

Here’s our test page’s markup. Note that we have three buttons on the page. The first one causes postback and refreshes the label with the current Session’s information. The other two start and stop the raising of the event.

<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="SM1" runat="server">
        <Scripts>
            <asp:ScriptReference Path="~/JS.js" />
        </Scripts>
    </asp:ScriptManager>
    <div>
        <asp:Label ID="lblNewSession" runat="server" />
        <asp:Button ID="btnCausePostback" runat="server" Text="Cause Postback" />
    </div>
    <input type="button" id="btnStopInterval" disabled='disabled' value="Stop Interval"
        onclick="window.clearInterval(window._intervalId);
                 $get('btnStartInterval').disabled = false;
                 $get('btnStopInterval').disabled=true; " />
    <input type="button" id="btnStartInterval" value="Start Interval"
        onclick="window._intervalId=window.setInterval(window.fn, 5000);
        $get('btnStartInterval').disabled = true;
        $get('btnStopInterval').disabled = false;" />
    </form>
</body>

Here’s the test page’s code behind.

public partial class _Default : Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        Session["lastAcessTime"] = DateTime.Now;
        if (this.Session.IsNewSession)
        {
            this.lblNewSession.Text = "New Session: " + this.Session.SessionID;
        }
        else
        {
            this.lblNewSession.Text = "Existing Session: " + this.Session.SessionID;
        }
    }
}

Here’s how the page looks when we initially load it.

FirstLoad
After waiting 30 seconds or so, and clicking the “Cause Postback” button, we see that we’re working with an existing Session.

ExistingSession

After waiting more than one minute and clicking the “Cause Postback” button, we see that the Session is again new.

NewSession
Now we turn on the interval that is going to raise our RevalidateSession event by clicking the “Start” button.

Interval Started
Note: for this test, I reduced the delay between requests from 60 seconds to 20 seconds.
Looking at the debug window of Visual Studio, we can see that our event is being raised every 5 seconds, but besides the first request, not until the 4th request (20 second mark) do we see the debug message that we’ve initiated a request to the OutOfBandSessionValidator.ashx.

RevalidatingSession

Again, after another 4 requests we see the request to the OutOfBandSessionValidator.ashx

MultipleCallsRevalidationSession
Now when I click the “Cause Postback” I get the existing Session message

ValidSession
When I click the “Stop” button, wait about a minute or so and then click the “Cause Postback” button, you’ll see that the Session has started over.

NewSessionAgain
So there you have it. My take on how to keep an ASP.NET Session alive in an Ajax application.

My final thought is this. It doesn’t make much sense to raise the event every 10 seconds or something like in order to keep the Session alive. Session timeouts are there for a reason. If you never wanted the Session to timeout a better way would be to increase the Session timeout to some ridiculous number. Rather, it is much better to raise the event in response to a user’s action. The user panned the map, requested a property detail, clicked a drop down list, etc. If you get real clever, you’ll figure out that you should centralize this event raising and figure out a way to write the code once and have it executed automatically.

19 Comments »

  1. FYI to people downloading the sample, you need to edit the csproj file so it doesn’t try and find an IIS on Joel’s PC, you need to set it up for your own before the project will load 😉

    Comment by Aaron Powell — November 11, 2008 @ 5:53 pm

  2. Ah, crap … I’ll try and take care of that tomorrow morning. Thx for letting me know. And know everybody knows my PC’s name. I’m glad it’s not something obnoxious. :-).

    Comment by Joel Rumerman — November 11, 2008 @ 10:08 pm

  3. I duno, I was rather offended by it 😛

    Also, I’ve just put a post up on my blog regarding this post: http://www.aaron-powell.com/blog/november-2008/maintaining-client-sessions.aspx

    Comment by Aaron Powell — November 12, 2008 @ 5:30 pm

  4. I am using IE 8 to browse my project.
    Its working fine with IE 7 but on IE 8 it randomly shows me the error Session Expired Please log in again.
    (i.e its diverting me to the Session expired page).But i have kept Session timeout=500 still it shows me the error.
    What should i do to avoid this problem.
    Please help me.
    Thanks in advance.

    Comment by Vitthal — June 23, 2009 @ 9:54 pm

  5. Excellent code sample !!!

    It works great.

    Thanks very much.

    JIm

    Comment by Jim Williamson — July 9, 2009 @ 5:24 am

  6. I am using IE 8, .Net Framework 2.0 , ASP.NET2.0 Ajax1.0,XP2 for my project.
    I have created one custom handler as-

    I have done all above ways to solve the issue but it still shows me error ‘Sys’
    is undefined in javascript file

    so what should i do to avoid this problem please help me.

    Comment by vitthal — August 3, 2009 @ 10:10 pm

  7. I came out of ‘Sys’ is undefined problem.

    But now the JS.js is working but without desired output.

    i have kept session timeout=1
    n i have called
    window._intervalId = window.setInterval(window.fn, 5000);
    on login button.

    i debugged the js file its working but my session still expires.
    Any Idea please help me.
    Thanks in advance.

    Comment by vitthal — August 4, 2009 @ 4:25 am

  8. Hi Vitthal,

    Is the handler executing on the server?

    Sorry, not much I can do without seeing the source code.

    Joel

    Comment by Joel Rumerman — August 5, 2009 @ 7:28 am

    • yes the handler is executing on the server.

      when i debugged the script EventPool.Lenght was zero n even OutofBandSessionValidator.length was zero.

      any idea.
      Please help me.

      Comment by Vitthal — August 5, 2009 @ 10:28 pm

      • I am getting one more error for custom handler i.e
        “Could not load type OutofBandSessionValidator”

        while i have written it like

        and i tried in this way also

        my class is as follows

        Imports System.Web
        Imports System.Web.SessionState
        Namespace CustomHandler
        Public Class OutOfBandSessionValidator
        Implements IHttpHandler
        Implements IRequiresSessionState
        Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
        Get
        Return False
        End Get
        End Property

        Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest

        End Sub
        End Class
        End Namespace

        Comment by Vitthal — August 5, 2009 @ 10:33 pm

  9. Because you’ve wrapped the handler in a namespace, the type is CustomHandler.OutOfBandSessionValidator.

    At least one of the errors lie in your web.config. Update it to look like this…

    Comment by Joel Rumerman — August 6, 2009 @ 6:52 am

  10. Hi… Great Code, Works great.

    One question.

    Is there a way to execute a final call when the OutOfBandSessionValidator is disposed.

    The reason for this is that I want to execute some server side code when the validator is stopped.

    Thanks

    Comment by Cesar — October 12, 2009 @ 8:26 am

    • Hi Cesar,

      Thanks!

      Sure. There’s no hook right now, but one could easily be added. The way I would do it is add a new EventPool event to the dispose method. Something like

      $evp.raiseEvent(“OutOfBandSessionValidatorDisposed”, this, Sys.EventArgs.Empty)

      (before the base class’s dispose is called) and then handle the event with some custom code that fires a request to your web service or http handler to do your server side processing. One thing to watch out for is because the EventPool passes this in the raiseEvent method, the validator’s memory won’t be released until your function has completed. Also, if you have an asynchronous request to your web service, even though you have a pointer to this it’ll have gone through its full disposal routine by the time your web service returns so watch out for that too.

      Comment by Joel Rumerman — October 12, 2009 @ 9:06 am

      • Thanks for your reply.

        Honestly, I don’t know where to begin with this. This concept of event handlers with javascript stuff is pretty new to me.

        I’m not good in javascript. Nor I fully understand what’s going on and when.

        Can you be a little more specific as to where I should raise the new event?

        I added it to the dispose: function() {} portion of your code and created a function called “OutOfBandSessionValidatorDisposed”, but obviously I’m doing something wrong.

        Sorry for the noob factor in JS, and thanks for the help.

        Comment by Cesar — October 12, 2009 @ 9:49 am

      • No worries, we were all new at everything at some point :).

        So what I’m recommending is that you alter my code and add another event that gets raised when the dispose method is called.


        dispose: function() {
        if (this._revalidateSessionDelegate !== null) {
        $evp.removeEvent(“RevalidateSession”, this._revalidateSessionDelegate);
        this._revalidateSessionDelegate = null;
        }

        // NEW EVENT RAISED
        $evp.raiseEvent(“OutOfBandSessionValidatorDisposed”, this, Sys.EventArgs.Empty)

        _OutOfBandSessionValidator.callBaseMethod(this, ‘dispose’);
        },

        from there, you can do whatever processing you need to do. Somewhere else, in your own code, you handle the event..

        $evp.addEvent(“OutOfBandSessionValidatorDisposed”, function (sender, args) {
        // do something here

        }); // (I didn’t check syntax, but that should be right or very close)

        Now when the dispose method in the OutOfBandSessionValidator executes, the new event will be raised and your code inside the function will execute. At that point you can call back to the server through a web service or whatever it is you need to accomplish when the OutOfBandSession validator is disposed.

        Comment by Joel Rumerman — October 12, 2009 @ 12:05 pm

  11. Thanks a million… I’m going to give this a run.

    Comment by Cesar — October 13, 2009 @ 6:07 am

  12. The most easy option for keep session timeout by any kind of call to our method is use a dummy session var:

    public function MyMethod()

    ‘Refresh Session TimeOut
    HttpContext.Current.Session(“DummySessionVar”) = 1

    end function

    Regards

    Comment by Oscar Salazar — December 22, 2010 @ 11:44 am


RSS feed for comments on this post. TrackBack URI

Leave a reply to Joel Rumerman Cancel reply

Create a free website or blog at WordPress.com.