Introduction
It is very common for a WebForm programmer to store the business object in session state. However, the business object stored in the session is not protected within the page state. It ends up that a business object in one webform may be overwritten when the user opens another browser instance for the same webform.
The solution proposed here is to maintain the session within page state, by giving a unique identifier to each page. By having different page that has its own session state, we may end up with a lot of unused data in memory when the user leaves the page. The solution used here is to provide a Session Garbage Collector to release that unused memory which is recognised as timeout page session.
Background
This is my first attempt to submit an article to The Code Project.
Using the Code
There are 3 main classes in this solution:
PageSession
ManagedSession
ManagedSessionGarbageCollector
1. PageSession
Instead of storing the data in a native Session
object, the page data will be stored in the dictionary of a PageSession
object.
The PageSession
object keeps the last access timestamp. This is to indicate whether a session data needs to be released during garbage collection.
[Serializable]
public sealed class PageSession
{
Dictionary<string, object> _items = new Dictionary<string, object>();
DateTime _timeLog = DateTime.Now;
string _key = Guid.NewGuid().ToString();
internal void TimeLog()
{
_timeLog = DateTime.Now;
}
public string ID
{
get { return _key; }
}
public DateTime LogTime
{
get { return _timeLog; }
}
public int Count
{
get { return _items.Count; }
}
public object this[string name]
{
get
{
if (_items.ContainsKey(name))
{
return _items[name];
}
return null;
}
set
{
if (!_items.ContainsKey(name))
{
_items.Add(name, value);
}
else
{
_items[name] = value;
}
}
}
public void Clear()
{
_items.Clear();
}
}
2. ManagedSession
The ManagedSession
object is responsible for maintaining the PageSession
life-cycle. It is also responsible for mapping the correct PageSession
to a responding web page. This is done by registering the PageSession
unique identifier in the webform viewstate.
[Serializable]
public sealed class ManagedSession
{
private const string JSL_ManageSession = "__JSL_ManageSession";
private const string JSL_PageSession_ID = "__JSL_PageSession_ID";
Dictionary<string, PageSession> _pageSession;
ManagedSessionGarbageCollector _sgc;
public ManagedSession()
{
_pageSession = new Dictionary<string, PageSession>();
_sgc = new ManagedSessionGarbageCollector(_pageSession);
}
public static PageSession GetPageSession(System.Web.UI.StateBag viewState)
{
return GetManagedSession().RegisterPageSession(viewState);
}
private static ManagedSession GetManagedSession()
{
var ms = System.Web.HttpContext.Current.Session[JSL_ManageSession] as ManagedSession;
if (ms == null)
{
ms = new ManagedSession();
System.Web.HttpContext.Current.Session[JSL_ManageSession] = ms;
}
return ms;
}
private PageSession RegisterPageSession(System.Web.UI.StateBag viewState)
{
string id = "";
if (viewState[JSL_PageSession_ID] != null)
{
id = viewState[JSL_PageSession_ID].ToString();
}
PageSession ps;
if (_pageSession.ContainsKey(id))
{
ps = _pageSession[id];
}
else
{
if (id.Length > 0)
{
var url = ManagedSessionSetting.ExpiredURL;
if (url == "")
throw new Exception("Page session expired!");
if (url == "REFRESH") url = System.Web.HttpContext.Current.Request.Url.PathAndQuery;
System.Web.HttpContext.Current.Response.Redirect(url, true);
}
ps = new PageSession();
_pageSession.Add(ps.ID, ps);
viewState[JSL_PageSession_ID] = ps.ID;
}
ps.TimeLog();
_sgc.GarbageCollection();
return ps;
}
}
3. ManagedSessionGarbageCollector
As the name implies, the ManagedSessionGarbageCollector
is responsible for performing garbage collection to release those expired/timeout PageSession
maintained by ManagedSession
.
internal class ManagedSessionGarbageCollector
{
DateTime _lastCollectionTime = DateTime.Now;
Dictionary<string, PageSession> _managedPageSession;
Thread _garbageCollectorThread;
public ManagedSessionGarbageCollector(Dictionary<string, PageSession> pageSession)
{
_managedPageSession = pageSession;
}
public void GarbageCollection()
{
lock (this)
{
if (IsCollectable())
{
_garbageCollectorThread = new Thread(new ThreadStart(Collect));
_garbageCollectorThread.Start();
}
}
}
public bool InProcess
{
get
{
if (_garbageCollectorThread == null) return false;
return (_garbageCollectorThread.ThreadState == ThreadState.Running);
}
}
private void Collect()
{
List<string> garbage = new List<string>();
foreach (var id in _managedPageSession.Keys)
{
if (CheckTimeOut(_managedPageSession[id].LogTime))
{
garbage.Add(id);
}
}
garbage.ForEach(id => _managedPageSession.Remove(id));
_lastCollectionTime = DateTime.Now;
}
private bool IsCollectable()
{
bool collectable = false;
if (!InProcess)
{
collectable = CheckTimeOut(_lastCollectionTime);
}
return collectable;
}
private bool CheckTimeOut(DateTime checkPoint)
{
TimeSpan ts = DateTime.Now.Subtract(checkPoint);
if ((ts.Minutes * 60 + ts.Seconds) > ManagedSessionSetting.SessionTimeout)
return true;
return false;
}
}
Code in Action
To use the code, you just need to get the PageSession
object from ManagedSession
in the Form_Load
event.
private JSL.Web.PageSession mySession;
protected void Page_Load(object sender, EventArgs e)
{
mySession = JSL.Web.ManagedSession.GetPageSession(this.ViewState);
}
After this, you need refer to mySession
instead of native Session
to store your data.
You may set the timeout in web.config file, default and minimum value is 30 seconds.
<appSettings>
<add key="JSL.ManagedSession.Timeout" value ="60"/>
<add key="JSL.ManagedSession.ExpiredURL" value ="Default2.aspx"/>
</appSettings>
When a page is trying to access a timeout and remove PageSession
, an exception will be thrown if the ExpiredURL
is not set.
Points of Interest
I use threading for garbage collection. However, I am not good in that area. I hope that some expert on CodeProject will help me to improve the code so that I can learn from it.
History
- 2009 January 17 - Original version posted