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?
Would it matter that the expiration time of the session is not updated as the user interacts with the website?
Hi Charlie,
I gave it a maximum age based upon the following. My average car trip is much less than 24 hours. In fact my longest trip is less than that. I was thinking of storing the gps data associated with trip segments. You can always go request another trip session. Are you thinking of a particular use case where extending the expiration would be important?
Bob
I didn’t think about that, it makes sense to keep it at 24 hours based on that assumption. But now that you mention it… if you had a client that could record gps points while disconnected and then upload the trip data when a data connection became available you may need a longer timeout. For example, a backcountry ski trip might require this, but that is completely outside of the design spec.
Why not just use the out of process mode for your session using the ASP state service to persist your session data in memory? Using this method should survive IIS restarts and your session data will still remain in memory. You can specify the timeout in your web.config as well. No offense but it seems you’re re-creating an existing wheel.
Refer to this article under Out-of-process Mode:
http://msdn.microsoft.com/en-us/library/ms972429.aspx
Hi,
Its a good article. I guess you will require to update the text file every time the user logs out of the session. As in this situation we do not need the entry to be there in the text file. So don’t you think adding and removing users session details for every log in and log out operation is expensive as this is an I/O operation.
What about Web Farm scenario? I mean this file needs to be shared by all the web servers. So isn’t it the critical part to implement?
For a single web server and not to have database storage for sessions of such a small size. This looks a good thing to do….
Interesting approach and nice-written article. I have similar experience in dealing with this particular scenario by leveraging stateserver, (http://msdn.microsoft.com/en-us/library/ms972429.aspx) which can fully work out within the server farm scenario as well by storing all the session state in the memory within the stateserver.