Profile 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

Profile providers provide the interface between ASP.NET's profile service and profile data sources. The two most common reasons for writing a custom profile provider are:

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

The fundamental job of a profile provider is to write profile property values supplied by ASP.NET to persistent profile data sources, and to read the property values back from the data source when requested by ASP.NET. Profile providers also implement methods that allows consumers to manage profile data sources-for example, to delete profiles that haven't been accessed since a specified date.

The ProfileProvider Class

Developers writing custom profile providers begin by deriving from System.Web.Profile.ProfileProvider.ProfileProvider derives from System.Configuration.SettingsProvider, which in turn derives from ProviderBase. Together, SettingsProvider and ProfileProvider define the mustoverride class methods and properties that a derived class must implement in order to serve as an intermediary between the profile service and profile data sources. ProfileProvider is prototyped as follows:

Public MustInherit Class ProfileProvider 
Inherits SettingsProvider
  
  Public MustOverride Function DeleteProfiles( _
   ByVal profiles As ProfileInfoCollection) As Integer

  Public MustOverride Function DeleteProfiles( _
   ByVal usernames As String()) As Integer

  Public MustOverride Function DeleteInactiveProfiles( _
   ByVal authenticationOption As ProfileAuthenticationOption, _
   ByVal userInactiveSinceDate As DateTime) As Integer

  Public MustOverride Function GetNumberOfInactiveProfiles( _
   ByVal authenticationOption As ProfileAuthenticationOption, _
   ByVal userInactiveSinceDate As DateTime) As Integer

  Public MustOverride Function GetAllProfiles( _
   ByVal authenticationOption As ProfileAuthenticationOption, _
   ByVal pageIndex As Integer, ByVal pageSize As Integer, _
   ByRef totalRecords As Integer) As ProfileInfoCollection

  Public MustOverride Function GetAllInactiveProfiles( _
   ByVal authenticationOption As ProfileAuthenticationOption, _
   ByVal userInactiveSinceDate As DateTime, _
   ByVal pageIndex As Integer, ByVal pageSize As Integer, _
   ByRef totalRecords As Integer) As ProfileInfoCollection

  Public MustOverride Function FindProfilesByUserName( _
   ByVal authenticationOption As ProfileAuthenticationOption, _
   ByVal usernameToMatch As String, _
   ByVal pageIndex As Integer, _
   ByVal pageSize As Integer, _
   ByRef totalRecords As Integer) As ProfileInfoCollection

  Public MustOverride Function FindInactiveProfilesByUserName( _
   ByVal authenticationOption As ProfileAuthenticationOption, _
   ByVal usernameToMatch As String, _
   ByVal userInactiveSinceDate As DateTime, _
   ByVal pageIndex As Integer, ByVal pageSize As Integer, _
   ByRef totalRecords As Integer) As ProfileInfoCollection
End Class

A ProfileProvider-derived class must also implement the mustoverride methods and properties defined in System.Configuration.SettingsProvider, which is prototyped as follows:

Public MustInherit Class SettingsProvider 
Inherits ProviderBase
  
  ' Properties
  Public MustOverride Property ApplicationName() As String

  ' Methods
  Public MustOverride Function GetPropertyValues( _
     ByVal context As SettingsContext, _
   ByVal properties As SettingsPropertyCollection _
   ) As SettingsPropertyValueCollection

  Public MustOverride Sub SetPropertyValues( _
   ByVal context As SettingsContext, _
   ByVal properties As SettingsPropertyValueCollection)
End Class

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

