Underpinnings of the Session State Implementation in ASP.NET
Dino Esposito
Wintellect
September 2003
Applies to:
Microsoft® ASP.NET
Summary: Discusses the implementation of the session state functions and facilities of ASP.NET 1.1, and how to optimize session state management from within managed Web applications. (13 printed pages)
Contents
Introduction
Overview of ASP.NET Session State
Synchronizing Access to the Session State
Comparing State Providers
State Serialization and Deserialization
Lifetime of a Session
Cookieless Sessions
Summary
Introduction
In a stateless world like that of Web applications, the concept of session state should make no sense at all. In spite of this, effective state management is a requirement for most Web applications. Microsoft® ASP.NET, as well as many other server-side programming environments, provides an abstraction layer that let applications store persistent data on a per-user and per-application basis.
In particular, the session state of a Web application is the data that an application caches and retrieves across different requests. A session represents all the requests sent by a user for the duration of a connection to the site, and the session state is the collection of persistent data that the user generated and used during the session. The state of each session is independent from that of another session and doesn't survive the end of the user session.
The session state has no correspondence to any of the logical entities that make up the HTTP protocol and specification. The session is an abstraction layer built by server-side development environments such classic ASP and ASP.NET. Both the way session state is exposed by ASP.NET, and how it is implemented internally are subject to the underpinnings of the platform. As a result, classic ASP and ASP.NET implement session state in radically different ways, and further changes and enhancements are expected with the next version of ASP.NET.
In this article, I'll examine the plumbing of the session state implementation in ASP.NET 1.1 and ways to optimize session state management from within managed Web applications.
Overview of ASP.NET Session State
The session state is not a native part of the HTTP infrastructure. This means that there should be an architectural component that binds the session state with each incoming request. The runtime environment (be it classic ASP or ASP.NET) accepts a keyword like Session and make it indicate blocks of data stored somewhere on the server. To successfully resolve any call to the Session object, the runtime environment must add the session state to the call context of the request being processed. How this happens is platform-specific, but it is a fundamental step for stateful Web applications.
In classic ASP, the session state is implemented as a free-threaded COM object contained in the asp.dll library. (If you're particularly curious, the CLSID of the object is D97A6DA0-A865-11cf-83AF-00A0C90C2BD8.) This object stores data organized as a collection of name/value pairs. The name placeholder indicates the key to retrieve the information; the value placeholder instead represents the content stored in the session state. Name/value pairs are grouped per session ID so that each user sees only the pairs he or she created.
In ASP.NET, the programming interface of the session state is nearly identical to classic ASP. However, the underlying implementation is radically different and provides for greater flexibility, scalability, and programming power. Before we venture deep into the ASP.NET session-state forest, though, let's briefly review some architectural features of the ASP.NET session infrastructure.
In ASP.NET, any incoming HTTP request passes through a pipeline of HTTP modules. Each module can filter and modify the amount of information carried over by the request. The information associated with each request is known as the call context and is programmatically represented with the HttpContext object. The context of the request should not be considered as yet another container of state information—though it supplies the Items collection, which is just a data container. The HttpContext object is different from all other state objects (that is, Session, Application, and Cache) in that it has a limited lifetime that exceeds the time needed to process the request. As the request passes through the chain of registered HTTP modules, its HttpContext object is endowed with references to state objects. When the request is finally ready for processing, the associated call context is bound to session-specific (Session) and global state objects (Application and Cache).
The HTTP module responsible for setting up the session state for each user is SessionStateModule. Structured after the IHttpModule interface, the module provides a variety of session-state–related services for ASP.NET applications. Services provided include session ID generation, cookieless session management, retrieval of session data from external state providers, and binding of data to the call context of the request.
The HTTP module doesn't store internally the session data. The session state is persisted in external components, named state providers. A state provider completely encapsulates the session-state data and communicates with the rest of the world through the methods of the IStateClientManager interface. The session-state HTTP module calls the methods on the interface to read and save the state of a session. ASP.NET 1.1 supports three different state providers, as listed in Table 1.
Table 1. State client providers
Provider | Description |
---|---|
InProc | Session values are kept as live objects in the memory of the ASP.NET worker process (aspnet_wp.exe or w3wp.exe in Microsoft® Windows Server™ 2003). This is the default option. |
StateServer | Session values are serialized and stored in the memory of a separate process (aspnet_state.exe). The process can also run on another machine. |
SQLServer | Session values are serialized and stored in a Microsoft® SQL Server™ table. The instance of SQL Server can run either locally or remotely. |
The session-state HTTP module reads the currently selected state provider from the <sessionState> section of the web.config file.
<sessionState mode="InProc | StateServer | SQLServer />
According to the value of the mode attribute, the session state is retrieved from, and stored to, different processes and through different procedures. By default, the state of a session is stored locally in the ASP.NET worker process. In particular, it is stored in a private slot (not programmatically accessible) of the ASP.NET Cache object. The session state can also be maintained in an external, even remote, process—a Windows NT service named aspnet_state.exe. The third option is keeping the session state in an ad-hoc database table managed by SQL Server 2000.
The HTTP module deserializes the session values into a dictionary object at the beginning of the request. The dictionary—actually an object of type HttpSessionState—is then made programmatically accessible through the property Session that classes such as HttpContext and Page expose. The binding between the session state values and the session object visible to developers lasts until the end of the request. If the request completes successfully, all state values are serialized back into the state provider and made available to other requests.
The Figure 1 illustrates the communication between requested ASP.NET pages and the session values. The code used by each page interfaces the Session property on the page class. The programming style is nearly identical to classic ASP.
Figure 1. The architecture of session state in ASP.NET 1.1
The physical values of a session state are locked for the time needed to complete a request. The lock is managed internally by the HTTP module and used to synchronize access to the session state.
The session-state module instantiates the state provider for the application and initializes it using the information read out of the web.config file. Next, each provider continues its own initialization, which is quite different depending on the type. For example, the SQL Server state manager opens a connection to the given database, whereas the out-of-process manager checks the specified TCP port. The InProc state manager, on the other hand, stores a reference to the callback function. This is executed when the element is removed from the cache, and is used to fire the Session_OnEnd event to the application.
Synchronizing Access to the Session State
So what really happens when a Web page makes an apparently simple and straightforward call into the Session property? A lot of work occurs in the background of some trivial code like the following:
int siteCount = Convert.ToInt32(Session["Counter"]);
The code actually accesses a local, in-memory copy of the session values that the HTTP module created, reading data from the specific state provider (see Figure 1). What if other pages attempt to concurrently access the session state? In that case, the current request might end up working on inconsistent data, or data that isn't up to date. Just to avoid this, the session state module implements a reader/writer locking mechanism and queues the access to state values. A page that has session-state write access will hold a writer lock on the session until the request terminates.
A page claims write access to the session state by setting the EnableSessionState attribute on the @Page directive to true. (This is the default setting.) A page, though, can also have read-only permissions on the session state, for example, when the EnableSessionState attribute is set to ReadOnly. In this case, the module will hold a reader lock on the session until the request for that page finishes. As a result, concurrent readings can occur.
If a page request sets a reader lock, other concurrently processed requests in the same session cannot update the session state but are at least allowed to read. This means that when a session read-only request is being served, awaiting read-only requests are given higher priority than requests needing a full access. If a page request sets a writer lock on the session state, all other pages are blocked, regardless of whether they have to read or write. For example, if two frames attempt to write to Session, one of them has to wait until the other finishes.
Comparing State Providers
By default, ASP.NET applications store the session state in the memory of the worker process, specifically in a private slot of the Cache object. When the InProc mode is selected, the session state is stored in a slot within the Cache object. This slot is marked as private and is not programmatically accessible. Put another way, if you enumerate all the items in the ASP.NET data cache, no object will be returned that looks like the state of a given session. The Cache object provides for two types of slots—private and public. Programmers are allowed to add and manipulate public slots; the system, specifically classes defined in the system.web assembly, reserves for itself private slots.
The state of each active session occupies a private slot in the cache. The slot is named after the session ID and the value is an instance of an internal, undocumented class named SessionStateItem. The InProc state provider takes the ID of the session and retrieves the corresponding element in the cache. The content of the SessionStateItem object is then poured into the HttpSessionState dictionary object and accessed by applications through the Session property. Notice that a bug in ASP.NET 1.0 makes private slots of the Cache object programmatically enumerable. If you run the following code under ASP.NET 1.0, you'll be able to enumerate items corresponding to objects packed with the state of each currently active session.
foreach(DictionaryEntry elem in Cache) { Response.Write(elem.Key + ": " + elem.Value.ToString()); }
This vulnerability has been closed in ASP.NET 1.1 and no system slot is any longer listed if you enumerate the content of the cache.
The InProc option is by far the fastest possible in terms of access. However, bear in mind that the more data you store in a session, the more memory is consumed on the Web server, which potentially increases the risk of performance hits. If you plan to use any of the out-of-process solutions, the possible impact of serialization and deserialization should be carefully considered. Out-of-process solutions use a Windows NT service (aspnet_state.exe) or a SQL Server table to store the session values. The session state therefore is held outside the ASP.NET worker process and an extra layer of code is needed to serialize and deserialize it to and from the actual storage medium. This operation takes place whenever a request is processed and subsequently must be highly optimized.
The need of copying session data from an external repository into the local session dictionary taxes the request causing a 15 percent (out-of-process) to 25 percent (SQL Server) decrease in performance. Notice, though, that this is only a rough estimate, but it's closer to the minimum impact rather than to the maximum impact. The estimate, in fact, does not fully consider the complexity of the types actually saved into the session state.
In out-of-process storage scenarios, your session state lives longer and makes your application more robust because it is protected against Microsoft® Internet Information Services (IIS) and ASP.NET failures. By separating the session state from the application itself, you can also much more easily scale an existing application to a Web farm and Web garden architecture. In addition, the session state living in an external process eliminates at the root the risk of periodically losing data because of process recycling.
Let's examine the use of a Windows NT service. As mentioned previously, it is a process named aspnet_state.exe and normally resides in the C:\WINNT\Microsoft.NET\Framework\v1.1.4322 folder.
The actual directory depends on the Microsoft® .NET Framework version you're actually running. Before using the state server, you should make sure that the service is up and running on the local or remote machine used as the session store. The state service is a constituent part of ASP.NET and gets installed along with it, so you have no additional setup to run. By default, the state service is stopped and requires a manual start. The ASP.NET application attempts to connect to the session-state server immediately after loading. For this reason, the service must be up and running, otherwise an HTTP exception is thrown. The figure below shows the properties dialog box for the service.
Figure 2. The property dialog box of the ASP.NET state server
An ASP.NET application needs to specify the TCP/IP address of the machine hosting the session-state service. These settings must be entered in the web.config file of the application.
<configuration> <system.web> <sessionState mode="StateServer" stateConnectionString="tcpip=expoware:42424" /> </system.web> </configuration>
The stateConnectionString attribute contains the IP address of the machine and the port to use for data exchange. The default machine address is 127.0.0.1 (local host), while the default port is 42424. You can also indicate the machine by name. Using the local or a remote machine is completely transparent to the code. Note that non-ASCII characters in the name are not supported and the port number is mandatory.
If you use out-of-process session storage, the session state will still be there, ready for further use, whatever happens to the ASP.NET worker process. If the service is paused, the data is preserved and automatically retrieved when the service resumes. However, if the state provider service is stopped or fails, the data is lost. If robustness is key for your application, drop the StateServer mode in favor of SQLServer.
<configuration> <system.web> <sessionState mode="SQLServer" sqlConnectionString="server=127.0.0.1;uid=<user id>;pwd=<password>;" /> </system.web> </configuration>
You specify the connection string through the sqlConnectionString attribute. Notice that the attribute string must include user ID, password, and server name. It cannot contain tokens, such as Database and Initial Catalog, because this information defaults to fixed names. User ID and passwords can be replaced by integrated security settings.
How is the database created? ASP.NET provides two pairs of scripts to configure the database environment. The scripts in the first pair are named InstallSqlState.sql and UninstallSqlState.sql and are located in the same folder as the session state NT service. They create a database called ASPState and several stored procedures. The data, though, is stored in the SQL Server temporary storage area—the TempDB database. This means that the session data is lost if the SQL Server machine is restarted.
To work around this limitation, use the second pair of scripts. The names of the scripts are InstallPersistSqlState.sql and UninstallPersistSqlState.sql. In this case, an ASPState database is created, but its tables are created within the same database and as such are persistent. When installing the SQL Server support for sessions, a job is also created to delete expired sessions from the session-state database. The job is named ASPState_Job_DeleteExpiredSessions and runs every minute. Note that the SQLServerAgent service needs to be running in order for the job to work.
Whatever mode you choose, the way of coding session state actions doesn't change. You always work against the Session property and read and write values as usual. Any behavioral difference is handled at a lower abstraction layer. State serialization is perhaps the most important difference between session modes.
State Serialization and Deserialization
When you use the in-process mode, objects are stored in the session state as live instances of respective classes. No real serialization and deserialization ever takes place, meaning that you can actually store in Session whatever objects you have created (including nonserializable objects and COM objects) and access them with no significant overhead. The situation is different if you opt for an out-of-process state provider.
In an out-of-process architecture, session values are to be copied from the native storage medium (external AppDomain, database) into the memory of the AppDomain that processes the request. A serialization/deserialization layer is needed to accomplish the task and represents one of the major costs for out-of-process state providers. This fact has one main impact on your code—only serializable objects can be stored in the session dictionary.
To perform data serialization and deserialization, ASP.NET uses two methods depending on the types involved. For basic types, ASP.NET resorts to an optimized internal serializer; for other types, including objects and user-defined classes, ASP.NET makes use of the .NET binary formatter. Basic types are String, DateTime, Boolean, byte, char, plus all numeric types. For these types, a tailor-made serializer is used that is faster than the default and general-purpose .NET binary formatter.
The optimized serializer is neither publicly available nor documented. It is nothing more than binary read/writer and employs a simple but effective storage schema. The serializer uses the BinaryWriter class to write out one byte to denote the type and then the value. While reading serialized bytes, the class first extracts one byte, detects the type of the data to read, and then resorts to a type-specific ReadXxx method on the BinaryReader class.
Note that Boolean and numeric types have a well-known size, but the same can't be said for strings. The reader determines the correct size of a string exploiting the fact that on the underlying stream the string is always prefixed with the length, encoded as an integer seven bits at a time. Date values instead are saved by writing only the total number of ticks that form the date. As a result, for the purposes of session serialization, a date equals an Int64 type.
More complex objects (as well as custom objects) are serialized using the BinaryFormatter class as long as the involved classes are marked as serializable. All nonbasic types are identified with the same type ID and are stored in the same stream as basic types. Overall, the performance hit caused by serialization ranges from 15 percent to 25 percent. However, note that this is a rough estimate based on the assumption that basic types are used. The more you use complex types, the more the overhead grows.
Effective session data storage is hard to achieve without extensive use of basic types. So, at least in theory, using three session slots to save three distinct string properties on an object sounds like a better approach than just serializing the object as a whole. But what if the object to serialize has 100 properties? Should you occupy 100 slots or just one? In many cases, an alternative approach is better to convert complex types to an aggregation of simpler types. This approach is based on type converters. A type converter is a sort of lightweight serializer that returns the key properties of a type as a collection of strings. The type converter is an external class bound to a base class using an attribute. The author decides which properties should be saved and how. Type converters are helpful also for ViewState storage and represent a more effective approach for session storage than binary formatters.
Lifetime of a Session
An important point about ASP.NET session management is that the life of a session state object begins only when the first item is added to the in-memory dictionary. Only after executing code like in the following snippet, can an ASP.NET session be considered started.
Session["MySlot"] = "Some data";
The Session dictionary generically contains Object types; to read data back, you need to cast the returned values to a more specific type.
string data = (string) Session["MySlot"];
When a page saves data to Session, the value is loaded into a made-to-measure dictionary class hosted by the HttpSessionState class. The contents of the dictionary is flushed to the state provider when the ongoing request completes. If the session state is empty because no data has been programmatically placed in the dictionary, no data is serialized to the storage medium and, more importantly, no slot is created in either the ASP.NET Cache, SQL Server, or NT state service to track the current session. This is done for performance reasons but has a key repercussion on the way in which the session ID is handled: A new session ID is generated for each request until some data is stored in the session dictionary.
When required to attach the session state to the ongoing request, the HTTP module retrieves the session ID (if it is not the start request) and looks for it in the configured state provider. If no data is returned, the HTTP module generates a new session ID for the request. This can be easily tested through the following page:
<%@ Page Language="C#" Trace="true" %> </html> <body> <form runat="server"> <asp:button runat="server" text="Click" /> </form> </body> </html>
Any time you click on the button and the page posts back, a new session ID is generated, as the trace information documents.
Figure 3. A new session ID is generated for each request in applications that do not store data in the session dictionary.
What about the Session_OnStart event? Is it fired for each request too? If the application defines a Session_OnStart handler, then the session state is always saved, even if empty. As a result, the session ID remains constant for all requests after the first. The bottom line is, use the Session_OnStart handler only if strictly necessary.
The session ID of stateless applications doesn't change with the next access if the session timed out or is abandoned. By design, even though the session state expires, the session ID lasts until the browser session is ended. This means that the same session ID is used to represent multiple sessions over time as long as the browser instance remains the same.
The Session_OnEnd event signals the end of the session and is used to perform any clean-up code needed to terminate the session. Notice, though, that the event is supported only in InProc mode—that is, only when the session data is stored in the ASP.NET worker process. For Session_OnEnd to fire, the session state has to exist first, meaning that you have to store some data in the session state and must have completed at least one request.
In InProc mode, the session state, added as an item to the cache, is given a sliding expiration policy. Sliding expiration means that the item is removed if not used for the specified amount of time. Any request served in the meantime resets the expiration countdown. The time interval for the session-state item is set to the session timeout. The technique used to reset the expiration of session state is pretty simple and intuitive: The session HTTP module just performs a read on the session-state item stored in the ASP.NET Cache. Given the internal structure of the ASP.NET Cache object, this evaluates to renew the sliding period. As a result, when the cache item expires, the session has timed out.
An expired item is automatically removed from the cache. As part of the expiration policy for this item, the state-session module also indicates a remove callback function. The cache automatically invokes the remove function which, in turn, fires the Session_OnEnd event. The ending event is never fired if an application implements session management through out-of-process components.
Cookieless Sessions
Each active ASP.NET session is identified using a 120-bit string made only of URL-allowed characters. The session ID is generated using the Random Number Generator (RNG) cryptographic provider. The service provider returns a sequence of 15 randomly generated numbers (15 bytes x 8 bit = 120 bits). The array of random numbers is then mapped to valid URL characters and returned as a string.
The session ID string is communicated to the browser and then returned to the server application in one of two ways: by using cookies (as in classic ASP) or a modified URL. By default, the session-state module creates an HTTP cookie on the client, but a modified URL can be used—especially for cookieless browsers—with the session ID string embedded. Which approach is taken depends upon the configuration settings stored in the application's web.config file. To configure session settings, you use the <sessionState> section and the cookieless attribute.
<sessionState cookieless="true|false" />
By default, the cookieless attribute is false, meaning that cookies are used. A cookie is really nothing more than a text file placed on the client's hard disk by a Web page. In ASP.NET, a cookie is represented by an instance of the HttpCookie class. Typically, a cookie has a name, a collection of values, and an expiration time. When the cookieless attribute setting is false, the session-state module actually creates a cookie named ASP.NET_SessionId and stores the session ID in it. The cookie is created as the following pseudocode shows:
HttpCookie sessionCookie; sessionCookie = new HttpCookie("ASP.NET_SessionId", sessionID); sessionCookie.Path = "/";
A session cookie is given a very short expiration term and is renewed at the end of each successful request. The cookie's Expires property indicates the time of day on the client at which the cookie expires. If not explicitly set, as is the case with session cookies, the Expires property defaults to DateTime.MinValue—that is, the smallest possible unit of time allowed in the .NET Framework.
To disable session cookies, you set the cookieless attribute to true in the configuration file, as shown here:
<configuration> <system.web> <sessionState cookieless="true" /> </system.web> </configuration>
At this point, suppose that you request a page at the following URL:
https://www.contoso.com/sample.aspx
What is really displayed in the browser's address bar is slightly different and now includes the session ID, as shown here:
https://www.contoso.com/(5ylg0455mrvws1uz5mmaau45)/sample.aspx
When instantiated, the session-state HTTP module checks the value of the cookieless attribute. If true, the request is redirected (HTTP 302) to a modified virtual URL that includes the session ID just before the page name. When processed again, the request embeds the session ID. If the request starts a new session, the HTTP module generates a new session ID and then redirects the request. If the request is a postback, the session ID is already there because postbacks use relative URLs.
The drawback of using cookieless sessions is that the session state is lost if an absolute URL is invoked. When cookies are used, you can clear the address bar, go to another application, and then return to the previous one and retrieve the same session values. If you do this when session cookies are disabled, the session data is lost. For example, the following code breaks the session:
<a runat="server" href="/code/page.aspx">Click</a>
If you need to use absolute URLs, resort to a little trick and manually add the session ID to the URL. You use the ApplyAppPathModifier method on the HttpResponse class.
<a runat="server" href=<% =Response.ApplyAppPathModifier("/code/page.aspx")%> >Click</a>
The ApplyAppPathModifier method takes a string representing a URL and returns an absolute URL, which embeds session information. For example, this trick is especially useful in situations in which you need to redirect from a HTTP page to an HTTPS page.
Summary
Originally introduced by classic ASP, session state is a dictionary-based API that allows developers to store custom data for the duration of a session. In ASP.NET, session state supports two key features: cookieless session ID storage and transmission, and state providers for actual storage of session data. To implement the new features, ASP.NET utilizes an HTTP module to govern the binding of the session state to the context of the ongoing request.
In classic ASP, the use of session state implies the use of cookies. This is no longer true in ASP.NET where an alternative, cookieless schema is possible. Thanks to the action of the HTTP module, any requested URL is mangled to include the session ID and is redirected. The next time, the same module will take care of extracting the session ID from the URL and use it to retrieve any stored state.
The physical state of a session can be stored in three possible ways—in-process memory, out-of-process memory, and in a SQL Server table. The data must undergo a serialization/deserialization process to become usable by the application. The HTTP module copies session values from the provider to the application's memory at the beginning of the request. When the request completes, the modified state is flushed back to the provider. This traffic of data can penalize the performance in variable measure, but greatly enhances reliability and robustness, and makes support for Web farm and Web garden architectures really straightforward.
About the Author
Dino Esposito is a trainer and consultant based in Rome, Italy. Member of the Wintellect team, Dino specializes in ASP.NET and ADO.NET and spends most of his time teaching and consulting across Europe and the United States. In particular, Dino manages the ADO.NET courseware for Wintellect and writes the Cutting Edge column for MSDN Magazine.