Session State Providers

 

Introduction to the Provider Model
Membership Providers
Role Providers
Site Map Providers
Session State Providers
Profile Providers
Web Event Providers
Web Parts Personalization Providers
Custom Provider-Based Services
Hands-on Custom Providers: The Contoso Times

Session state providers provide the interface between ASP.NET session state and session state data sources. The two most common reasons for writing a custom session state provider are:

  • You wish to store session state in a data source that is not supported by the session state providers included with the .NET Framework, such as an Oracle database.
  • You wish to store session state in a SQL Server database whose schema differs from that of the database used by System.Web.SessionState.SqlSessionStateStore.

Core ASP.NET session state services are provided by System.Web.SessionState.SessionStateModule, instances of which are referred to as session state modules. Session state modules encapsulate session state in instances of System.Web.SessionState.SessionStateStoreData, allocating one SessionStateStoreData per session (per user). The fundamental job of a session state provider is to serialize SessionStateDataStores to session state data sources and deserialize them on demand. SessionStateDataStore has three properties that must be serialized in order to hydrate class instances:

  • Items, which encapsulates a session's non-static objects
  • StaticObjects, which encapsulates a session's static objects
  • Timeout, which specifies the session's time-out (in minutes)

Items and StaticObjects can be serialized and deserialized easily enough by calling their Serialize and Deserialize methods. The Timeout property is a simple System.Int32 and is therefore also easily serialized and deserialized. Thus, if sssd is a reference to an instance of SessionStateStoreData, the core logic to write the contents of a session to a session state data source is often no more complicated than this:

stream1 = new MemoryStream ();
stream2 = new MemoryStream ();
writer1 = new BinaryWriter (stream1);
writer2 = new BinaryWriter (stream2);

// Serialize Items and StaticObjects
((SessionStateItemCollection) sssd.Items).Serialize (writer1);
sssd.StaticObjects.Serialize (writer2);

// Convert serialized items into byte arrays
byte[] items = stream1.ToArray ();
byte[] statics = stream2.ToArray ();
int timeout = sssd.Timeout;

// TODO: Write items, statics, and timeout to the data source

One of the challenges to writing a session state provider is implementing a locking mechanism that prevents a given session from being accessed by two or more concurrent requests. That mechanism ensures the consistency of session state data by preventing one request from reading a session at the same time that another request is writing it. The locking mechanism must work even if the session state data source is a remote resource shared by several Web servers. Locking behavior is discussed in Synchronizing Concurrent Accesses to a Session.

Another consideration to take into account when designing a session state provider is whether to support expiration callbacks notifying SessionStateModule when sessions time out. Expiration callbacks are discussed in Expiration Callbacks and Session_End Events.

The SessionStateStoreProviderBase Class

Developers writing custom session state providers begin by deriving from System.Web.SessionState.SessionStateStoreProviderBase, which derives from ProviderBase and adds abstract methods defining the basic characteristics of a session state provider. SessionStateStoreProviderBase is prototyped as follows:

public abstract class SessionStateStoreProviderBase : ProviderBase
{
    public abstract void Dispose();

    public abstract bool SetItemExpireCallback
        (SessionStateItemExpireCallback expireCallback);

    public abstract void InitializeRequest(HttpContext context);

    public abstract SessionStateStoreData GetItem
        (HttpContext context, String id, out bool locked,
        out TimeSpan lockAge, out object lockId,
        out SessionStateActions actions);

    public abstract SessionStateStoreData GetItemExclusive
        (HttpContext context, String id, out bool locked,
        out TimeSpan lockAge, out object lockId,
        out SessionStateActions actions);

    public abstract void ReleaseItemExclusive(HttpContext context, 
        String id, object lockId);

    public abstract void SetAndReleaseItemExclusive
        (HttpContext context, String id, SessionStateStoreData item, 
        object lockId, bool newItem);

    public abstract void RemoveItem(HttpContext context, 
        String id, object lockId, SessionStateStoreData item);

    public abstract void ResetItemTimeout(HttpContext context,
        String id);

    public abstract SessionStateStoreData CreateNewStoreData
        (HttpContext context, int timeout);