Method or Property Description
ApplicationName The name of the application using the profile provider. ApplicationName is used to scope profile data so that applications can choose whether to share profile data with other applications. This property can be read and written.
GetPropertyValues Reads profile property values from the data source and returns them in a SettingsPropertyValueCollection. See GetPropertyValues for details.
SetPropertyValues Writes profile property values to the data source. The values are provided by ASP.NET in a SettingsPropertyValueCollection. See SetPropertyValues for details.
DeleteProfiles (ProfileInfoCollection) Deletes the specified profiles from the data source.
DeleteProfiles (string[]) Deletes the specified users' profiles from the data source.
DeleteInactiveProfiles Deletes all inactive profiles-profiles that haven't been accessed since the specified date-from the data source.
GetNumberOfInactiveProfiles Returns the number of profiles that haven't been accessed since the specified date.
GetAllProfiles Returns a collection of ProfileInfo objects containing administrative information about all profiles, including user names and last activity dates.
GetAllInactiveProfiles Returns a collection of ProfileInfo objects containing administrative information regarding profiles that haven't been accessed since the specified date.
FindProfilesByUserName Returns a collection of ProfileInfo objects containing administrative information regarding profiles whose user names match a specified pattern.
FindInactiveProfilesByUserName Returns a collection of ProfileInfo objects containing administrative information regarding profiles whose user names match a specified pattern and that haven't been accessed since the specified date.

Scoping of Profile Data

Profile data is inherently scoped by user name so that profile data can be maintained independently for each user. When storing profile data, a provider must take care to key the data by user name so it can be retrieved using the same key later. For anonymous users, profile providers use anonymous user IDs rather than user names to key profile properties. The user names passed to profile provider methods are in fact anonymous user IDs for users who are not authenticated.

In addition, all profile providers inherit from SettingsProvider a property named ApplicationName whose purpose it to scope the data managed by the provider. Applications that specify the same ApplicationName when configuring the profile service share profile data; applications that specify unique ApplicationNames do not. In addition to associating profiles with user names or anonymous user IDs (profiles are, after all, a means for storing per-user data), profile-provider implementations must associate profiles with application names so operations performed on profile data sources can be scoped accordingly.

As an example, a provider that stores profile data in a SQL database might use a command similar to the following to retrieve profile data for the user named "Jeff" and the application named "Contoso:"

SELECT * FROM Profiles
WHERE UserName='Jeff' AND ApplicationName='Contoso'

The AND in the WHERE clause ensures that other applications containing profiles keyed by the same user name don't conflict with the "Contoso" application.

GetPropertyValues

The two most important methods in a profile provider are the GetPropertyValues and SetPropertyValues methods inherited from SettingsProvider. These methods are called by ASP.NET to read property values from the data source and write them back. Other profile provider methods play a lesser role by performing administrative functions such as enumerating and deleting profiles.

When code that executes within a request reads a profile property, ASP.NET calls the default profile provider's GetPropertyValues method. The context parameter passed to GetPropertyValues is a dictionary of key/value pairs containing information about the context in which GetPropertyValues was called. It contains the following keys:

  • UserName—User name or user ID of the profile to read
  • IsAuthenticated—Indicates whether the requestor is authenticated

The properties parameter contains a collection of SettingsProperty objects representing the property values ASP.NET is requesting. Each object in the collection represents one of the properties defined in the <profile> configuration section. GetPropertyValues' job is to return a SettingsPropertyValuesCollection supplying values for the properties in the SettingsPropertyCollection. If the property values have been persisted before, then GetPropertyValues can retrieve the values from the data source. Otherwise, it can return a SettingsPropertyValuesCollection that instructs ASP.NET to assign default values.

As an example, suppose the <profile> configuration section is defined this way:

<profile>
  <properties>
    <add name="Greeting" type="String" />
    <add name="Count" type="Int32" defaultValue="0" />
  </properties>
</profile>

Each time GetPropertyValues is called, the SettingsPropertyCollection passed to it contains two SettingsProperty objects: one representing the Greeting property, the other representing the Count property. The first time GetPropertyValues is called, the provider can simply do this since the property values haven't yet been persisted in the data source:

Dim settings As SettingsPropertyValueCollection =
    new SettingsPropertyValueCollection ()

For Each [property] As SettingsProperty In properties
   settings.Add (New SettingsPropertyValue ([property]))
Next [property]
Return settings

