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 abstract 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  abstract class ProfileProvider : SettingsProvider
{
    public abstract int DeleteProfiles
        (ProfileInfoCollection profiles);

    public abstract int DeleteProfiles (string[] usernames);

    public abstract int DeleteInactiveProfiles
        (ProfileAuthenticationOption authenticationOption,
        DateTime userInactiveSinceDate);

    public abstract int GetNumberOfInactiveProfiles
        (ProfileAuthenticationOption authenticationOption,
        DateTime userInactiveSinceDate);

    public abstract ProfileInfoCollection GetAllProfiles
        (ProfileAuthenticationOption authenticationOption,
        int pageIndex, int pageSize, out int totalRecords);

    public abstract ProfileInfoCollection GetAllInactiveProfiles
        (ProfileAuthenticationOption authenticationOption,
        DateTime userInactiveSinceDate, int pageIndex,
        int pageSize, out int totalRecords);

    public abstract ProfileInfoCollection FindProfilesByUserName
        (ProfileAuthenticationOption authenticationOption,
        string usernameToMatch, int pageIndex, int pageSize,
        out int totalRecords);

    public abstract ProfileInfoCollection
        FindInactiveProfilesByUserName (ProfileAuthenticationOption
        authenticationOption, string usernameToMatch,
        DateTime userInactiveSinceDate, int pageIndex,
        int pageSize, out int totalRecords);
}

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

public abstract class SettingsProvider : ProviderBase
{
    // Properties
    public abstract string ApplicationName { get; set; }

    // Methods
    public abstract SettingsPropertyValueCollection
        GetPropertyValues (SettingsContext context,
        SettingsPropertyCollection properties);

    public abstract void SetPropertyValues(SettingsContext context,
        SettingsPropertyValueCollection properties);
}

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:

SettingsPropertyValueCollection settings =
    new SettingsPropertyValueCollection ();