    public abstract void CreateUninitializedItem
        (HttpContext context, String id, int timeout);

    public abstract void EndRequest(HttpContext context);
}

The following table describes SessionStateStoreProviderBase's methods and provides helpful notes regarding their implementation:

Method Description
CreateNewStoreData Called to create a SessionStateStoreData for a new session.
CreateUninitializedItem Called to create a new, uninitialized session in the data source. Called by SessionStateModule when session state is cookieless to prevent the session from being unrecognized following a redirect.
Dispose Called when the provider is disposed of to afford it the opportunity to perform any last-chance cleanup.
EndRequest Called in response to EndRequest events to afford the provider the opportunity to perform any per-request cleanup.
GetItem If session state is read-only (that is, if the requested page implements IReadOnlySessionState), called to load the SessionStateStoreData corresponding to the specified session ID and apply a read lock so other requests can read the SessionStateDataStore but not modify it until the lock is released.

If the specifed session doesn't exist in the data source, this method returns null (Nothing in Visual Basic) and sets the out parameter named locked to false. SessionStateModule then calls CreateNewStoreData to create a new SessionStateStoreData to serve this session.

If the specified session is currently locked, this method returns null (Nothing in Visual Basic) and sets the out parameter named locked to true, causing SessionStateModule to retry GetItem at half-second intervals until the lock comes free or times out. In addition to setting locked to true, this method also returns the lock age and lock ID using the lockAge and lockId parameters.

GetItemExclusive If session state is read-write (that is, if the requested page implements IRequiresSessionState), called to load the SessionStateStoreData corresponding to the specified session ID and apply a write lock so other requests can neither read nor write the SessionStateStoreData until the lock is released.

If the specifed session doesn't exist in the data source, this method returns null (Nothing in Visual Basic) and sets the out parameter named locked to false. SessionStateModule then calls CreateNewStoreData to create a new SessionStateStoreData to serve this session.

If the specified session is currently locked, this method returns null (Nothing in Visual Basic) and sets the out parameter named locked to true, causing SessionStateModule to retry GetItemExclusive at half-second intervals until the lock comes free or times out. In addition to setting locked to true, this method also returns the lock age and lock ID using the lockAge and lockId parameters.

InitializeRequest Called in response to AcquireRequestState events to afford the provider the opportunity to perform any per-request initialization.
ReleaseItemExclusive Called to unlock the specified session if a request times out waiting for the lock to come free.
RemoveItem Called to remove the specified session from the data source.
ResetItemTimeout Called to reset the expiration time of the specified session.
SetAndReleaseItemExclusive Called to write modified session state to the data source. The newItem parameter indicates whether the supplied SessionStateStoreData corresponds to an existing session in the data source or a new one. If newItem is true, SetAndReleaseItemExclusive adds a new session to the data source. Otherwise, it updates an existing one.
SetItemExpireCallback Called to supply the provider with a callback method for notifying SessionStateModule that a session has expired. If the provider supports session expiration, it should return true from this method and notify ASP.NET when sessions expire by calling the supplied callback method. If the provider does not support session expiration, it should return false from this method.

Your job in implementing a custom session state provider in a derived class is to override and provide implementations of SessionStateStoreProviderBase's abstract members, and optionally to override key virtuals such as Initialize.

Synchronizing Concurrent Accesses to a Session

ASP.NET applications are inherently multithreaded. Because requests that arrive in parallel are processed on concurrent threads drawn from a thread pool, it's possible that two or more requests targeting the same session will execute at the same time. (The classic example is when a page contains two frames, each targeting a different ASPX in the same application, causing the browser to submit overlapping requests for the two pages.) To avoid data collisions and erratic behavior, the provider "locks" the session when it begins processing the first request, causing other requests targeting the same session to wait for the lock to come free.

Because there's no harm in allowing concurrent requests to perform overlapping reads, the lock is typically implemented as a reader/writer lock-that is, one that allows any number of threads to read a session but that prevents overlapping reads and writes as well as overlapping writes.

Which brings up two very important questions:

  1. How does a session state provider know when to apply a lock?
  2. How does the provider know whether to treat a request as a reader or a writer?

