A Persistent ASP.NET Session Manager

I recently had a need for an ASP.NET Session that lasted longer that a normal web session (one day) and was able to survive application restarts (or IIS restarts). I know there are other solutions that exist. Some involve storing the Session State in a database. Adding to my database schema to store the Session State data seemed a bit over kill. The data I am storing is small and would normally be held in-memory (like the normal ASP.NET Session State). I just wanted to add a bit of ‘life-after-a-restart’ feature to the normal in-memory Session.

Session Domain Model

Before looking at the implementation, let’s take a look at the data that I am storing in this Session Manager. I wanted a typed object that new how to re-hydrate / serialize itself from / to the Session. The following code is the typed object:

public class TripSession
{
    public Guid Id { get; set; }
    public DateTime Expiration { get; set; }
    public string UserName { get; set; }
    public int TruckId { get; set; }

    public override string ToString()
    {
        return Id + "t" + Expiration + "t" + UserName + "t" + TruckId;
    }

    public static TripSession ParseLine(string line)
    {
        string[] parts = line.Split('t');
        if(parts.Length!=4)
        {
            return null;
        }
        try
        {
            TripSession tripSession = new TripSession
                                          {
                                              Id = new Guid(parts[0]),
                                              Expiration = DateTime.Parse(parts[1]),
                                              UserName = parts[2],
                                              TruckId = int.Parse(parts[3])
                                          };
            return tripSession;
        }
        catch (Exception)
        {
            return null;
        }
    }
}

As you can see this object has four public properties. In addition the ‘ToString’ has an override that serializes these properties into tabbed-separated string. Finally, there is a static method that takes a tabbed separated string and re-hydrates it into an object.

Session Manager

The higher level class that manages the ‘TripSession’ objects is shown in the following code:

public class TripSessions
{
    private const string _relativeSesionFile = "~/App_Data/TripSessions.txt";
    private readonly string _sessionFile;
    private readonly ILogger _logger;
    private readonly Dictionary<Guid, TripSession> _sessions;
    private readonly TimeSpan _maxAge = new TimeSpan(1, 0, 0, 0);

    public TripSessions(ILogger logger, HttpContextBase httpContextBase)
    {
        _logger = logger;


        _sessionFile = httpContextBase.Server.MapPath(_relativeSesionFile);

        _sessions = ReadSessionFile();
    }

    public TripSession CreateSession(string userName, int truckId)
    {
        try
        {
            TripSession tripSession = new TripSession
                                          {
                                              Id = Guid.NewGuid(),
                                              Expiration = DateTime.Now + _maxAge,
                                              TruckId = truckId,
                                              UserName = userName
                                          };
            _sessions[tripSession.Id] = tripSession;
            SaveSessionFile();
            _logger.Debug("Created session for: username=" + userName + ",truckid=" + truckId);
            return tripSession;
        }
        catch (Exception ex)
        {
            _logger.Error("Failed to create session. ", ex);
        }
        return null;
    }

    public TripSession GetSession(Guid id)
    {
        if(_sessions.ContainsKey(id))
        {
            return _sessions[id];
        }
        return null;
    }

    private void SaveSessionFile()
    {
        _logger.Debug("Saving trip session data to file.");
        List<string> lines = new List<string>();
        foreach (KeyValuePair<Guid, TripSession> keyValuePair in _sessions)
        {
            TripSession tripSession = keyValuePair.Value;
            lines.Add(tripSession.ToString());
        }
        File.WriteAllLines(_sessionFile, lines.ToArray());
    }

    private Dictionary<Guid, TripSession> ReadSessionFile()
    {
        _logger.Debug("******READING TRIP SESSION FILE**********");
        Dictionary<Guid, TripSession> result = new Dictionary<Guid, TripSession>();

        if(!File.Exists(_sessionFile))
        {
            _logger.Debug("The session file does not exist. file=" + _sessionFile);
            return result;
        }

        string[] lines = File.ReadAllLines(_sessionFile);
        foreach (string line in lines)
        {
            TripSession tripSession = TripSession.ParseLine(line);
            if (tripSession != null && (DateTime.Now - tripSession.Expiration) < _maxAge)
            {
                result[tripSession.Id] = tripSession;
                _logger.Debug("ADDED---->" + line);
            }
            else
            {
                _logger.Debug("EXPIRED-->" + line);
            }
        }
        return result;
    }
}

One instance of the ‘TripSession’ class is created by my IOC container (Ninject). In essence the ‘TripSession’ entity is a “singleton”. The constructor of the ‘TripSessions’ class accepts two dependencies (again both supplied by my IOC container). The ‘ILogger’ instance is used for logging and the ‘HttpContextBase’  is used to resolve the relative path of the file used to store the session data. Once the relative path has been resolved, the session data is loaded from the disk by calling the private ‘ReadSessionFile’ method. Because this object is a “singleton” this reading from disk only occurs during application startup.

The ‘ReadSessionFile’ creates a Dictionary with a Guid key and a TripSession as the value. The code then populates this dictionary by processing each line in the session file. A bit of logic is applied to enforce an expiration (one day) of the TripSession objects. This result is then used by the class as an in-memory data store of TripSession objects (via the ‘_sessions’ field).

New ‘TripSession’ object are created by calling the ‘CreateSession’ method. An expiration date is applied to the object as it is created. To insure the new data will persist across restarts, the data is then serialized to disk (by calling ‘SaveSessionFile’). The ‘SaveSessionFile’ method simply accumulates all the current TripSession data into a list of strings which is then serialized out to the data file.

The remaining method, ‘GetSession’, takes a Guid as a parameter, finds the object in the dictionary and returns the instance. Otherwise it returns null.

Summary

The Session Manager above will persist session data across application (and IIS) restarts. Being file based has advantages and disadvantages which you will need to weigh in your design decision. I minimized the disk thrashing by reading and writing only win necessary. I would be interested to hear your solutions to this problem. How do you persist Session data across application restarts?

Tags:,
Comments
  1. Charlie Knoll
    • rcravens
      • Charlie Knoll
  2. Eric Brumfield
  3. Amit
  4. Eric Xue

Leave a Reply

Your email address will not be published. Required fields are marked *

*