Since its beginning, the ASP.NET session state was devised to be an easy-to-customize and extensible feature. For various reasons, in ASP.NET 1.x it came out with a high degree of customization but a total lack of extensibility. In ASP.NET 2.0, the session-state subsystem was refactored to allow developers to replace most of the functionalities a characteristic that is often referred to as session-state pluggability.
All things considered, you have the following three options to customize session-state management:
You can stay with the default session-state module but write a custom state provider to change the storage medium (for example, a non_SQL Server database or a different table layout). In doing so, you also have the chance to override some of the helper classes (mostly collections) that are used to bring data from the store to the Session object and back.
You can stay with the default session state module but replace the session ID generator. But hold on! The algorithm that generates session IDs is a critical element of the application, as making session IDs too easy for attackers to guess can lead straight to session-hijacking attacks. Nonetheless, this remains a customizable aspect of session state that, properly used, can make your application even more secure.
You can unplug the default session-state module and roll your own. Technically possible also in ASP.NET 1.x, this option should be used as a last resort. Obviously, it provides the maximum flexibility, but it is also extremely complicated and is recommended only if strictly necessary and if you know exactly what you're doing. We won't cover this topic in the book.
The first option the easiest and least complicated of all addresses most of the scenarios for which some custom session management is desirable. So let's tackle it first.
A session-state provider is the component in charge of serving any data related to the current session. Invoked when the request needs to acquire state information, it retrieves data from a given storage medium and returns that to the module. Invoked by the module when the request ends, it writes the supplied data to the storage layer. As mentioned, ASP.NET supports three state providers, as listed in Table 13-11.
Name | Class | Storage Medium |
---|---|---|
InProc | InProcSessionStateStore | Stores data as live objects in the ASP.NET Cache. |
StateServer | OutOfProcSessionStateStore | Stores data as serialized objects to the memory of a Windows service named aspnet_state.exe. |
SQLServer | SqlSessionStateStore | Stores data as serialized objects into a SQL Server database. |
In ASP.NET 2.0, you can write your own state-provider class that uses the storage medium of your choice. Note that the default state providers also make use of various helper classes to move data around. In your custom provider, you can replace these classes, too, or just stick to the standard ones.
A state provider (also often referred to as a session-state store) is a class that inherits from SessionStateStoreProviderBase. The main methods of the interface are listed in Table 13-12.
Method | Description |
---|---|
CreateNewStoreData | Creates an object to contain the data of a new session. It should return an object of type SessionStateStoreData. |
CreateUninitializedItem | Creates a new and uninitialized session in the data source. The method is called when an expired session is requested in a cookieless session state. In this case, the module has to generate a new session ID. The session item created by the method prevents the next request with the newly generated session ID from being mistaken for a request directed at an expired session. |
Dispose | Releases all resources (other than memory) used by the state provider. |
EndRequest | Called by the default session-state module when it begins to handle the EndRequest event. |
GetItem | Returns the session item matching the specified ID from the data store. The session item selected is locked for reading. The method serves requests from applications that use read-only session state. |
GetItemExclusive | Returns the session item matching the specified ID from the data store and locks it for writing. Used for requests originated by applications that use read/write session state. |
Initialize | Inherited from base provider class, performs one-off initialization. |
InitializeRequest | Called by the default session-state module when it begins to handle the AcquireRequestState event. |
ReleaseItemExclusive | Unlocks a session item that was previously locked by a call to the GetItemExclusive method. |
RemoveItem | Removes a session item from the data store. Called when a session ends or is abandoned. |
ResetItemTimeout | Resets the expiration time of a session item. Invoked when the application has session support disabled. |
SetAndReleaseItemExclusive | Writes a session item to the data store. |
SetItemExpireCallback | The default module calls this method to notify the data store class that the caller has registered a Session_End handler. |
Classes that inherit the SessionStateStoreProviderBase class work with the default ASP.NET session-state module and replace only the part of it that handles session-state data storage and retrieval. Nothing else in the session functionality changes.
Can two requests for the same session occur concurrently? You bet. Take a look at Figure 13-3. Requests can arrive in parallel for example, from two frames or when a user works with two instances of the same browser, the second of which is opened as a new window. To avoid problems, a state provider must implement a locking mechanism to serialize access to a session. The session-state module determines whether the request requires read-only or read/write access to the session state and calls GetItem or GetItemExclusive accordingly. In the implementation of these methods, the provider's author should create a reader/writer lock mechanism to allow multiple concurrent reads but prevent writing on locked sessions.
Another issue relates to letting the session-state module know when a given session has expired. The session-state module calls the method SetItemExpireCallback when there's a Session_End handler defined in global.asax. Through the method, the state provider receives a callback function with the following prototype:
public delegate void SessionStateItemExpireCallback( string sessionID, SessionStateStoreData item);
It has to store that delegate internally and invoke it whenever the given session times out. Supporting expiration callbacks is optional and, in fact, only the InProc provider actually supports it. If your custom provider is not willing to support expiration callbacks, you should instruct the SetItemExpireCallback method to return false.
Note | A provider that intends to support cookieless sessions must also implement the CreateUninitialized method to write a blank session item to the data store. More precisely, a blank session item means an item that is complete in every way except that it contains no session data. In other words, the session item should contain the session ID, creation date, and perhaps lock IDs, but no data. ASP.NET 2.0 generates a new ID (in cookieless mode only) whenever a request is made for an expired session. The session-state module generates the new session ID and redirects the browser. Without an uninitialized session item marked with a newly generated ID, the new request will again be recognized as a request for an expired session. |
SessionStateStoreData is the class that represents the session item that is, a data structure that contains all the data that is relevant to the session. GetItem and GetItemExclusive, in fact, are defined to return an instance of this class. The class has three properties: Items, StaticObjects, and Timeout.
Items indicates the collection of name/values that will ultimately be passed to the page through the Session property. StaticObjects lists the static objects belonging to the session, such as objects declared in the global.asax file and scoped to the session. As the name suggests, Timeout indicates how long, in minutes, the session-state item is valid. The default value is 20 minutes.
Once the session-state module has acquired the session state for the request, it flushes the contents of the Items collection to a new instance of the HttpSessionStateContainer class. This object is then passed to the constructor of the HttpSessionState class and becomes the data container behind the familiar Session property.
The SessionStateStoreData class is used in the definition of the base state provider class, meaning that you can't entirely replace it. If you don't like it, you can inherit a new class from it, however. To both the session module and state provider, the container of the session items is merely a class that implements the ISessionStateItemCollection interface. The real class being used by default is SessionStateItemCollection. You can replace this class with your own as long as you implement the aforementioned interface.
Tip | To write a state provider, you might find helpful the methods of the SessionStateUtility class. The class contains methods to serialize and deserialize session items to and from the storage medium. Likewise, the class has methods to extract the dictionary of data for a session and add it to the HTTP context and the Session property. |
To make a custom session-state provider available to an application, you need to register it in the web.config file. Suppose you have called the provider class SampleSessionStateProvider and compiled it to MyLib. Here's what you need to enter:
<system.web> <sessionState mode="Custom" customProvider="SampleSessionProvider"> <providers> <add name="SampleSessionProvider" type="SampleSessionStateProvider, MyLib" /> </providers> </sessionState> </system.web>
The name of the provider is arbitrary but necessary. To force the session-state module to find it, set the mode attribute to Custom.
To generate the session ID, ASP.NET 2.0 uses a special component named SessionIDManager. Technically speaking, the class is neither an HTTP module nor a provider. More simply, it is a class that inherits from System.Object and implements the ISessionIDManager interface. You can replace this component with a custom component as long as the component implements the same ISessionIDManager interface. To help you decide whether you really need a custom session-ID generator, let's review some facts about the default module.
The default session-ID module generates a session ID as an array of bytes with a cryptographically strong random sequence of 15 values. The array is then encoded to a string of 24 URL-accepted characters, which is what the system will recognize as the session ID.
The session ID can be round-tripped to the client in either an HTTP cookie or a mangled URL, based on the value of the cookieless attribute in the <sessionState> configuration section. Note that when cookieless sessions are used, the session-ID module is responsible for adding the ID to the URL and redirecting the browser. The default generator redirects the browser to a fake URL like the following one:
http://www.contoso.com/test/(S(session_id))/page.aspx
In ASP.NET 1.x, the fake URL is slightly different and doesn't include the S( ) delimiters. How can a request for this fake URL be served correctly? In the case of a cookieless session, the session-ID module depends on a small and simple ISAPI filter (aspnet_filter.dll, which is also available to ASP.NET 1.x) to dynamically rewrite the real URL to access. The request is served correctly, but the path on the address bar doesn't change. The detected session ID is placed in a request header named AspFilterSessionId.
Now that we've ascertained that a session-ID manager is a class that implements ISessionIDManager, you have two options: build a new class and implement the interface from the ground up, or inherit a new class from SessionIDManager and override a couple of virtual methods to apply some personalization. The first option offers maximum flexibility; the second is simpler and quicker to implement, and it addresses the most compelling reason you might have to build a custom session-ID generator supply your own session-ID values.
Let's start by reviewing the methods of the ISessionIDManager interface, which are shown in Table 13-13.
Method | Description |
---|---|
CreateSessionID | Virtual method. It creates a unique session identifier for the session. |
Decode | Decodes the session ID using HttpUtility.UrlDecode. |
Encode | Encodes the session ID using HttpUtility.UrlEncode. |
Initialize | Invoked by the session state immediately after instantiation; performs one-time initialization of the component. |
InitializeRequest | Invoked by the session state when the session state is being acquired for the request. |
GetSessionID | Gets the session ID from the current HTTP request. |
RemoveSessionID | Deletes the session ID from the cookie or from the URL. |
SaveSessionID | Saves a newly created session ID to the HTTP response. |
Validate | Confirms that the session ID is valid. |
If you plan to roll your own completely custom session-ID generator, bear in mind the following points:
The algorithm you choose for ID generation is critical. If you don't implement strong cryptographic randomness, a malicious user can guess a valid session ID when the same session is still active, thus accessing some user's data. (This is known as session hijacking.) A good example of a custom session-ID algorithm is one that returns a globally unique identifier (GUID).
You can choose whether to support cookieless sessions. If you do, you have to endow the component with the ability to extract the session ID from the HTTP request and redirect the browser. You probably need an ISAPI filter or HTTP module to preprocess the request and enter appropriate changes. The algorithm you use to store session IDs without cookies is up to you.
If you are absolutely determined to have the system use your session IDs, you derive a new class from SessionIDManager and override two methods: CreateSessionID and Validate. The former returns a string that contains the session ID. The latter validates a given session ID to ensure that it conforms to the specification you set. Once you have created a custom sessionID module, you register it in the configuration file. Here's how to do it:
<sessionState sessionIDManagerType="Samples.MyIDManager, MyLib" /> </sessionState>
State management is a necessary evil. By enabling it, you charge your application with an extra burden. The September 2005 issue of MSDN Magazine contains an article with the ASP.NET team's coding best practices to reduce the performance impact of session state on Web applications.
The first guideline is disabling session state whenever possible. However, to prevent the session from expiring, the HTTP module still marks the session as active in the data store. For out-of-process state servers, this means that a roundtrip is made. A custom session-ID manager returning a null session ID for requests that are known not to require session state is the best way to work around this issue and avoid the overhead entirely. (Write a class that inherits from SessionIDManager and override GetSessionID.)
The second guideline entails minimizing contention on session data by avoiding frames and downloadable resources served by session-enabled handlers.
The third guideline regards data serialization and deserialization. You should always use simple types and break complex classes to arrays of simple properties at least as far as session management is concerned. In other words, I'm not suggesting that you factor out your DAL classes just the way you serialize them into the session store. An alternate approach entails building a custom serialization algorithm that is optimized for sessionstate storage. Breaking a class into various properties each stored in a session slot is advantageous not only because of the simple types being used, but also because the extreme granularity of the solution minimizes the data to save in case of changes. If one property changes, only one slot with a simple type is updated instead of a single slot with a complex type.