Session State Providers

 

Microsoft ASP.NET 2.0 Providers: Introduction
Membership Providers
Role Providers
Site Map Providers
Session State Providers
Profile Providers
Web Event Providers
Web Parts Personalization Providers

Introduction

Session state providers provide the interface between Microsoft ASP.NET's session state module and session state data sources. ASP.NET 2.0 ships with three session state providers:

  • InProcSessionStateStore, which stores session state in memory in the ASP.NET worker process
  • OutOfProcSessionStateStore, which stores session state in memory in an external state server process
  • SqlSessionStateStore, which stores session state in Microsoft SQL Server and Microsoft SQL Server Express databases

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 timeout (in minutes)

Items and StaticObjects can be serialized and deserialized easily enough, by calling their Serialize and Deserialize methods, respectively. The Timeout property is a simple System.Int32, and it is therefore also easily serialized and deserialized.

The .NET Framework's System.Web.SessionState namespace includes a class named SessionStateStoreProviderBase that defines 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);
}

Three of the most important methods in a session state provider are GetItem, GetItemExclusive, and SetAndReleaseItemExclusive. The first two are called by SessionStateModule to retrieve a session from the data source. If the requested page implements the IRequiresSessionState interface (by default, all pages implement IRequiresSessionState), SessionStateModule's AcquireRequestState event handler calls the session state provider's GetItemExclusive method. The word "Exclusive" in the method name means that the session should be retrieved only if it's not currently being used by another request. If, on the other hand, the requested page implements the IReadOnlySessionState interface (the most common way to achieve this is to include an EnableSessionState="ReadOnly" attribute in the page's @ Page directive), SessionStateModule calls the provider's GetItem method. No exclusivity is required here, because overlapping read accesses are permitted by SessionStateModule.

In order to provide the exclusivity required by GetItemExclusive, a session state provider must implement a locking mechanism that prevents a given session from being accessed by two or more concurrent requests requiring read/write access to session state. That mechanism ensures the consistency of session state, by preventing concurrent requests from overwriting each other's changes. The locking mechanism must work even if the session state data source is a remote resource shared by several Web servers.

SessionStateModule reads sessions from the data source at the outset of each request, by calling GetItem or GetItemExclusive from its AcquireRequestState handler. At the end of the request, SessionStateModule's ReleaseRequestState handler calls the session state provider's SetAndReleaseItemExclusive method to commit changes to the data source, and release locks held by GetItemExclusive. A related method named ReleaseItemExclusive exists so that SessionStateModule can time out a locked session by commanding the session state provider to release the lock.

The following section documents the implementation of SqlSessionStateStore, which derives from SessionStateStoreProviderBase.

SqlSessionStateStore

SqlSessionStateStore is the Microsoft session state provider for SQL Server databases. It stores session data using the schema documented in "Data Schema," and it uses the stored procedures documented in "Data Access.". All knowledge of the database schema is hidden in the stored procedures, so that porting SqlSessionStateStore to other database types requires little more than modifying the stored procedures. (Depending on the targeted database type, the ADO.NET code used to call the stored procedures might have to change, too. The Microsoft Oracle .NET provider, for example, uses a different syntax for named parameters.)

SqlSessionStateStore is alone among SQL providers, in that it doesn't use the provider database. Instead, it uses a separate session state database whose name (by default) is ASPState. The session state database can be created by running Aspnet_regsql.exe with an -ssadd switch. The session state database can be stored either in tempdb (which will not survive a server restart), or in a custom database (for example, ASPState) that will survive a server restart. Both persistence options can be exercised using Aspnet_regsql.exe's –sstype switch.

SqlSessionStateStore supports a key scalability feature of ASP.NET 2.0 known as session state partitioning. By default, all sessions for all applications are stored in a single SQL Server database. However, developers can implement custom partition resolvers—classes that implement the IPartitionResolver interface—to partition sessions into multiple databases. Partition resolvers convert session IDs into database connection strings; before accessing the session state database, SqlSessionStateStore calls into the active partition resolver to get the connection string it needs. One use for custom partition resolvers is to divide session state for one application into two or more databases. Session state partitioning helps ASP.NET applications scale out horizontally, by eliminating the bottleneck of a single session state database. For an excellent overview of how partitioning works, and how to write custom partition resolvers, see "Fast, Scalable, and Secure Session State Management for Your Web Applications."

The ultimate reference for SqlSessionStateStore is the SqlSessionStateStore source code, which is found in SqlStateClientManager.cs. The sections that follow highlight key aspects of SqlSessionStateStore's design and operation.