If the requested page implements the IRequiresSessionState interface (by default, all pages implement IRequiresSessionState), ASP.NET assumes that the page requires read/write access to session state. In response to the AcquireRequestState event fired from the pipeline, SessionStateModule calls the session state provider's GetItemExclusive method. If the targeted session isn't already locked, GetItemExclusive applies a write lock and returns the requested data along with a lock ID (a value that uniquely identifies the lock). However, if the session is locked when GetItemExclusive is called, indicating that another request targeting the same session is currently executing, GetItemExclusive returns null and uses the out parameters passed to it to return the lock ID and the lock's age (how long, in seconds, the session has been locked).

If the requested page implements the IReadOnlySessionState interface instead, ASP.NET assumes that the page reads but does not write session state. (The most common way to implement IReadOnlySessionState is to include an EnableSessionState="ReadOnly" attribute in the page's @ Page directive.) Rather than call the provider's GetItemExclusive method to retrieve the requestor's session state, ASP.NET calls GetItem instead. If the targeted session isn't locked by a writer when GetItem is called, GetItem applies a read lock and returns the requested data. (The read lock ensures that if a read/write request arrives while the current request is executing, it waits for the lock to come free. This prevents read/write requests from overlapping with read requests that are already executing.) Otherwise, GetItem returns null and uses the out parameters passed to it to return the lock ID and the lock's age-just like GetItemExclusive.

The third possiblity—that the page implements neither IRequiresSessionState nor IReadOnlySessionState—tells ASP.NET that the page doesn't use session state, in which case SessionStateModule calls neither GetItem nor GetItemExclusive. The most common way to indicate that a page should implement neither interface is to include an EnableSessionState="false" attribute in the page's @ Page directive.

If SessionStateModule encounters a locked session when it calls GetItem or GetItemExclusive (that is, if either method returns null), it rerequests the data at half-second intervals until the lock is released or the request times out. If a time-out occurs, SessionStateModule calls the provider's ReleaseItemExclusive method to release the lock and allow the session to be accessed.

SessionStateModule identifies locks using the lock IDs returned by GetItem and GetItemExclusive. When SessionStateModule calls SetAndReleaseItemExclusive or ReleaseItemExclusive, it passes in a lock ID. Due to the possibility that a call to ReleaseItemExclusive on one thread could free a lock just before another thread calls SetAndReleaseItemExclusive, the SetAndReleaseItemExclusive method of a provider that supports multiple locks IDs per session should only write the session to the data source if the lock ID input to it matches the lock ID in the data source.

Expiration Callbacks and Session_End Events

After loading a session state provider, SessionStateModule calls the provider's SetItemExpireCallback method, passing in a SessionStateItemExpireCallback delegate that enables the provider to notify SessionStateModule when a session times out. If the provider supports expiration callbacks, it should save the delegate and return true from SetItemExpireCallback. Then, whenever a session times out, the provider should notify SessionStateModule that a session has expired by calling the callback method encapsulated in the delegate. This enables SessionStateModule to fire Session_End events when sessions expire.

Session state providers aren't required to support expiration callbacks. A provider that doesn't support them should return false from SetItemExpireCallback. In that case, SessionStateModule will not be notified when a session expires and will not fire Session_End events.

Inside the ASP.NET Team
How do the built-in session state providers handle expiration callbacks? The in-process session state provider, InProcSessionStateStore, stores session content in the ASP.NET application cache and takes advantage of the cache's sliding-expiration feature to expire sessions when they go for a specified period of time without being accessed. When the provider is notified via a cache removal callback that a session expired from the cache, it notifies SessionStateModule, and SessionStateModule fires a Session_End event.

The other two built-in providers—OutOfProcSessionStateStore and SqlSessionStateStore—don't support expiration callbacks. Both return false from SetItemExpireCallback. OutOfProcSessionStateStore uses the application cache to store sessions, but since session data is stored in a remote process (the "state server" process), the provider doesn't attempt to notify SessionStateModule when a session expires. SqlSessionStateStore relies on a SQL Server agent to "scavenge" the session state database and clean up expired sessions. Having the agent notify the provider about expired sessions so the provider could, in turn, notify SessionStateModule would be a tricky endeavor indeed-especially in a Web farm.

Cookieless Sessions

ASP.NET supports two different types of sessions: cookied and cookieless. The term actually refers to the mechanism used to round-trip session IDs between clients and Web servers and does not imply any difference in the sessions themselves. Cookied sessions round-trip session IDs in HTTP cookies, while cookieless sessions embed session IDs in URLs using a technique known as "URL munging."

In order to support cookieless sessions, a session state provider must implement a CreateUninitializedItem method that creates an uninitialized session. When a request arrives and session state is configured with the default settings for cookieless mode (for example, when the <sessionState> configuration element contains cookieless="UseUri" and regenerateExpiredSessionId="true" attributes), SessionStateModule creates a new session ID, munges it onto the URL, and passes it to CreateUninitializedItem. Afterwards, a redirect occurs with the munged URL as the target. The purpose of calling CreateUninitializedItem is to allow the session ID to be recognized as a valid ID following the redirect. (Otherwise, SessionStateModule would think that the ID extracted from the URL following the redirect represents an expired session, in which case it would generate a new session ID, which would force another redirect and result in an endless loop.) If sessions are cookied rather than cookieless, the provider's CreateUninitializedItem method is never called. When testing a custom session state provider, be certain to test it in both cookied and cookieless mode.

A CreateUninitializedItem implementation can use any technique it desires to ensure that the session ID passed to it is recognized as a valid ID following a redirect. ASP.NET's InProcSessionStateStore provider, for example, inserts an empty SessionStateStoreData (that is, a SessionStateStoreData object with null Items and StaticObjects properties) into the application cache accompanied by a flag marking it as an uninitialized session. SqlSessionStateStore acts similarly, adding a row representing the session to the session state database and flagging it as an uninitialized session.

When a session state provider's GetItem or GetItemExclusive method is called, it returns a SessionStateActions value through the out parameter named actions. The value returned depends on the state of the session identified by the supplied ID. If the data corresponding to the session doesn't exist in the data source or if it exists and is already initialized (that is, if the session was not created by CreateUninitializedItem), GetItem and GetItemExclusive should return SessionStateActions.None through the actions parameter. However, if the session data exists in the data source but is not initialized (indicating the session was created by CreateUninitializedItem), GetItem and GetItemExclusive should return SessionStateActions.InitializeItem. SessionStateModule responds to a SessionStateActions.InitializeItem flag by firing a Session_Start event signifying the start of a new session. It also raises a Session_Start event if GetItem or GetItemExclusive returns SessionStateActions.None following the creation of a new session.

TextFileSessionStateProvider

Listing 1 contains the source code for a SessionStateStoreProviderBase-derivative named TextFileSessionStateProvider that demonstrates the basics of custom session state providers. TextFileSessionStateProvider stores session state in text files named SessionID_Session.txt in the application's ~/App_Data/Session_Data directory, where SessionID is the ID of the corresponding session. Each file contains the state for a specific session and consists of either two or four lines of text:

  • A "0" or "1" indicating whether the session is initialized, where "1" means the session is initialized, and "0" means it is not
  • If line 1 contains a "1", a base-64 string containing the session's serialized non-static objects
  • If line 1 contains a "1", a base-64 string containing the session's serialized static objects
  • A numeric string specifying the session's time-out in minutes

Thus, a file containing an initialized session contains four lines of text, and a file containing an uninitialized session-that is, a session created by CreateUninitializedItem in support of cookieless session state-contains two. You must create the ~/App_Data/Session_Data directory before using the provider; the provider doesn't attempt to create the directory if it doesn't exist. In addition, the provider must have read/write access to the ~/App_Data/Session_Data directory.

Listing 1. TextFileSessionStateProvider

using System;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.SessionState;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Configuration.Provider;
using System.Security.Permissions;
using System.Web.Hosting;
using System.IO;

public class TextFileSessionStateProvider :
    SessionStateStoreProviderBase
{
    private Dictionary<string, FileStream> _sessions =
        new Dictionary<string, FileStream> ();
    
    public override void Initialize(string name,
        NameValueCollection config)
    {
        // Verify that config isn't null
        if (config == null)
            throw new ArgumentNullException("config");

        // Assign the provider a default name if it doesn't have one
        if (String.IsNullOrEmpty(name))
            name = "TextFileSessionStateProvider";

        // Add a default "description" attribute to config if the
        // attribute doesn't exist or is empty
        if (string.IsNullOrEmpty(config["description"]))
        {
            config.Remove("description");
            config.Add("description",
                "Text file session state provider");
        }

        // Call the base class's Initialize method
        base.Initialize(name, config);

        // Throw an exception if unrecognized attributes remain
        if (config.Count > 0)
        {
            string attr = config.GetKey(0);
            if (!String.IsNullOrEmpty(attr))
                throw new ProviderException
                    ("Unrecognized attribute: " + attr);
        }

        // Make sure we can read and write files in the
        // ~/App_Data/Session_Data directory
        FileIOPermission permission =
            new FileIOPermission(FileIOPermissionAccess.AllAccess,
            HttpContext.Current.Server.MapPath(
                            "~/App_Data/Session_Data"));
        permission.Demand();
    }

    public override SessionStateStoreData
        CreateNewStoreData(HttpContext context, int timeout)
    {
        return new SessionStateStoreData(
            new SessionStateItemCollection(),
            SessionStateUtility.GetSessionStaticObjects(context),
            timeout
        );
    }

    public override void CreateUninitializedItem(HttpContext context,
        string id, int timeout)
    {
        // Create a file containing an uninitialized flag
        // and a time-out
        StreamWriter writer = null;

        try
        {
            writer = new StreamWriter(context.Server.MapPath
                (GetSessionFileName(id)));
            writer.WriteLine("0");
            writer.WriteLine(timeout.ToString());
        }
        finally
        {
            if (writer != null)
                writer.Close();
        }
    }

    public override SessionStateStoreData GetItem(HttpContext context,
        string id, out bool locked, out TimeSpan lockAge,
        out object lockId, out SessionStateActions actions)
    {
        return GetSession(context, id, out locked, out lockAge,
            out lockId, out actions, false);
    }

    public override SessionStateStoreData
        GetItemExclusive(HttpContext context, string id,
        out bool locked, out TimeSpan lockAge, out object lockId,
        out SessionStateActions actions)
    {
        return GetSession(context, id, out locked, out lockAge,
            out lockId, out actions, true);
    }

    public override void
        SetAndReleaseItemExclusive(HttpContext context, string id,
        SessionStateStoreData item, object lockId, bool newItem)
    {
        // Serialize the session
        byte[] items, statics;
        SerializeSession(item, out items, out statics);
        string serializedItems = Convert.ToBase64String(items);
        string serializedStatics = Convert.ToBase64String(statics);

        // Get a FileStream representing the session state file
        FileStream stream = null;

        try
        {
            if (newItem)
                stream = File.Create(context.Server.MapPath
                    (GetSessionFileName (id)));
            else
            {
                stream = _sessions[id];
                stream.SetLength(0);
                stream.Seek(0, SeekOrigin.Begin);
            }

            // Write session state to the file        
            StreamWriter writer = null;

            try
            {
                writer = new StreamWriter(stream);
                writer.WriteLine("1"); // Initialized flag
                writer.WriteLine(serializedItems);
                writer.WriteLine(serializedStatics);
                writer.WriteLine(item.Timeout.ToString());
            }
            finally
            {
                if (writer != null)
                    writer.Close();
            }
        }
        finally
        {
            if (newItem && stream != null)
                stream.Close();
        }
        
        // Unlock the session
        ReleaseItemExclusive(context, id, lockId);
    }

    public override void ReleaseItemExclusive(HttpContext context,
        string id, object lockId)
    {
        // Release the specified session by closing the corresponding
        // FileStream and deleting the lock file
        FileStream stream;

        if (_sessions.TryGetValue (id, out stream))
        {
            _sessions.Remove(id);
            ReleaseLock(context, (string) lockId);
            stream.Close();
        }
    }

    public override void ResetItemTimeout(HttpContext context,
        string id)
    {
        // Update the time stamp on the session state file
        string path = context.Server.MapPath(GetSessionFileName(id));
        File.SetCreationTime(path, DateTime.Now);
    }

    public override void RemoveItem(HttpContext context, string id,
        object lockId, SessionStateStoreData item)
    {
        // Make sure the session is unlocked
        ReleaseItemExclusive(context, id, lockId);

        // Delete the session state file
        File.Delete(context.Server.MapPath(GetSessionFileName(id)));
    }

    public override bool SetItemExpireCallback
        (SessionStateItemExpireCallback expireCallback)
    {
        // This provider doesn't support expiration callbacks,
        // so simply return false here
        return false;
    }

    public override void InitializeRequest(HttpContext context)
    {
    }

    public override void EndRequest(HttpContext context)
    {
    }

    public override void Dispose()
    {
        // Make sure no session state files are left open
        foreach (KeyValuePair<string, FileStream> pair in _sessions)
        {
            pair.Value.Close();
            _sessions.Remove(pair.Key);
        }

        // Delete session files and lock files
        File.Delete (HostingEnvironment.MapPath
            ("~/App_Data/Session_Data/*_Session.txt"));
        File.Delete (HostingEnvironment.MapPath
            ("~/App_Data/Session_Data/*_Lock.txt"));    }

    // Helper methods
    private SessionStateStoreData GetSession(HttpContext context,
        string id, out bool locked, out TimeSpan lockAge,
        out object lockId, out SessionStateActions actions,
        bool exclusive)
    {
        // Assign default values to out parameters
        locked = false;
        lockId = null;
        lockAge = TimeSpan.Zero;
        actions = SessionStateActions.None;

        FileStream stream = null;
        
        try
        {
            // Attempt to open the session state file
            string path =
                context.Server.MapPath(GetSessionFileName(id));
            FileAccess access = exclusive ?
                FileAccess.ReadWrite : FileAccess.Read;
            FileShare share = exclusive ?
                FileShare.None : FileShare.Read;
            stream = File.Open(path, FileMode.Open, access, share);
        }
        catch (FileNotFoundException)
        {
            // Not an error if file doesn't exist
            return null;
        }
        catch (IOException)
        {
            // If we come here, the session is locked because
            // the file couldn't be opened
            locked = true;
            lockId = id;
            lockAge = GetLockAge(context, id);
            return null;
        }

        // Place a lock on the session
        CreateLock(context, id);
        locked = true;
        lockId = id;

        // Save the FileStream reference so it can be used later
        _sessions.Add(id, stream);

        // Find out whether the session is initialized
        StreamReader reader = new StreamReader(stream);
        string flag = reader.ReadLine ();
        bool initialized = (flag == "1");

        if (!initialized)
        {
            // Return an empty SessionStateStoreData
            actions = SessionStateActions.InitializeItem;
            int timeout = Convert.ToInt32(reader.ReadLine ());

            return new SessionStateStoreData(
                new SessionStateItemCollection (),
                SessionStateUtility.GetSessionStaticObjects (context),
                timeout
            );
        }
        else
        {
            // Read Items, StaticObjects, and Timeout from the file
            // (NOTE: Don't close the StreamReader, because doing so
            // will close the file)
            byte[] items = Convert.FromBase64String(reader.ReadLine());
            byte[] statics =
                Convert.FromBase64String(reader.ReadLine());
            int timeout = Convert.ToInt32(reader.ReadLine());

            // Deserialize the session
            return DeserializeSession(items, statics, timeout);
        }
    }

    private void CreateLock(HttpContext context, string id)
    {
        // Create a lock file so the lock's age can be determined
        File.Create(context.Server.MapPath
            (GetLockFileName(id))).Close();
    }

    private void ReleaseLock(HttpContext context, string id)
    {
        // Delete the lock file
        string path = context.Server.MapPath(GetLockFileName(id));
        if (File.Exists (path))
            File.Delete(path);
    }

    private TimeSpan GetLockAge(HttpContext context, string id)
    {
        try
        {
            return DateTime.Now –                File.GetCreationTime(context.Server.MapPath
                (GetLockFileName(id)));
        }
        catch (FileNotFoundException)
        {
            // This is important, because it's possible that
            // a lock is active but the lock file hasn't been
            // created yet if another thread owns the lock
            return TimeSpan.Zero;
        }
    }


    string GetSessionFileName(string id)
    {
        return String.Format("~/App_Data/Session_Data/{0}_Session.txt",
                             id);
    }

    string GetLockFileName(string id)
    {
        return String.Format("~/App_Data/Session_Data/{0}_Lock.txt",
                             id);
    }

    private void SerializeSession(SessionStateStoreData store,
        out byte[] items, out byte[] statics)
    {
        MemoryStream stream1 = null, stream2 = null;
        BinaryWriter writer1 = null, writer2 = null;

        try
        {
            stream1 = new MemoryStream();
            stream2 = new MemoryStream();
            writer1 = new BinaryWriter(stream1);
            writer2 = new BinaryWriter(stream2);

            ((SessionStateItemCollection)
                store.Items).Serialize(writer1);
            store.StaticObjects.Serialize(writer2);

            items = stream1.ToArray();
            statics = stream2.ToArray();
        }
        finally
        {
            if (writer2 != null)
                writer2.Close();
            if (writer1 != null)
                writer1.Close();
            if (stream2 != null)
                stream2.Close();
            if (stream1 != null)
                stream1.Close();
        }
    }

    private SessionStateStoreData DeserializeSession(byte[] items,
        byte[] statics, int timeout)
    {
        MemoryStream stream1 = null, stream2 = null;
        BinaryReader reader1 = null, reader2 = null;

        try
        {
            stream1 = new MemoryStream(items);
            stream2 = new MemoryStream(statics);
            reader1 = new BinaryReader(stream1);
            reader2 = new BinaryReader(stream2);

            return new SessionStateStoreData(
                SessionStateItemCollection.Deserialize(reader1),
                HttpStaticObjectsCollection.Deserialize(reader2),
                timeout
            );
        }
        finally
        {
            if (reader2 != null)
                reader2.Close();
            if (reader1 != null)
                reader1.Close();
            if (stream2 != null)
                stream2.Close();
            if (stream1 != null)
                stream1.Close();
        }
    }
}

TextFileSessionStateProvider's locking strategy is built around file-sharing modes. GetItemExclusive attempts to open the session state file with a FileShare.None flag, giving it exclusive access to the file. GetItem, however, attempts to open the session state file with a FileShare.Read flag, allowing other readers to access the file, but not writers. So that GetItem and GetItemExclusive can return the lock's age if the session state file is locked when they're called, a 0-byte "lock file" named SessionID_Lock.txt is created in the ~/App_Data/Session_Data directory when a session state file is successfully opened. GetItem and GetItemExclusive compute a lock's age by subtracting the lock file's creation time from the current time. ReleaseItemExclusive and SetAndReleaseItemExclusive release a lock by closing the session state file and deleting the corresponding lock file.

TextFileSessionStateProvider takes a simple approach to lock IDs. When it creates a lock, it assigns the lock a lock ID that equals the session ID. That's sufficient because the nature of the locks managed by TextFileSessionStateProvider is such that a given session never has more than one lock applied to it. This behavior is consistent with that of SqlSessionStateStore, which also uses one lock ID per given session.

Listing 2 demonstrates how to make TextFileSessionStateProvider the default session state provider. It assumes that TextFileSessionStateProvider is implemented in an assembly named CustomProviders. Note the syntactical differences between session state providers and other provider types. The <sessionState> element uses a customProvider attribute rather than a defaultProvider attribute to designate the default provider, and the customProvider attribute is ignored unless a mode="Custom" attribute is included, too.

Listing 2. Web.config file making TextFileSessionStateProvider the default session state provider

<configuration>
  <system.web>
    <sessionState mode="Custom"
      customProvider="TextFileSessionStateProvider">
      <providers>
        <add name="TextFileSessionStateProvider"
          type="TextFileSessionStateProvider" />
      </providers>
    </sessionState>
  </system.web>
</configuration>

TextFileSessionStateProvider is fully capable of reading and writing any session state generated by application code. It does not, however, support expiration callbacks. In fact, the session state files that it generates don't get cleaned up until the provider's Dispose method is called, which normally occurs when the application is shut down and the AppDomain is unloaded.

Click here to continue on to part 5, Profile Providers

© Microsoft Corporation. All rights reserved.