The returned SettingsPropertyValueCollection contains two SettingsPropertyValues: one representing the Greeting property's property value, and the other representing the Count property's property value. Moreover, because PropertyValue and SerializedValue are set to Nothing in the SettingsPropertyValue objects and Deserialized is set to false, ASP.NET assigns each property a default value (which come from the properties' defaultValue attributes if present.)

The second time GetPropertyValues is called, it retrieves the property values from the data source (assuming the properties were persisted there in the call to SetPropertyValues that followed the previous call to GetPropertyValues). Once more, its job is to return a SettingsPropertyValueCollection containing property values. This time, however, GetPropertyValues has a choice of ways to communicate property values to ASP.NET:

  • It can set the corresponding SettingsPropertyValue object's PropertyValue property equal to the actual property value and the object's Deserialized property to true. ASP.NET will retrieve the property value from PropertyValue. This is useful for primitive types that do not require serialization. It's also useful for explicitly assigning null values to reference types by setting PropertyValue to null and Deserialized to true.
  • It can set the corresponding SettingsPropertyValue object's SerializedValue property equal to the serialized property value and the object's Deserialized property to false. ASP.NET will deserialize SerializedValue to obtain the actual property value, using the serialization type specified in the SettingsProperty object's SerializeAs property. This is useful for complex types that require serialization. The provider typically doesn't do the serialization itself; rather, it reads the serialized property value that was persisted in the data source by SetPropertyValues.
Inside the ASP.NET Team
Another reason for providing serialized data to ASP.NET via the SerializedValue property is that there is no guarantee the calling code that triggered the call to GetPropertyValues is actually interested in all the profile properties. Providing data through SerializedValue allows for lazy deserialization by SettingsBase. If you have ten properties being retrieved by the profile provider, and the calling code on a page only uses one of these properties, then nine of properties don't have to be deserialized, resulting in a potentially significant performance win.

Thus, GetPropertyValues might perform its duties this way the second time around:

Private settings As SettingsPropertyValueCollection = _
  New SettingsPropertyValueCollection ()

For Each [property] As SettingsProperty In properties
  ' Create a SettingsPropertyValue
  Dim pp As SettingsPropertyValue = New SettingsPropertyValue ([property])

  ' Read a persisted property value from the data source
  Dim val As Object = GetPropertyValueFromDataSource ([property].Name)

  ' If val is null, set the property value to null
  If val Is Nothing Then
   pp.PropertyValue = Nothing
   pp.Deserialized = True
   pp.IsDirty = False

      ' If val is not null, set the property value to a non-null value
  Else
   ' TODO: Set pp.PropertyValue to the property value and
   ' pp.Deserialized to true, or set pp.SerializedValue to
   ' the serialized property value and Deserialized to false.
   ' Which strategy you choose depends on which was written
   ' to the data source: PropertyValue or SerializedValue.
  End If

  ' Add the SettingsPropertyValue to the collection
  settings.Add (pp)
Next [property]

' Return the collection
Return settings

SetPropertyValues

SetPropertyValues is the counterpart to GetPropertyValues. It's called by ASP.NET to persist property values in the profile data source. Like GetPropertyValues, it's passed a SettingsContext object containing a user name (or ID) and a Boolean indicating whether the user is authenticated. It's also passed a SettingsPropertyValueCollection containing the property values to be persisted. The format in which the data is persisted-and the physical storage medium that it's persisted in-is up to the provider. Obviously, the format in which SetPropertyValues persists profile data must be understood by the provider's GetProfileProperties method.

SetPropertyValues' job is to iterate through the supplied SettingsPropertyValue objects and write each property value to the data source where GetPropertyValues can retrieve it later on. Where SetPropertyValues obtains the property values from depends on the Deserialized properties of the corresponding SettingsPropertyValue objects:

  • If Deserialized is true, SetPropertyValues can obtain the property value directly from the SettingsPropertyValue object's PropertyValue property.
  • If Deserialized is false, SetPropertyValues can obtain the property value, in serialized form, from the SettingsPropertyValue object's SerializedValue property. There's no need for the provider to attempt to deserialize the serialized property value; it can treat the serialized property value as an opaque entity and write it to the data source. Later, GetPropertyValues can fetch the serialized property value from the data source and return it to ASP.NET in a SettingsPropertyValue object whose SerializedValue property holds the serialized property value and whose Deserialized property is false.

A profile provider's SetPropertyValues method might therefore be structured like this:

For Each [property] As SettingsPropertyValue In properties
  ' Get information about the user who owns the profile
  Dim username As String = CStr(context("UserName"))
  Dim authenticated As Boolean = CBool(context("IsAuthenticated"))

  ' Ignore this property if the user is anonymous and
  ' the property’s AllowAnonymous property is false
  If (Not authenticated) AndAlso _
   (Not CBool(property_Renamed.Property.Attributes( _
"AllowAnonymous"))) Then
   Continue For
  End If

  ' Otherwise persist the property value
  If [property].Deserialized Then
   ' TODO: Write property.PropertyValue to the data source
  Else
    ' TODO: Write property.SerializedValue to the data source
 End If
Next [property]

Alternatively, SetPropertyValues could ignore PropertyValue and simply write SerializedValue to the data source, regardless of whether Deserialized is true or false. The GetPropertyValues implementation would read SerializedValue from the data source and return it in a SettingsPropertyValue object's SerializedValue property with Deserialized set to false. ASP.NET would then compute the actual property value. This is the approach taken by ASP.NET's SqlProfileProvider provider, which only stores serialized property values in the profile database.

The example above doesn't persist a property value if the user isn't authenticated and the property isn't attributed to allow anonymous users. It assumes that if the property appears in a SettingsPropertyCollection passed to GetPropertyValues, GetPropertyValues will see that the property value isn't in the data source and allow ASP.NET to assign a default value. Similarly, SetPropertyValues may choose not to write to the data source properties whose UsingDefaultValue property is true, because such values are easily recreated when GetPropertyValues is called. If a profile provider only persists property values that have changed since they were loaded, it could even ignore properties whose IsDirty property is false.

Inside the ASP.NET Team
ASP.NET's SqlProfileProvider writes property values to the database even if IsDirty is false. (The TextFileProfileProvider class presented in the next section does the same.) That's because each time SqlProfileProvider records profile property values in the database, it overwrites existing values. It does, however, refrain from saving values whose AllowAnonymous property is false if the user is unauthenticated, and properties with IsDirty equal to false and UsingDefaultValue equal to true. A custom profile provider that stores property values in individual fields in the data source-fields that can be individually updated without affecting other fields-could be more efficient in its SetPropertyValues method by checking the properties' IsDirty values and only updating the ones that are dirty.

TextFileProfileProvider

Figure 6-1 contains the source code for a ProfileProvider-derivative named TextFileProfileProvider that demonstrates the minimum functionality required of a profile provider. It implements the two key ProfileProvider methods-GetPropertyValues and SetPropertyValues-but provides trivial implementations of the others. Despite its simplicity, TextFileProfileProvider is fully capable of reading and writing data generated from any profile defined in the <profile> configuration section.

TextFileProfileProvider stores profile data in text files named Username_Profile.txt in the application's ~/App_Data/Profile_Data directory. Each file contains the profile data for a specific user and consists of a set of three strings (described later in this section). You must create the ~/App_Data/Profile_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/Profile_Data directory.

Listing 1. TextFileProfileProvider

Imports System
Imports System.Configuration
Imports System.Configuration.Provider
Imports System.Collections.Specialized
Imports System.Security.Permissions
Imports System.Web
Imports System.Web.Profile
Imports System.Web.Hosting
Imports System.Globalization
Imports System.IO
Imports System.Text

<SecurityPermission(SecurityAction.Assert, _
Flags:=SecurityPermissionFlag.SerializationFormatter)> _
Public Class TextFileProfileProvider : Inherits ProfileProvider
    Public Overrides Property ApplicationName() As String
        Get
            Throw New NotSupportedException()
        End Get
        Set(ByVal value As String)
            Throw New NotSupportedException()
        End Set
    End Property

    Public Overrides Sub Initialize( _
        ByVal name As String, ByVal config As NameValueCollection)
        ' Verify that config isn’t null
        If config Is Nothing Then
            Throw New ArgumentNullException("config")
        End If

        ' Assign the provider a default name if it doesn’t have one
        If String.IsNullOrEmpty(name) Then
            name = "TextFileProfileProvider"
        End If

        ' Add a default "description" attribute to config if the
        ' attribute doesn’t exist or is empty
        If String.IsNullOrEmpty(config("description")) Then
            config.Remove("description")
            config.Add("description", "Text file profile provider")
        End If

        ' Call the base class’s Initialize method
        MyBase.Initialize(name, config)

        ' Throw an exception if unrecognized attributes remain
        If config.Count > 0 Then
            Dim attr As String = config.GetKey(0)
            If (Not String.IsNullOrEmpty(attr)) Then
                Throw New _
                ProviderException("Unrecognized attribute: " & attr)
            End If
        End If

        ' Make sure we can read and write files
        ' in the ~/App_Data/Profile_Data directory
        Dim permission As FileIOPermission = _
            New FileIOPermission( _
            FileIOPermissionAccess.AllAccess, _
            HttpContext.Current.Server.MapPath( _
            "~/App_Data/Profile_Data"))
        permission.Demand()
    End Sub

    Public Overrides Function GetPropertyValues( _
        ByVal context As SettingsContext, _
        ByVal properties As SettingsPropertyCollection _
        ) As SettingsPropertyValueCollection
        Dim settings As SettingsPropertyValueCollection = _
            New SettingsPropertyValueCollection()

        ' Do nothing if there are no properties to retrieve
        If properties.Count = 0 Then
            Return settings
        End If

        ' For properties lacking an explicit SerializeAs setting, set
        ' SerializeAs to String for strings and primitives, and XML
        ' for everything else
        For Each [property] As SettingsProperty In properties
            If [property].SerializeAs = _
                SettingsSerializeAs.ProviderSpecific Then
                If [property].PropertyType.IsPrimitive OrElse _
                    [property].PropertyType Is GetType(String) Then
                    [property].SerializeAs = _
                    SettingsSerializeAs.String
                Else
                    [property].SerializeAs = SettingsSerializeAs.Xml
                End If
            End If

            settings.Add(New SettingsPropertyValue([property]))
        Next [property]

        ' Get the user name or anonymous user ID
        Dim username As String = CStr(context("UserName"))

        ' NOTE: Consider validating the user name here to prevent
        ' malicious user names such as "../Foo" from targeting
        ' directories other than ~/App_Data/Profile_Data

        ' Load the profile
        If (Not String.IsNullOrEmpty(username)) Then
            Dim reader As StreamReader = Nothing
            Dim names As String()
            Dim values As String
            Dim buf As Byte() = Nothing

            Try
                ' Open the file containing the profile data
                Try
                    Dim path As String = String.Format( _
                    "~/App_Data/Profile_Data/{0}_Profile.txt", _
                    username.Replace("\"c, "_"c))
                    reader = New StreamReader( _
                        HttpContext.Current.Server.MapPath(path))
                Catch e1 As IOException
                    ' Not an error if file doesn’t exist
                    Return settings
                End Try

                ' Read names, values, and buf from the file
                names = reader.ReadLine().Split(":"c)

                values = reader.ReadLine()
                If (Not String.IsNullOrEmpty(values)) Then
                    Dim encoding As UnicodeEncoding = _
                        New UnicodeEncoding()
                    values = encoding.GetString( _
                        Convert.FromBase64String(values))
                End If

                Dim temp As String = reader.ReadLine()
                If (Not String.IsNullOrEmpty(temp)) Then
                    buf = Convert.FromBase64String(temp)
                Else
                    buf = New Byte() {}
                End If
            Finally
                If Not reader Is Nothing Then
                    reader.Close()
                End If
            End Try

            ' Decode names, values, and buf and initialize the
            ' SettingsPropertyValueCollection returned to the caller
            DecodeProfileData(names, values, buf, settings)
        End If

        Return settings
    End Function

    Public Overrides Sub SetPropertyValues( _
        ByVal context As SettingsContext, _
        ByVal properties As SettingsPropertyValueCollection)
        ' Get information about the user who owns the profile
        Dim username As String = CStr(context("UserName"))
        Dim authenticated As Boolean = CBool( _
            context("IsAuthenticated"))

        ' NOTE: Consider validating the user name here to prevent
        ' malicious user names such as "../Foo" from targeting
        ' directories other than ~/App_Data/Profile_Data

        ' Do nothing if there is no user name or no properties
        If String.IsNullOrEmpty(username) OrElse _
            properties.Count = 0 Then
            Return
        End If

        ' Format the profile data for saving
        Dim names As String = String.Empty
        Dim values As String = String.Empty
        Dim buf As Byte() = Nothing

        EncodeProfileData(names, values, buf, properties, _
            authenticated)

        ' Do nothing if no properties need saving
        If names = String.Empty Then
            Return
        End If

        ' Save the profile data    
        Dim writer As StreamWriter = Nothing

        Try
            Dim path As String = String.Format( _
                "~/App_Data/Profile_Data/{0}_Profile.txt", _
                username.Replace("\"c, "_"c))
            writer = New StreamWriter( _
                HttpContext.Current.Server.MapPath(path), False)

            writer.WriteLine(names)

            If (Not String.IsNullOrEmpty(values)) Then
                Dim encoding As UnicodeEncoding = _
                    New UnicodeEncoding()
                writer.WriteLine( _
                    Convert.ToBase64String(encoding.GetBytes(values)))
            Else
                writer.WriteLine()
            End If

            If Not buf Is Nothing AndAlso buf.Length > 0 Then
                writer.WriteLine(Convert.ToBase64String(buf))
            Else
                writer.WriteLine()
            End If
        Finally
            If Not writer Is Nothing Then
                writer.Close()
            End If
        End Try
    End Sub

    Public Overrides Function DeleteInactiveProfiles( _
        ByVal authenticationOption As ProfileAuthenticationOption, _
        ByVal userInactiveSinceDate As DateTime) As Integer
        Throw New NotSupportedException()
    End Function

    Public Overloads Overrides Function DeleteProfiles( _
        ByVal usernames As String()) As Integer
        Throw New NotSupportedException()
    End Function

    Public Overloads Overrides Function DeleteProfiles( _
        ByVal profiles As ProfileInfoCollection) As Integer
        Throw New NotSupportedException()
    End Function

    Public Overrides Function FindInactiveProfilesByUserName( _
        ByVal authenticationOption As ProfileAuthenticationOption, _
        ByVal usernameToMatch As String, _
        ByVal userInactiveSinceDate As DateTime, _
        ByVal pageIndex As Integer, ByVal pageSize As Integer, _
        ByRef totalRecords As Integer) As ProfileInfoCollection
        Throw New NotSupportedException()
    End Function

    Public Overrides Function FindProfilesByUserName( _
        ByVal authenticationOption As ProfileAuthenticationOption, _
        ByVal usernameToMatch As String, _
        ByVal pageIndex As Integer, _
        ByVal pageSize As Integer, _
        ByRef totalRecords As Integer) As ProfileInfoCollection
        Throw New NotSupportedException()
    End Function

    Public Overrides Function GetAllInactiveProfiles( _
        ByVal authenticationOption As ProfileAuthenticationOption, _
        ByVal userInactiveSinceDate As DateTime, _
        ByVal pageIndex As Integer, _
        ByVal pageSize As Integer, _
        ByRef totalRecords As Integer) As ProfileInfoCollection
        Throw New NotSupportedException()
    End Function

    Public Overrides Function GetAllProfiles( _
        ByVal authenticationOption As ProfileAuthenticationOption, _
        ByVal pageIndex As Integer, ByVal pageSize As Integer, _
        ByRef totalRecords As Integer) As ProfileInfoCollection
        Throw New NotSupportedException()
    End Function

    Public Overrides Function GetNumberOfInactiveProfiles( _
        ByVal authenticationOption As ProfileAuthenticationOption, _
        ByVal userInactiveSinceDate As DateTime) As Integer
        Throw New NotSupportedException()
    End Function

    ' Helper methods
    Private Sub DecodeProfileData(ByVal names As String(), _
        ByVal values As String, ByVal buf As Byte(), _
        ByVal properties As SettingsPropertyValueCollection)
        If names Is Nothing OrElse values Is Nothing OrElse _
            buf Is Nothing OrElse properties Is Nothing Then
            Return
        End If

        Dim i As Integer = 0
        Do While i < names.Length
            ' Read the next property name from "names" and retrieve
            ' the corresponding SettingsPropertyValue from
            ' "properties"
            Dim name As String = names(i)
            Dim pp As SettingsPropertyValue = properties(name)

            If pp Is Nothing Then
                Continue Do
            End If

            ' Get the length and index of the persisted property value
            Dim pos As Integer = Int32.Parse(names(i + 2), _
                CultureInfo.InvariantCulture)
            Dim len As Integer = Int32.Parse(names(i + 3), _
                CultureInfo.InvariantCulture)

            ' If the length is -1 and the property is a reference
            ' type, then the property value is null
            If len = -1 AndAlso _
                (Not pp.Property.PropertyType.IsValueType) Then
                pp.PropertyValue = Nothing
                pp.IsDirty = False
                pp.Deserialized = True

                ' If the property value was peristed as a string,
                ' restore it from "values"
            ElseIf names(i + 1) = "S" AndAlso _
                    pos >= 0 AndAlso len > 0 AndAlso _
                    values.Length >= pos + len Then
                pp.SerializedValue = values.Substring(pos, len)

                ' If the property value was peristed as a byte array,
                ' restore it from "buf"
            ElseIf names(i + 1) = "B" AndAlso _
                pos >= 0 AndAlso len > 0 AndAlso _
                buf.Length >= pos + len Then
                Dim buf2 As Byte() = New Byte(len - 1) {}
                Buffer.BlockCopy(buf, pos, buf2, 0, len)
                pp.SerializedValue = buf2
            End If
            i += 4
        Loop
    End Sub

    Private Sub EncodeProfileData(ByRef allNames As String, _
        ByRef allValues As String, ByRef buf As Byte(), _
        ByVal properties As SettingsPropertyValueCollection, _
        ByVal userIsAuthenticated As Boolean)
        Dim names As StringBuilder = New StringBuilder()
        Dim values As StringBuilder = New StringBuilder()
        Dim stream As MemoryStream = New MemoryStream()

        Try
            For Each pp As SettingsPropertyValue In properties
                ' Ignore this property if the user is anonymous and
                ' the property’s AllowAnonymous property is false
                If (Not userIsAuthenticated) AndAlso _
                    (Not CBool(pp.Property.Attributes( _
                        "AllowAnonymous"))) Then
                    Continue For
                End If

                ' Ignore this property if it’s not dirty and is
                ' currently assigned its default value
                If (Not pp.IsDirty) AndAlso pp.UsingDefaultValue Then
                    Continue For
                End If

                Dim len As Integer = 0, pos As Integer = 0
                Dim propValue As String = Nothing

                ' If Deserialized is true and PropertyValue is null,
                ' then the property’s current value is null (which
                ' we’ll represent by setting len to -1)
                If pp.Deserialized AndAlso _
                        pp.PropertyValue Is Nothing Then
                    len = -1

                    ' Otherwise get the property value from
                    ' SerializedValue
                Else
                    Dim sVal As Object = pp.SerializedValue

                    ' If SerializedValue is null, then the property’s
                    ' current value is null
                    If sVal Is Nothing Then
                        len = -1

                        ' If sVal is a string, then encode it as a 
                        'string
                    ElseIf TypeOf sVal Is String Then
                        propValue = CStr(sVal)
                        len = propValue.Length
                        pos = values.Length

                        ' If sVal is binary, then encode it as a byte
                        ' array
                    Else
                        Dim b2 As Byte() = CType(sVal, Byte())
                        pos = CInt(stream.Position)
                        stream.Write(b2, 0, b2.Length)
                        stream.Position = pos + b2.Length
                        len = b2.Length
                    End If
                End If

                ' Add a string conforming to the following format
                ' to "names:"
                '                
                ' "name:B|S:pos:len"
                '    ^   ^   ^   ^
                '    |   |   |   |
                '    |   |   |   +--- Length of data
                '    |   |   +------- Offset of data
                '    |   +----------- Location (B="buf", S="values")
                '    +--------------- Property name

                If (Not propValue Is Nothing) Then
                    names.Append(pp.Name & ":" & ("S") & _
                        ":" & pos.ToString( _
                        CultureInfo.InvariantCulture) & _
                        ":" & len.ToString( _
                        CultureInfo.InvariantCulture) & ":")
                Else
                    names.Append(pp.Name & ":" & ("B") & _
                        ":" & pos.ToString( _
                        CultureInfo.InvariantCulture) & _
                        ":" & len.ToString( _
                        CultureInfo.InvariantCulture) & ":")
                End If

                ' If the propery value is encoded as a string,
                ' add the string to "values"
                If Not propValue Is Nothing Then
                    values.Append(propValue)
                End If
            Next pp

            ' Copy the binary property values written to the
            ' stream to "buf"
            buf = stream.ToArray()
        Finally
            If Not stream Is Nothing Then
                stream.Close()
            End If
        End Try

        allNames = names.ToString()
        allValues = values.ToString()
    End Sub
End Class

TextFileProfileProvider stores profile data in exactly the same format as ASP.NET's SqlProfileProvider, with some extra base-64 encoding thrown in to allow binary data and XML data to be stored in a single line of text. Its EncodeProfileData and DecodeProfileData methods, which do the encoding and decoding, are based on similar methods-methods which are internal and therefore can't be called from user code-in ASP.NET's ProfileModule class.

EncodeProfileData packs all the property values passed to it into three values:

  • A string variable named names that encodes each property value in the following format:

    Name:B|S:StartPos:Length
    

    Name is the property value's name. The second parameter, which is either B (for "binary") or S (for "string"), indicates whether the corresponding property value is stored in the string variable named values (S) or the byte[] variable named buf (B). StartPos and Length indicate the starting position (0-based) within values or buf and the length of the data, respectively. A length of -1 indicates that the property is a reference type and that its value is null.

  • A string variable named values that stores string and XML property values. Before writing values to a text file, TextFileProfileProvider base-64 encodes it so that XML data spanning multiple lines can be packed into a single line of text.

  • A byte[] variable named buf that stores binary property values. Before writing buf to a text file, TextFileProfileProvider base-64 encodes it so that binary data can be packed into a line of text.

DecodeProfileData reverses the encoding, converting names, values, and buf back into property values and applying them to the members of the supplied SettingsPropertyValueCollection. Note that profile providers are not required to persist data in this format or any other format. The format in which profile data is stored is left to the discretion of the implementor.

Listing 2 demonstrates how to make TextFileProfileProvider the default profile provider. It assumes that TextFileProfileProvider is implemented in an assembly named CustomProviders.

Listing 2. Web.config file making TextFileProfileProvider the default profile provider

<configuration>
  <system.web>
    <profile defaultProvider="TextFileProfileProvider">
      <properties>
        ...
      </properties>
      <providers>
        <add name="TextFileProfileProvider"
          type="TextFileProfileProvider, CustomProviders"
          description="Text file profile provider"
        />
      </providers>
    </profile>
  </system.web>
</configuration>

For simplicity, TextFileProfileProvider does not honor the ApplicationName property. Because TextFileProfileProvider stores profile data in text files in a subdirectory of the application root, all data that it manages is inherently application-scoped. A full-featured profile provider must support ApplicationName so profile consumers can choose whether to keep profile data private or share it with other applications.

Click here to continue on to part 6, Web Event Providers.

© Microsoft Corporation. All rights reserved.