Introduction
ASP.NET provides the intrinsic Session object as a convenient
storage facility to persist small data items. (Note the emphasis on
small . If you store recordsets or
monstrous arrays in session state, you shouldn't be discussing the
subtleties of ASP.NET session state; you should be fixing the
architecture of your application!) Session variables save you a lot
of custom state maintenance code that you had to write before the
advent of ASP. But they are not easy to deal with at design time.
For instance, you must remember or look up session variable names
while coding because VisualStudio.NET cannot provide intellisense
for them. You must remember their data types too. An incorrect cast
is not caught by the compiler and manifests only at run time.
You can work around these and many similar annoyances by creating
a custom class for your session variables. The class will make your
life easier when using the VisualStudio.NET editor. You will be able
to build code faster, organize your code better and your code will
have fewer bugs.
System Requirements
To follow along with this article, you will need the following
software:
- Windows 2000 Professional/Windows 2000 Server/ Windows
XP/Windows 2003 Server
- The .NET framework (including ASP.NET) version 1.0 or later
- Visual Studio .NET
You also need the following background:
- Familiarity with ASP.NET using VB.NET
- Firsthand experience with session variables in order to
appreciate their limitations
- Familiarity with Object Oriented Programming
The accompanying sample code is written in VB.NET using version
1.0 of the framework and tested on a Windows 2000 Server.
The Problem: Session Variables Are Difficult To
Code With
In the introduction, I briefly mentioned that I find session
variables awkward to deal with while coding. Here is a more detailed
list of reasons why:
- Normally, you must declare a variable before you can
manipulate it. (This is always the case in C# and is the case in
VB.NET if Option Strict or Option
Explicit is on.) Not so with session variables. You can manipulate
Session("anything") without
declaring it first. It is not flagged with the familiar squiggly
line or a compilation error. This results in programmers adding
variables to the session all over the application. I would like to
have a way to force programmers to declare all session variables
in one place for easy reference.
- Once you decide on a name, you must type it in
VisualStudio.NET editor.VisualStudio.NET cannot provide
intellisense for session variables because the environment cannot
obtain type information that intellisense requires. This compels
your to remember a variable's name. A typographical error can be
disastrous. For instance, if you type Session("ProductOd")= 3 when
you intended to type Session("ProductId") = 3, you create two
problems. First, you unknowingly create a superfluous variable.
Second, when you check the value of Session("ProductId") on
another page, it does not contain the value you expect it to.
Tracking down bugs introduced by inadvertently using the same
session variable name for two distinct entities can be very
tricky. I would like to have a way to get intellisense for session
variables in VisualStudio.NET environment
- Type safety is a core feature of the Common Language Runtime.
The managed compliers are designed to throw a fit at the slightest
hint of type safety violation. No matter what type of value you
store in it, a session variable is always of the type Object. You
must not only remember the data type of the value stored in a
session variable, you must also explicitly cast it to that data
type every time you use the variable. An incorrect cast manifests
at the least desirable time - run time. I would like to have a way
to catch casting violations at compile time rather than at run
time.
- Properties of an object can be used to store data as well as
to execute code. Setting a session variable is often logically
associated with setting other session variables or executing some
piece of code. For example, every time you get a product id, you
may want to fetch its name from the database and set a
corresponding session variable as well. I would like the ability
to encapsulate associated logic with session variables.
- Values stored in Session are always modifiable; you can't
declare ReadOnly session variables. Consider the example in the
previous bullet point. The name of the product should probably not
be modifiable on its own since it changes in conjunction with the
product id. I would like the ability to declare ReadOnly session
variables.
The Solution: Create A Custom Class
To arrive at a solution, you must first understand what causes
the problem. When you store values as session variables, they get
added to the intrinsic Session object, which is an instance of
HttpSessionState class. This type is implemented as a weakly typed
collection, which stores entities of type Object. This gives you
some flexibility and you can store pretty much anything in the
Session object. But the flexibility comes at a cost. When you store
an instance of any type in a collection, it gets cast to the type
Object. You must cast it back to its appropriate type upon
retrieval. The compiler has no knowledge of the intended type of the
object on retrieval and, it is unable to catch casting errors.
VisualStudio.NET faces the same problem. It can't determine the type
of an object when you refer to it in the editor as a member of a
collection and consequently, can't provide intellisense.
Armed with this knowledge, you can devise a solution as
follows:
- Implement all your session variables as properties of a custom
class. Properties can have types and can execute code.
- Instead of adding many individual variables to the intrinsic
Session object, add a single instance of the custom class to it
when a session begins. You will now have a solitary session
variable.
- In the Load event of each page, reference the instance of your
class from Session and cast it appropriately. While coding you
will refer to your custom class rather than the session object.
VisualStudio.NET can access type information for your class and
provide intellisense. The compiler can use the type information to
perform casting checks.
- Retrieving and casting the object on every page can quickly
become cumbersome. So abstract the functionality into a class
inherited from Page class and use the descendent class as the
ancestor of all our pages.
Let's write a small application based on the outline above to
demonstrate the technique. Here is an overview of the steps:
- Create a new project
- Create a class to hold the session variables you want to
define. I will call the class SessionManager.
- Declare a property in the class that represents each session
variable you want to store. You can make your properties ReadOnly
if you wish and also assign them types.
- Declare a constructor for the class so as to "capture"
built-in Session properties such as SessionID.
- In the Session_Start event in Global.asax, declare and
instantiate an object of the type SessionManager and add the
object to the intrinsic Session object with a variable name, say,
ThisSession.
- Inherit a class from System.Web.UI.Page class and use it as
the ancestor class for all the pages in your project. I will call
this class PageManager.
- Declare a Protected object of the type SessionManager called
CurrentSession, in PageManager class. Fetch the ThisSession object
from the session and assign it to the CurrentSession variable in
Page_Load event.
- Add the necessary UI to the application to test the class.
The Code
Create a New Project
Create a new ASP.NET Web Application project under Visual Basic
Projects and call it SessionManagerDemo. Make sure from the
Project/Add Reference menu item that System.Web.dll has been
referenced
Declare SessionManager Class
Add a new class file to the project and call it
SessionManager.vb. Create a class as follows: <Serializable()> Public Class SessionManager
End Class
Notice the <Serializable> attribute in the class
declaration. It indicates to the runtime that this object can be
serialized. If you use StateServer or SqlServer session state modes,
your application needs the ability to serialize the data stored in
the session object out to the state server process or the database
and deserialize it when it is retrieved. The <Serializable>
attribute enables the CLR to use the framework's binary formatter to
serialize and deserialize session state data if and when required.
You don't need to worry about how it is done; ASP.NET automatically
takes care of it for you. If your application uses the InProc mode,
you don't need to specify the attribute. But there is no harm in
including it.
Incidentally, if you are using StateServer or SqlServer session
state modes and you fail to include the <Serializable>
attribute, the application will throw an exception.
Declare session variables as properties or
methods of SessionManager
Declare two variables, one to hold a product id and the other for
product name. Declare and initialize the variables as private
variables in SessionManager class as follows: Private _productId As Int32 = 0
Private _productName As String = "Undefined"
Next, declare corresponding properties and methods to get and set
these variables. To illustrate ReadOnly variables and the ability to
execute code from properties, I have defined the ProductName
property to be ReadOnly. The Set accessor of ProductId property,
will call the private method SetProductName , which sets the value
of the private variable _productName. Notice that each private
variable and its corresponding property have a specific type. Public Property ProductId() As Int32
Get
Return _productid
End Get
Set(ByVal Value As Int32)
_productid = Value
SetProductName(Value)
End Set
End Property
Public ReadOnly Property ProductName() As String
Get
Return _productname
End Get
End Property
Private Sub SetProductName(ByVal productid As Int32)
'Some code here that will fetch the name
'from the database. Pretend the name is "Thingamajig"
_productName = "Thingamajig"
End Sub
While you code your application, you will come back here from
time to time to add properties and methods for every new session
variable you want to declare.
Declare a new constructor for the class
You often have to use Session.SessionID in your code. For
instance, your application may have to collect user input over a
series of pages and persist it in database tables until it is
processed during the course of the session. In such a situation, you
need a unique identifier that will enable you to associate the input
with a specific session being run by a specific user. You may decide
that the combination of session id and user id suits the purpose and
use it as the unique identifier. The session id does not change
during the session. Therefore, you can create it to be a read only
property on SessionManager and set it in the object's constructor.
You can propagate any built-in property of the intrinsic session
object to SessionManager in this manner. Private _sessionId As String = "Undefined"
Sub New(ByVal sessionid As String)
_sessionId = sessionid
End Sub
I have chosen to capture the session id and store it in the
custom class because when I want to refer to the session id in the
code, I would like to refer to it as a property of the custom class
rather than as a property of the session object. You can set other
intrinsic properties that you want to refer to in your code in this
fashion as well and obviate the need to refer to the Session object
directly in your Web Forms. Each of the properties you propagate to
the custom class will require an accessor. Declare one for the
session id as follows:
Public ReadOnly Property SessionId() As String
Get
Return _sessionId
End Get
End Property
Code Session_Start Event in Global.asax
In Global.asax, add the following code to Session_Start Event Dim ThisSession As SessionManager = New SessionManager(Session.SessionID)
Session.Add("CurrentSession", ThisSession)
This creates a new instance of the SessionManager class and adds
it to the intrinsic session.
Declare PageManager Class
Add a new class file to the project and call it PageManager.vb.
Create a class as follows:
Public Class PageManager
Inherits System.Web.UI.Page
End Class
The PageManager class will replace System.Web.UI.Page as the
ancestor for all Web Forms in the project. The PageManager class is
derived from System.Web.UI.Page class because the application still
needs all its functionality.
Creating PageManager class is not strictly necessary. You can
achieve the same effect by instantiating CurrentSession on every
page instead. It is a good idea, however, to create an ancestor
class for your pages because it provides an additional layer of
abstraction that allows you to write code that you want all your
pages to inherit.
Notice that I created SessionManager and PageManager classes in
two separate physical files. This is not necessary either, but it is
a good idea. Keeping each class in its own file allows you to locate
a class quickly and check classes in and out individually in a
source control manager such as SourceSafe.
Add code to PageManager class
You now have an instance of SessionManager in the intrinsic
Session. All you have to do is create a reference to it during the
loading of the page. Once you do that, the functionality of
SessionManager class will be available to all pages derived from
PageManager.
Add the following code to the PageManager class to do just
that.
Protected CurrentSession As SessionManager
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As _
System.EventArgs) Handles MyBase.Load
'Retrieve Sessionmanager from the Session and assign
'to a local variable
CurrentSession = DirectCast(Session("CurrentSession"),
SessionManager)
End Sub
As we saw earlier, when you want to manipulate an object stored
in the intrinsic session, you must first cast it to its correct
type. The code above retrieves the object stored as
Session("CurrentSession") and casts it to it's correct type, namely
SessionManager. VisualBasic.NET provides two operators for casting:
DirectCast and the more commonly used CType. DirectCast is the
better option of the two because it is more efficient. However, it
can only be used for casting reference types. Since we are dealing
with reference types here, I chose to use DirectCast instead of
CType
Testing The Code
Its time to verify that the class functions as advertised. To do
so, we need to write a couple of WebForms. Follow the steps
below
- Add a new Web Form to the project. Call it FirstForm.aspx
- Open the code behind file FirstForm.aspx.vb .
Change the line Inherits System.Web.UI.Page
to Inherits PageManager
- In Page_Load event, write the following Code
CurrentSession.ProductId = 3
The moment you type the dot after CurrentSession, an intellisense
pop-up appears. (See figure 1). While you are at it, try assigning a
String value such as "abcd" to CurrentSession.ProductId. The
customary squiggly line will appear right away (See figure 2). Try
misspelling "ProductId" and you will again see the squiggly line
underscoring the error (See figure 3). So you see it is difficult to
make mistakes with session variables when you use the class.
Figure
1: Intellisense for session variables
Figure 2: Incorrect type assignments underscored by squiggly
lines
Figure 3: Typos are caught at design time and underscored by
squiggly lines
- In Button1_Click event, write the following code
Response.Redirect ("SecondForm.aspx")
- Save and close the file.
- Add another Web Form to the project and call it
SecondForm.aspx.
- Open the code behind file SecondForm.aspx.vb and change the
second line as you did with FirstForm to
Inherits PageManager
- In Page_Load event, add the following code
Response.Write("Session Id of the Intrinsic Session object is :" &
Session.SessionID)
Response.Write("<BR>")
Response.Write("SessionId propagetd by SessionManager class is :" _
& CurrentSession.SessionId)
Response.Write("<BR>")
Response.Write("ProductId propagated by SessionManager class is :" _
& CurrentSession.ProductId)
Response.Write("<BR>")
Response.Write("ProductName propagated by SessionManager class is :" _
& CurrentSession.ProductName)
- Save and close the file.
- Set FirstForm.aspx as Start Page in Solutions Explorer.
Run the application. When FirstForm comes up, click the button.
You will see the session variable propagated to SecondForm (See
figure 4).
Figure 4: Session variables are propagated to SecondForm.aspx
Evaluating Impact on Performance
Adding a layer of abstraction negatively affects performance. We
must evaluate the impact and judge whether the tradeoff is worth the
effort.
Outwardly, this whole mechanism seems to be dubious from the
performance point of view because it appears that you end up
instantiating the local variable CurrentSession every time a page is
invoked. Shouldn't this use up memory on the managed heap and send
the garbage collector out working furiously?
Not really. The class has very little overhead. SessionManager is
a reference type. The runtime allocates memory for the value stored
in an instance of a reference type on the managed heap and allocates
a reference to the value on the stack.
When you first instantiate a variable of the type SessionManager
in the Session_Start event, the statement Dim ThisSession As SessionManager = New SessionManager(Session.SessionID)
causes the runtime to allocate memory for an object of
SessionManager type on the managed heap and create a reference to it
on the stack. Next, the statement Session.Add("CurrentSession", ThisSession)
Will cause the runtime to create a reference to the memory
allocated for ThisSession on the heap and store it in a variable
(effectively) called Session("CurrentSession")on the stack. The
statement Protected CurrentSession As SessionManager =
DirectCast(Session("CurrentSession"), SessionManager)
in Page_Load event of PageManager class does not cause the
runtime to physically allocate another chunk of memory on the
managed heap because a constructor of SessionManager class is not
invoked in the code. The runtime just creates another reference to
the same block of memory that is allocated to ThisSession variable
stored in the intrinsic Session object and stores a reference to it
in a local variable called CurrentSession on the stack. When the
page goes out of scope, the runtime unwinds the stack and frees up
the memory taken up by the reference (CurrentSession). Reference
pointers are only 32 bits long and hence this mechanism is not
nearly as memory intensive as it appears at first glance.
You may also have noticed that you retrieve the session
information into the CurrentSession variable during the Page_Load
event of PageManager but never seem to load CurrentSession back into
the intrinsic Session object with a statement such as Session("CurrentSession") = CurrentSession
The reason again, is that every time you modify a data item in
the local CurrentSession variable, the memory location it refers to
on the managed heap is modified. The ThisSession variable stored in
the intrinsic Session object refers to the same physical location on
the heap and it too, in effect, is "modified". This is somewhat like
passing a parameter by reference.
We can confirm these assertions programmatically. The Is operator
in VisualBasic.NET checks whether two variables are identical, i.e.
if two variables refer to the same physical memory location (as
opposed to "=" operator which checks whether two variables contain
the same value). Add a button and a label to SecondForm.aspx. If you
have followed along with the code, they will be called Button1 and
Label1 respectively. In Button1_Click event, add the following
code Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As _
System.EventArgs) Handles Button1.Click
Label1.Text = ((CurrentSession Is Session("CurrentSession")).ToString() & _
" : CurrentSession and Session("CurrentSession") are Identical!")
End Sub
Run the application and click your way to SecondForm.aspx. Click
the button and you will see the label populated with "True". (See
figure 5). By the way, the class Object has a shared method called
ReferenceEquals that compares two variables to determine whether
they both point to the same object. You can use that method as
follows instead of the Is operator to arrive at the same boolean
value. Label1.Text = Object.ReferenceEquals(CurrentSession, _
Session("CurrentSession")).ToString())
Figure 5: CurrentSession is identical to
Session("CurrentSession")
The Sample Application
To run the sample application in the downloadable code
- Unzip the downloaded zip file and extract the
SessionManagerDemo folder.
- Copy the folder to your Inetpub/wwwroot folder.
- Bring up Internet Services Manager and right click on
SessionManagerDemo folder to bring up the properties window. Click
the Create button and then click OK. Close Internet Services
Manager.
- Open a browser window and enter the URL http://localhost/SessionObjectDemo/FirstForm.aspx?WROXEMPTOKEN=WROXEMPTOKENVALUE
and hit enter.
- You should see the session variables displayed on SecondForm
in the browser.
The sample application contains descriptive text on FirstForm and
SecondForm, which I have omitted in the article for brevity.
You can also run the application online at http://www.designerwebsites.com/pitre/samples/SessionmanagerDemo/FirstForm.aspx?WROXEMPTOKEN=WROXEMPTOKENVALUE
Conclusion
You have just seen how to work around the lack of design-time
support for the intrinsic Session object by creating a custom class
to hold your session variables. The class encapsulates all session
variables and related code. It compels you to declare and initialize
all your session variables in one place. It streamlines your code
and helps reduce bugs by providing intellisense and type checking at
design time. Also it does not add a noticeable overhead in doing
so.
The custom class, however, is not the panacea. It does not
prevent you from indiscriminately declaring Session variables
outside of the class. Nor does it stop you from storing huge arrays
or recordsets in the Session. It is merely a tool that helps you use
session variables more effectively. |