foreach (SettingsProperty property in properties)
    settings.Add (new SettingsPropertyValue (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 null 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:

SettingsPropertyValueCollection settings =
    new SettingsPropertyValueCollection ();

foreach (SettingsProperty property in properties)
{
    // Create a SettingsPropertyValue
    SettingsPropertyValue pp = new SettingsPropertyValue (property);

    // Read a persisted property value from the data source
    object val = GetPropertyValueFromDataSource (property.Name);

    // If val is null, set the property value to null
    if (val == null)
    {
        pp.PropertyValue = null;
        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.
    }

    // Add the SettingsPropertyValue to the collection
    settings.Add (pp);
}

// 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:

foreach (SettingsPropertyValue property in properties)
{
    // Get information about the user who owns the profile
    string username = (string) context["UserName"];
    bool authenticated = (bool) context["IsAuthenticated"];

    // Ignore this property if the user is anonymous and
    // the property's AllowAnonymous property is false
    if (!authenticated &&
        !(bool) property.Property.Attributes["AllowAnonymous"])
        continue;

    // Otherwise persist the property value
    if (property.Deserialized)
    {
        // TODO: Write property.PropertyValue to the data source
    }
    else
    {
        // TODO: Write property.SerializedValue to the data source
    }
}

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

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

[SecurityPermission(SecurityAction.Assert,
  Flags=SecurityPermissionFlag.SerializationFormatter)]
public class TextFileProfileProvider : ProfileProvider
{
    public override string ApplicationName
    {
        get { throw new NotSupportedException(); }
        set { throw new NotSupportedException(); }
    }

    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 = "TextFileProfileProvider";

        // 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 profile 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/Profile_Data directory
        FileIOPermission permission =
            new FileIOPermission (FileIOPermissionAccess.AllAccess,
                                  HttpContext.Current.Server.MapPath(
                                  "~/App_Data/Profile_Data"));
        permission.Demand();
    }

    public override SettingsPropertyValueCollection
        GetPropertyValues(SettingsContext context,
        SettingsPropertyCollection properties)
    {
        SettingsPropertyValueCollection settings =
            new SettingsPropertyValueCollection();

        // Do nothing if there are no properties to retrieve
        if (properties.Count == 0)
            return settings;

        // For properties lacking an explicit SerializeAs setting, set
        // SerializeAs to String for strings and primitives, and XML
        // for everything else
        foreach (SettingsProperty property in properties)
        {
            if (property.SerializeAs ==
                SettingsSerializeAs.ProviderSpecific)
                if (property.PropertyType.IsPrimitive ||
                    property.PropertyType == typeof(String))
                    property.SerializeAs = SettingsSerializeAs.String;
                else
                    property.SerializeAs = SettingsSerializeAs.Xml;

            settings.Add(new SettingsPropertyValue(property));
        }

        // Get the user name or anonymous user ID
        string username = (string)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 (!String.IsNullOrEmpty(username))
        {
            StreamReader reader = null;
            string[] names;
            string values;
            byte[] buf = null;

            try
            {
                // Open the file containing the profile data
                try
                {
                    string path =                           
                      String.Format(
                        "~/App_Data/Profile_Data/{0}_Profile.txt",
                        username.Replace('\\', '_'));
                    reader = new StreamReader
                        (HttpContext.Current.Server.MapPath(path));
                }
                catch (IOException)
                {
                    // Not an error if file doesn't exist
                    return settings;
                }

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

                values = reader.ReadLine();
                if (!string.IsNullOrEmpty(values))
                {
                    UnicodeEncoding encoding = new UnicodeEncoding();
                    values = encoding.GetString
                        (Convert.FromBase64String(values));
                }

                string temp = reader.ReadLine();
                if (!String.IsNullOrEmpty(temp))
                {
                    buf = Convert.FromBase64String(temp);
                }
                else
                    buf = new byte[0];
            }
            finally
            {
                if (reader != null)
                    reader.Close();
            }

            // Decode names, values, and buf and initialize the
            // SettingsPropertyValueCollection returned to the caller
            DecodeProfileData(names, values, buf, settings);
        }

        return settings;
   }

    public override void SetPropertyValues(SettingsContext context,
        SettingsPropertyValueCollection properties)
    {
        // Get information about the user who owns the profile
        string username = (string) context["UserName"];
        bool authenticated = (bool) 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) || properties.Count == 0)
            return;

        // Format the profile data for saving
        string names = String.Empty;
        string values = String.Empty;
        byte[] buf = null;

        EncodeProfileData(ref names, ref values, ref buf,
            properties, authenticated);

        // Do nothing if no properties need saving
        if (names == String.Empty)
            return;
        
        // Save the profile data    
        StreamWriter writer = null;

        try
        {
            string path =
                String.Format(
                   "~/App_Data/Profile_Data/{0}_Profile.txt",
                   username.Replace('\\', '_'));
            writer = new StreamWriter
                (HttpContext.Current.Server.MapPath(path), false);

            writer.WriteLine(names);

            if (!String.IsNullOrEmpty(values))
            {
                UnicodeEncoding encoding = new UnicodeEncoding();
                writer.WriteLine(Convert.ToBase64String
                    (encoding.GetBytes(values)));
            }
            else
                writer.WriteLine();

            if (buf != null && buf.Length > 0)
                writer.WriteLine(Convert.ToBase64String(buf));
            else
                writer.WriteLine();
        }
        finally
        {
            if (writer != null)
                writer.Close();
        }
    }

    public override int DeleteInactiveProfiles
        (ProfileAuthenticationOption authenticationOption,
        DateTime userInactiveSinceDate)
    {
        throw new NotSupportedException();
    }

    public override int DeleteProfiles(string[] usernames)
    {
        throw new NotSupportedException();
    }

    public override int DeleteProfiles(ProfileInfoCollection profiles)
    {
        throw new NotSupportedException();
    }

    public override ProfileInfoCollection
        FindInactiveProfilesByUserName(ProfileAuthenticationOption
        authenticationOption, string usernameToMatch, DateTime
        userInactiveSinceDate, int pageIndex, int pageSize, out int
        totalRecords)
    {
        throw new NotSupportedException();
    }

    public override ProfileInfoCollection FindProfilesByUserName
        (ProfileAuthenticationOption authenticationOption,
        string usernameToMatch, int pageIndex, int pageSize,
        out int totalRecords)
    {
        throw new NotSupportedException();
    }

    public override ProfileInfoCollection GetAllInactiveProfiles
        (ProfileAuthenticationOption authenticationOption,
        DateTime userInactiveSinceDate, int pageIndex, int pageSize,
        out int totalRecords)
    {
        throw new NotSupportedException();
    }

    public override ProfileInfoCollection GetAllProfiles
        (ProfileAuthenticationOption authenticationOption,
        int pageIndex, int pageSize, out int totalRecords)
    {
        throw new NotSupportedException();
    }

    public override int GetNumberOfInactiveProfiles
        (ProfileAuthenticationOption authenticationOption,
        DateTime userInactiveSinceDate)
    {
        throw new NotSupportedException();
    }

    // Helper methods
    private void DecodeProfileData(string[] names, string values,
        byte[] buf, SettingsPropertyValueCollection properties)
    {
        if (names == null || values == null || buf == null ||
            properties == null)
            return;

        for (int i=0; i<names.Length; i+=4)
        {
            // Read the next property name from "names" and retrieve
            // the corresponding SettingsPropertyValue from
            // "properties"
            string name = names[i];
            SettingsPropertyValue pp = properties[name];

            if (pp == null)
                continue;

            // Get the length and index of the persisted property value
            int pos = Int32.Parse(names[i + 2],
                CultureInfo.InvariantCulture);
            int len = 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 && !pp.Property.PropertyType.IsValueType)
            {
                pp.PropertyValue = null;
                pp.IsDirty = false;
                pp.Deserialized = true;
            }

            // If the property value was peristed as a string,
            // restore it from "values"
            else if (names[i + 1] == "S" && pos >= 0 && len > 0 &&
                values.Length >= pos + len)
                pp.SerializedValue = values.Substring(pos, len);

            // If the property value was peristed as a byte array,
            // restore it from "buf"
            else if (names[i + 1] == "B" && pos >= 0 && len > 0 &&
                buf.Length >= pos + len)
            {
                byte[] buf2 = new byte[len];
                Buffer.BlockCopy(buf, pos, buf2, 0, len);
                pp.SerializedValue = buf2;
            }
        }
    }
    
    private void EncodeProfileData(ref string allNames,
        ref string allValues, ref byte[] buf,
        SettingsPropertyValueCollection properties,
        bool userIsAuthenticated)
    {
        StringBuilder names = new StringBuilder();
        StringBuilder values = new StringBuilder();
        MemoryStream stream = new MemoryStream();

        try
        {
            foreach (SettingsPropertyValue pp in properties)
            {
                // Ignore this property if the user is anonymous and
                // the property's AllowAnonymous property is false
                if (!userIsAuthenticated &&
                    !(bool)pp.Property.Attributes["AllowAnonymous"])
                    continue;

                // Ignore this property if it's not dirty and is
                // currently assigned its default value
                if (!pp.IsDirty && pp.UsingDefaultValue)
                    continue;

                int len = 0, pos = 0;
                string propValue = null;

                // 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 && pp.PropertyValue == null)
                    len = -1;

                // Otherwise get the property value from
                // SerializedValue
                else
                {
                    object sVal = pp.SerializedValue;

                    // If SerializedValue is null, then the property's
                    // current value is null
                    if (sVal == null)
                        len = -1;

                    // If sVal is a string, then encode it as a string
                    else if (sVal is string)
                    {
                        propValue = (string)sVal;
                        len = propValue.Length;
                        pos = values.Length;
                    }

                    // If sVal is binary, then encode it as a byte
                    // array
                    else
                    {
                        byte[] b2 = (byte[])sVal;
                        pos = (int)stream.Position;
                        stream.Write(b2, 0, b2.Length);
                        stream.Position = pos + b2.Length;
                        len = b2.Length;
                    }
                }

                // 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

                names.Append(pp.Name + ":" + ((propValue != null) ?
                    "S" : "B") + ":" +
                    pos.ToString(CultureInfo.InvariantCulture) + ":" +
                    len.ToString(CultureInfo.InvariantCulture) + ":");

                // If the propery value is encoded as a string, add the
                // string to "values"
                if (propValue != null)
                    values.Append(propValue);
            }

            // Copy the binary property values written to the
            // stream to "buf"
            buf = stream.ToArray();
        }
        finally
        {
            if (stream != null)
                stream.Close();
        }

        allNames = names.ToString();
        allValues = values.ToString();
    }
}

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.