Provider Initialization

Initialization occurs in SqlSessionStateStore.Initialize, which is called when the provider is loaded by SessionStateModule. The version of Initialize that's called isn't the one inherited from ProviderBase, but a special one defined in SessionStateStoreProviderBase that receives an IPartitionResolver parameter. If a custom partition resolver is registered (registration is accomplished by including a partitionResolverType attribute in the <sessionState> configuration element), that parameter references a custom partition resolver. Otherwise, it's set to null, indicating the absence of a custom partition resolver. SqlSessionStateStore caches the reference (null or not) in a private field named _partitionResolver.

The Initialize method called by SessionStateModule then calls the Initialize method inherited from ProviderBase. This method calls base.Initialize, and then delegates to a private helper method named OneTimeInit. If _partitionResolver is null, OneTimeInit creates a SqlPartitionInfo object, and caches a reference to it in a private static field. (SqlPartitionInfo essentially wraps a database connection string, and is used when the connection string used to access the session state database won't change over time.) However, if _partitionResolver is not null, then OneTimeInit sets a static private field named s_usePartition to true, creates a new PartitionManager object (note that this is an internal type) to encapsulate the partition resolver, and stores a reference to the PartitionManager in another private static field. Whenever SqlSessionStateStore needs to access the session state database, it calls the helper method GetConnection, which retrieves a connection targeting the proper session state database, using the partition resolver, if present, to acquire the connection. GetConnection also includes pooling logic, allowing session state database connections to be pooled when circumstances permit.

Initialize's final task is to register a handler for the DomainUnload event that fires when the host application domain unloads. The handler—OnAppDomainUnload—performs cleanup duties by calling Dispose on the SqlPartitionInfo or PartitionManager object.

Data Schema

SqlSessionStateStore stores session data in the ASPStateTempSessions table of the session state database. Each record in ASPStateTempSessions holds the serialized session state for one session, and the auxiliary data that accompanies that session. Table 5-1 documents the ASPStateTempSessions table's schema.

Table 5-1. The ASPStateTempSessions table

Column Name Column Type Description
SessionId nvarchar(88) Session ID + application ID
Created datetime Date and time session was created (UTC)
Expires datetime Date and time session expires (UTC)
LockDate datetime UTC date and time session was locked
LockDateLocal datetime Local date and time session was locked
LockCookie int Lock ID
Timeout int Session timeout in minutes
Locked bit 1=Session locked, 0=Session not locked
SessionItemShort varbinary(7000) Serialized session state (if <= 7,000 bytes)
SessionItemLong image Serialized session state (if > 7,000 bytes)
Flags int Session state flags (1=Uninitialized session)

Serialized session state is stored in binary form in the SessionItemShort and SessionItemLong fields. Sessions that serialize to a length of 7,000 bytes or less are stored in SessionItemShort, whereas sessions that serialize to a length of more than 7,000 bytes are stored in SessionItemLong. (Storing "short" sessions in a varbinary field offers a performance advantage, because the data can be stored in the table row, rather than externally in other data pages.) The Expires field is used to clean up expired sessions, as described in "Session Expiration." The Locked, LockDate, LockDateLocal, and LockCookie fields are used to lock concurrent accesses to a session. SqlSessionStateStore's locking strategy is described in "Serializing Concurrent Accesses to a Session."

In addition to scoping data by user, SqlSessionStateStore scopes data by application, so that multiple applications can store sessions in one session state database. To that end, the session state database contains an ASPStateTempApplications table that records application names and application IDs. Application names are not explicitly specified as they are for other providers; instead, SqlSessionStateStore uses the website's IIS metabase path as the application name. Application IDs are hashes generated from application names by the stored procedure GetHashCode. (SqlSessionStateStore differs in this respect, too, from other SQL providers, which use randomly generated GUIDs as application IDs.) Table 5-2 documents the schema of the ASPStateTempApplications table.

Table 5-2. The ASPStateTempApplications table

Column Name Column Type Description
AppId int Application ID
AppName char(280) Application name

Curiously, the ASPStateTempSessions table lacks an AppId column linking it to ASPTempStateApplications. The linkage occurs in ASPStateTempSessions's SessionId field, which doesn't store just session IDs. It stores session IDs with application IDs appended to them. The statement

cmd.Parameters[0].Value = id + _partitionInfo.AppSuffix; // @id

in SqlSessionStateStore.DoGet (discussed in "Reading Sessions from the Database") is one example of how SqlSessionStateStore generates the session ID values input to database queries.

Data Access

SqlSessionStateStore performs all database accesses through stored procedures. Table 5-3 lists the stored procedures that it uses.

Table 5-3. Stored procedures used by SqlSessionStateStore

Stored Procedure Description
CreateTempTables Creates the ASPStateTempSessions and ASPStateTempApplications tables; called during setup, but not called by SqlSessionStateStore.
DeleteExpiredSessions Used by SQL Server Agent to remove expired sessions.
GetHashCode Hashes an application name and returns the hash; called by TempGetAppID.
GetMajorVersion Returns SQL Server's major version number.
TempGetAppID Converts an application name into an application ID; queries the ASPStateTempApplications table and inserts a new record if necessary.
TempGetStateItem Retrieves read-only session state from the database (ASP.NET 1.0; ASP.NET 1.1/SQL Server 7).
TempGetStateItem2 Retrieves read-only session state from the database (ASP.NET 1.1).
TempGetStateItem3 Retrieves read-only session state from the database (ASP.NET 2.0).
TempGetStateItemExclusive Retrieves read/write session state from the database (ASP.NET 1.0; ASP.NET 1.1/SQL Server 7).
TempGetStateItemExclusive2 Retrieves read/write session state from the database (ASP.NET 1.1).
TempGetStateItemExclusive3 Retrieves read/write session state from the database (ASP.NET 2.0).
TempGetVersion Marker whose presence indicates to ASP.NET 2.0 that the session state database is ASP.NET 2.0-compatible.
TempInsertStateItemLong Adds a new session, whose size is > 7,000 bytes, to the database.
TempInsertStateItemShort Adds a new session, whose size is <= 7,000 bytes, to the database.
TempInsertUninitializedItem Adds a new uninitialized session to the database in support of cookieless sessions.
TempReleaseStateItemExclusive Releases a lock on a session; called when ASP.NET determines that a request has timed out and calls the provider's ReleaseItemExclusive method.
TempRemoveStateItem Removes a session from the database when the session is abandoned.
TempResetTimeout Resets a session's timeout by writing the current date and time to the corresponding record's Expires field.
TempUpdateStateItemLong Updates a session whose size is > 7,000 bytes.
TempUpdateStateItemLongNullShort Updates a session whose old size is <= 7,000 bytes, but whose new size is > 7,000 bytes.
TempUpdateStateItemShort Updates a session whose size is <= 7,000 bytes.
TempUpdateStateItemShortNullLong Updates a session whose old size is > 7,000 bytes, but whose new size is <= 7,000 bytes.

Some of the stored procedures exist in different versions (for example, TempGetStateItem, TempGetStateItem2, and TempGetStateItem3), in order to support different versions of ASP.NET and different versions of SQL Server. ASP.NET 2.0 uses the "3" versions of these stored procedures, and it never calls the old versions. The older stored procedures are retained to allow ASP.NET 1.1 servers to use the same session state database as ASP.NET 2.0.

Note Many of the stored procedures used by SqlSessionStateStore have "Temp" in their names for historical reasons. In ASP.NET 1.0, SQL Server session state was always stored in tempdb, and "Temp" in a stored procedure name indicated that the stored procedure targeted the tempdb database. ASP.NET's reliance on tempdb changed in version 1.1, when InstallPersistSqlState.sql appeared, offering administrators the option of storing SQL Server session state in a conventional database. The names of the stored procedures remained the same, so that one source code base could target both temporary and persistent session state databases.

Reading Sessions from the Database

To retrieve a session from the session state data source, SessionStateModule calls the default session state provider's GetItem or GetItemExclusive method. The former is called for pages that implement the IReadOnlySessionState interface (pages that read session state, but do not write it), whereas the latter is called for pages that implement IRequiresSessionState (indicating that they both read and write session state). Both GetItem and GetItemExclusive delegate to a private helper method named DoGet. GetItem passes false in DoGet's third parameter, indicating that exclusivity is not required. GetItemExclusive passes in true.

SqlSessionStateStore.DoGet retrieves the session from the database, or returns locking information if the session is locked because it's being used by a concurrent request. It begins by calling GetConnection to get a connection to the session state database. Then, it calls one of two stored procedures: TempGetStateItem3 if the third parameter passed to DoGet is false (that is, if DoGet was called by GetItem), or TempGetStateItemExclusive3 if the third parameter is true (if DoGet was called by GetItemExclusive).

What happens when the stored procedure returns depends on whether the requested session is currently locked. If the session isn't locked, DoGet extracts the serialized session state from the SessionItemShort or SessionItemLong field, deserializes the session state into a SessionStateStoreData object, and returns it. However, if the session is locked, DoGet returns null, but uses the out parameters locked and lockAge to inform SessionStateModule that the session is locked and how long the lock has been active. Lock age is used by SessionStateModule to forcibly release a lock if the lock is held for too long.

Note How SqlSessionStateStore computes a lock's age depends on the version of SQL Server it's running against. For SQL Server 2000 and higher, SqlSessionStateStore uses T-SQL's DATEDIFF to compute the lock age in SQL Server. For SQL Server 7, SqlSessionStateStore reads the lock date from the database and subtracts it from DateTime.Now to compute the lock's age. The downside to the DateTime.Now approach is that lock age is computed incorrectly if the Web server and database server are in different time zones. It also introduces daylight saving time issues that can adversely affect lock age computations.

Writing Sessions to the Database

To save a session to the session state data source, SessionStateModule calls the default session state provider's SetAndReleaseItemExclusive method. SqlSessionStateStore.SetAndReleaseItemExclusive serializes the SessionStateStoreData passed to it, taking care to call SqlSessionStateStore.ReleaseItemExclusive to release the lock (if any) on the session if the serialization attempt fails.

Next, SqlSessionStateStore.SetAndReleaseItemExclusive calls GetConnection to get a database connection. Then, it checks the newItem parameter passed to it, to determine whether the session being saved is a new session or an existing session. If newItem is true, indicating that the session has no corresponding row in the database, SetAndReleaseItemExclusive calls the stored procedure TempInsertStateItemShort or TempInsertStateItemLong (depending on the size of the serialized session) to record the session in a new row in the session state database. If newItem is false, indicating that the database already contains a row representing the session, SetAndReleaseItemExclusive calls one of the following stored procedures to update that row:

  • TempUpdateStateItemShort if the serialized session contains 7,000 or fewer bytes, and if it formerly contained 7,000 or fewer bytes also
  • TempUpdateStateItemLong if the serialized session contains more than 7,000 bytes, and if it formerly contained more than 7,000 bytes also
  • TempUpdateStateItemLongNullShort if the serialized session contains more than 7,000 bytes, but it formerly contained 7,000 or fewer bytes
  • TempUpdateStateItemShortNullLong if the serialized session contains 7,000 or fewer bytes, but it formerly contained more than 7,000 bytes

The "Null" versions of these stored procedures nullify the field containing the old session data before recording new session data in SessionItemShort or SessionItemLong.

Serializing Concurrent Accesses to a Session

SqlSessionStateStore employs a locking strategy that relies on fields in the session state database and the stored procedures that access them. The following is a synopsis of how it works.

When SessionStateModule calls GetItem to retrieve a session from the database, SqlSessionStateStore calls the stored procedure TempGetStateItem3. The stored procedure does no locking of its own, but checks the Locked field of the corresponding record before deciding what to return. If Locked is 0, indicating that the session isn't locked, TempGetStateItem3 returns the serialized session data through the itemShort output parameter if the session is stored in the SessionItemShort field, or as a query result if the session is stored in the SessionItemLong field. If Locked is not 0, however, TempGetStateItem3 returns no session state. Instead, it uses the output parameters named locked, lockAge, and lockCookie, to return a nonzero value indicating that the session is locked, the lock age, and the lock ID, respectively. SessionStateModule responds by retrying the call to GetItem at half-second intervals until the lock is removed.

How does a record become locked in the first place? That happens in TempGetStateItemExclusive3, which is called when SessionStateModule calls SqlSessionStateStore's GetItemExclusive method. If called to retrieve a session that's already locked (Locked=1), TempGetStateItemExclusive3 behaves much like TempGetStateItem3. But, if called to retrieve a session that isn't locked (Locked=0), TempGetStateItemExclusive3 sets LockDate and LockDateLocal to the current time, returns 0 through the locked parameter—indicating that the session wasn't locked when the read occurred—and returns the serialized session. It also sets the record's Locked field to 1, effectively locking the session and preventing subsequent calls to TempGetStateItem3 or TempGetStateItemExclusive3 from returning sessions until Locked is reset to 0.

Locked is reset to 0 by all of the stored procedures called by SqlSessionStateStore's SetAndReleaseItemExclusive method to write a session to the database. TempUpdateStateItemShort, reproduced in Listing 5-1, is one example. A session can also be unlocked with the stored procedure TempReleaseStateItemExclusive, which is called by SqlSessionStateStore's ReleaseItemExclusive method. SessionStateModule calls that method to forcibly release a lock if repeated attempts to retrieve the session don't succeed. Listing 5-2 shows the relevant code in SessionStateModule.

Listing 5-1. TempUpdateStateItemShort

CREATE PROCEDURE [dbo].[TempUpdateStateItemShort]
    @id         tSessionId,
    @itemShort  tSessionItemShort,
    @timeout    int,
    @lockCookie int
AS    
    UPDATE [ASPState].dbo.ASPStateTempSessions
    SET Expires = DATEADD(n, Timeout, GETUTCDATE()), 
        SessionItemShort = @itemShort, 
        Timeout = @timeout,
        Locked = 0 /* Unlock the session! */
    WHERE SessionId = @id AND LockCookie = @lockCookie
    RETURN 0                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            

Listing 5-2. SessionStateModule code for timing out locks

if (_rqReadonly) {
    _rqItem = _store.GetItem(_rqContext, _rqId, out locked,
        out lockAge, out _rqLockId, out _rqActionFlags);
}
else {
   _rqItem = _store.GetItemExclusive(_rqContext, _rqId, out locked,
       out lockAge, out _rqLockId, out _rqActionFlags);
}

// We didn't get it because it's locked....
if (_rqItem == null && locked) {
    if (lockAge >= _rqExecutionTimeout) {
        /* Release the lock on the item, which is held
        by another thread*/
        _store.ReleaseItemExclusive(_rqContext, _rqId, _rqLockId);
    }
    isCompleted = false;
    PollLockedSession();
}

Supporting Cookieless Sessions

ASP.NET supports two different types of sessions: cookied and cookieless. The terms "cookied" and "cookieless" refer to the mechanism used to round-trip session IDs between clients and Web servers. Cookied sessions round-trip session IDs in HTTP cookies, whereas 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 after the redirect. (Otherwise, SessionStateModule would think that the ID extracted from the URL after 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.

SqlSessionStateStore supports cookied and cookieless sessions. Its CreateUninitializedItem method calls TempInsertUninitializedItem, which adds a row to the session state database, and flags it as an uninitialized session by setting the Flags field to 1. The flag is reset to 0 when the session is retrieved from the database by TempGetStateItem3 or TempGetStateItemExclusive3, following a redirect.

Session Expiration

Each session created by ASP.NET has a timeout value (by default, 20 minutes) associated with it. If no accesses to the session occur within the session timeout, the session is deemed to be expired, and it is no longer valid.

SqlSessionStateStore uses the Expires field of the ASPStateTempSessions table to record the date and time that each session expires. All stored procedures that read or write a session set the Expires field equal to the current date and time plus the session timeout, effectively extending the session's lifetime for another full timeout period.

SqlSessionStateStore doesn't actively monitor the Expires field. Instead, it relies on an external agent to scavenge the database and delete expired sessions—sessions whose Expires field holds a date and time less than the current date and time. The ASPState database includes a SQL Server Agent job that periodically (by default, every 60 seconds) calls the stored procedure DeleteExpiredSessions to remove expired sessions. DeleteExpiredSessions uses the following simple DELETE statement to delete all qualifying rows from the ASPStateTempSessions table:

DELETE [ASPState].dbo.ASPStateTempSessions WHERE Expires < @now

SessionStateModule doesn't fire Session_End events when SqlSessionStateStore is the default session state provider, because SqlSessionStateStore doesn't notify it when a session expires.

If an application abandons a session by calling Session.Abandon, SessionStateModule calls the provider's RemoveItem method. SqlSessionStateStore.RemoveItem calls the stored procedure TempRemoveStateItem to delete the session from the database.

Differences Between the Published Source Code and the .NET Framework's SqlSessionStateStore

The published source code for SqlSessionStateStore differs from the .NET Framework version in the following respects:

  • Some imperative CAS checks were commented out. Because the source code can be compiled standalone, and thus will run as user code rather than trusted code in the global assembly cache, the CAS checks are not necessary.
  • Calls to performance counters were commented out, because the .NET Framework provider relies on internal helper classes for manipulating these counters. For reference, the original code has been retained as comments.
  • The published version does not support the use of multiple database partitions, because the .NET Framework provider uses a number of internal classes to implement this functionality. However, the published version contains commented code, so that you can see how the .NET Framework provider supports multiple database partitions.
  • Some internal helper methods used by the .NET Framework provider for serializing and deserializing session data have been cloned in the published provider.

Return to part 4, Site Map Providers.

Go on to part 6, Profile Providers.

© Microsoft Corporation. All rights reserved.