Prevent Multiple Logins Using the Cache in ASP.NET

By Robbe D. Morris

Printer Friendly Version

Robbe Morris
Robbe & Melisa Morris
In classic ASP, there has a been long standing problem with relying on the Session_OnEnd event firing properly.  One of the main reasons developers wanted to use this event was to manage restricted access to a website.  For instance, if the site needed to restrict multiple users from utilizing the same username/password combination at the same time because it would allow licensing abuses.  Accurately keeping track of who is currently logged in was easy.  The difficult part was consistently clearing out locks for a user when he or she closed the browser or left to visit another site.  The end result of this was an increase in technical support calls to remove the locks and angry users.
In ASP.NET, the Session_OnEnd event appears to fire fairly consistently when using InProc as the Session management option.  However, if the ASP.NET worker process recycles, you are likely to get inconsistant results.  The Session_OnEnd event doesn't fire properly when utilizing State Server or SQL Server as the State management option.  For many of us, this creates a real problem.


After giving various options a great deal of thought, I opted to use the Application Cache to provide duplicate functionality to the Session_OnEnd event.  As you may already know, the Application Cache has enormous power and flexibility.  One of the main reasons I opted to use the cache was it's ability to react to the removal of an item from the cache.  Plus, you can set the cache to expire x number of minutes (or other desired timeframe) if the cache entry hasn't been accessed within the desired timeframe.  The combination of these two capabilities permits the developer to literally duplicate the Session_OnEnd event.  This includes when .NET recycles the ASP.NET worker process (not if you kill it via Task Manager or it crashes) and you can test this by adjusting the processmodel node and idleTimeout attribute to say 00:00:45 (45 seconds) instead of Infinite in the machine.config file.  Restart your website (just to be sure the setting was taken although it should automatically).  Browse to your test page and leave the browser up without leaving the page.  In 45 seconds or less, the Application Cache removal will get triggered.
Here's how it works.  The developer attempts to insert an entry into the Application Cache (code is in global.asax) with a unique cache item name.  I chose to include the user's database id as part of it's name.  During the insert, set the sliding expiration to a timeframe just under your setting for Session.Timeout and register a callback event method for when the item is removed from the Application Cache.  Then, on every desired page in the site where we want to refresh the sliding expiration of the cached item, simply retrieve the value from the cache.  That's it.  If the user leaves the site or closes their browser without following your proper signout procedures, when your cached item expires the method you registered will be triggered and perform your unlocking code.  You may also want to look at the protected void Application_PreRequestHandlerExecute(Object sender, EventArgs e) event in global.asax.  It is not added to the file by default and fires just before ASP.NET begins executing a handler such as a page or XML Web service.  Depending on your application requirements, you may or may not want this to occur with every request.
In most applications, I think you can get away with not doing anything in the expiration callback method.  I included it in this article to demonstrate how just in case you needed it.  The whole process could be as simple as not letting another user with the same username/password combination log into the site if an entry exists in the Application Cache for the same user id.  If the account is already in use, you can tell the user that if they logged out inappropriately, their account will be free again in x minutes as long as noone else has logged in.  This functionality should be sufficient for most applications.  As a fall back, you'll also want to enable your technical support representatives with the ability to remove locks manually should your code ever fail.
The code sample below has two files: global.asax and default.aspx.  They contain a very basic example of how to create entries in the cache and react to cache removal.  I simply instructed the SessionEnd method to email me when it fires and didn't try to create a more complex user login process to make it easy for you to test the sample out.  As long as you have the SMTP service running, you should be able to change the email address to your own for testing.  You'll also want to experiment with the different Session management options in your web.config file and how they affect session values in the method called when the item is removed from cache.  This should give you a good start on your own process for enabling single user access to a user account at any one given time at a minimal cost to performance.

global.asax Source Code
  
<SCRIPT LANGUAGE="c#" RUNAT="server">
       
 private int mnSessionMinutes = 2;
 private string msSessionCacheName="SessionTimeOut";
  
public void InsertSessionCacheItem(string sUserID)
{
   
   try
     {
         if (this.GetSessionCacheItem(sUserID) != "") { return; }
         CacheItemRemovedCallback oRemove = new CacheItemRemovedCallback(this.SessionEnded);
         System.Web.HttpContext.Current.Cache.Insert(msSessionCacheName + sUserID,sUserID,null, DateTime.MaxValue, 

TimeSpan.FromMinutes(mnSessionMinutes),CacheItemPriority.High,oRemove);
      }
      catch (Exception e) { Response.Write(e.Message); }
}

public string GetSessionCacheItem(string sUserID)
{
      string sRet="";

      try
       {
         sRet = (string)System.Web.HttpContext.Current.Cache[msSessionCacheName + sUserID];
         if (sRet == null) { sRet = ""; }
        }
       catch (Exception) { }
       return sRet;
}
 

public void SessionEnded(string key, object val, CacheItemRemovedReason r)
{

    string sUserID="";
    string sSessionTest="";

   try
    {
      sSessionTest = Session["Test"].ToString();
    }
    catch (Exception e) { sSessionTest = e.Message;}

    try
    {

        sUserID = (string)val;
     // Make sure your SMTP service has started in order for this to work

        System.Web.Mail.MailMessage message = new System.Web.Mail.MailMessage();

        message.Body = "your session has ended for user : " + sUserID + " - Session String: " + sSessionTest;
        message.To = "your email goes here";
        message.From = "info@eggheadcafe.com";
        message.BodyFormat = System.Web.Mail.MailFormat.Text;
        message.Subject = "Session On End";

        System.Web.Mail.SmtpMail.Send(message);		
 
        message = null;

     }
     catch (Exception) { }

}
 
</SCRIPT>
    
default.aspx Source Code
  
 <script Language="C#" runat="server">
 
 protected string msUserID = "201";
 protected string msSessionCacheValue="";
 
 protected void Page_Load(object sender, EventArgs e) { ProcessPage(); }
           
 public void ProcessPage()
 {
    Session["Test"] = "test string to see if session variables still exist after app recycle.";

    ((global_asax)Context.ApplicationInstance).InsertSessionCacheItem(msUserID);

   // Test to access the cache and make sure it still exists
    msSessionCacheValue = ((global_asax)Context.ApplicationInstance).GetSessionCacheItem(msUserID);
 }
 
</script>   

<HTML><BODY>

 <table border="0" align="center" cellpadding="2" cellspacing="2" width="70%">
  <tr><td><br></td></tr>
  <tr><td><br></td></tr>
  <tr><td align="left">Close the browser and wait for email notification</td></tr>
  <tr><td><br></td></tr>
  <tr><td align="left"><a href=default.aspx target=_self>Redirect to this page to reaccess the cache</a></td></tr>
  <tr><td><br></td></tr>
  <tr><td align="left"><%=  Session["Test"].ToString()  %></td></tr>
  <tr><td><br></td></tr>
  <tr><td align="left"><%= msSessionCacheValue %></td></tr>
 </table>

</body></html>
    

Robbe has been a Microsoft MVP in C# since 2004.  He is also the co-founder of NullSkull.com which provides .NET articles, book reviews, software reviews, and software download and purchase advice.