Building Your Own Profile Provider





Building Your Own Profile Provider

The Membership and Profile systems are undoubtedly something you'll want to use on new projects as is. They'll save you a ton of work because they handle the grunt work of managing user data. There will be times, however, when you have specific needs or existing data that you'd like to use in the context of these systems. That's why ASP.NET enables you to write your own providers. Let's walk through an example of a custom Profile provider to match our Membership provider.

Just as our custom Membership provider had to inherit from and implement members of an abstract class, our custom Profile provider will do the same, this time using System.Web.Profile.ProfileProvider as the base. This provider, however, only has two properties, Name and ApplicationName. It also has an Initialize() method like the Membership provider. Listing 12.4 shows the two properties and the Initialize() method.

If you're curious about the lineage of these providers, they do share some common ground. ProfileProvider inherits from System.Configuration.SettingsProvider, which in turn inherits from System.Configuration.Provider.ProviderBase. MembershipProvider inherits directly from ProviderBase. You can see by looking at the "roots" of the class where they get their members.


Listing 12.4. The two required properties and Initialize() method of our Profile provider

C#

public class CustomProfileProvider : ProfileProvider
{
  public override void Initialize(string name, NameValueCollection config)
  {
    _name = name;
    if (config["applicationName"] == null || config["applicationName"].Trim() == "")
    {
      _applicationName = HttpContext.Current.Request.ApplicationPath;
    }
    else
    {
      _applicationName = config["applicationName"];
    }
  }

  private string _name;
  public override string Name
  {
    get { return _name; }
  }

  private string _applicationName;
  public override string ApplicationName
  {
    get { return _applicationName; }
    set { _applicationName = value; }
  }
  }


VB.NET

Public Class CustomProfileProvider
  Inherits ProfileProvider

  Public Overrides Sub Initialize(name As String, _
config As NameValueCollection)
    _name = name
    If config("applicationName") Is Nothing Or _
config("applicationName").Trim() = "" Then
      _applicationName = HttpContext.Current.Request.ApplicationPath
    Else
      _applicationName = config("applicationName")
    End If
  End Sub

  Private _name As String
  Public Overrides ReadOnly Property Name() As String
    Get
      Return _name
    End Get
  End Property 

  Private _applicationName As String
  Public Overrides Property ApplicationName() As String
    Get
      Return _applicationName
    End Get
    Set
      _applicationName = value
    End Set
  End Property
End Class

If this code looks familiar to you, that's probably because it isn't that different from the humble start of our custom Membership class in Listing 11.5. The difference here is that we don't have the extra properties to deal with. We set the properties in the Initialize() method, where Name is still read-only and comes from web.config, while ApplicationName is still determined by the path of the application or the application name.

As with the custom Membership provider we created in Chapter 11, we aren't going to make use of the ApplicationName here. As with Membership, this property can be used to associate profile data with a specific application in a shared database.


We could provide a connection string property here as well, similar again to those used in Membership providers. The same principles apply when passing in values via attributes in the web.config provider add elements, setting them up in the provider itself via the Initialize() method.


We need to be concerned about two primary methods to get data in and out of our data store. GetPropertyValues() pulls data out of the store, while SetPropertyValues() persists them when Profile.Save() is called.

GetPropertyValues() takes a System.Configuration. SettingsContext object and a System.Configuration. SettingsPropertyCollection object as parameters and returns a System.Configuration.SettingsPropertyValueCollection. This returned collection is used by the Profile property to return property values. Let's sort these out, one at a time.

The SettingsContext object is derived from Hashtable and contains two key-value pairs. The first value is "UserName," which not surprisingly is a string of the name of the user whose profile we want to retrieve. The second value, "IsAuthenticated," is a Boolean and indicates what the name implies. The temptation here is to not look up profile data if the user isn't authenticated, but keep in mind that you can load any user's profile data into an HttpProfile object using Profile.GetProfile(). If anonymous identification is enabled (as in Listing 12.3), you may want to store and retrieve anonymous user data. If the user is not authenticated and a call is made to the anonymous user's profile, the "UserName" value returned is a Guid (in string form) used to identify the anonymous user.

The SettingsPropertyCollection is, believe it or not, a collection of SettingsProperty objects. The method will get one of these objects for every property you declared in web.config. If your provider is configured as the default provider, then any property not specifically set to use another provider (with the provider attribute) will be in this collection. If it's not the default provider, then only those properties specified to use your provider will be in the collection. Grouped properties will have a name like the one you'd use in the calling code. In our previous grouping example, calling for Profile.Pets.CatName would correspond to a SettingsProperty object with the name "Pets.CatName" here.

The SettingsPropertyValueCollection object should return a collection of SettingsPropertyValue objects, one for each of the SettingsProperty objects passed in to the method.

Now that you're familiar with the parameters and return value for GetPropertyValues(), you need to make some design decisions. You'll need to decide if your provider will be capable of storing any property you specify in web.config, or if it will handle only properties you specify. The default SQL provider does a good job of accommodating any property, so duplicating that functionality is a lot like reinventing the wheel. Chances are that if you're writing a custom provider, you've got very specific needs in mind, and therefore you only need to handle specific properties.

In case you're wondering, the default SQL provider stores the data in a single row for a particular user. In the aspnet_Profile table, the UserId column references an entry in the aspnet_Users table (where the name of the user, or Guid as a string for anonymous users, is stored). Two columns hold the actual property values, PropertyValuesString and PropertyValuesBinary, which store strings and binary values, respectively. The PropertyNames column stores the property name, a flag indicating whether the value is stored as a string or binary object, the starting position of the property value in either the PropertyValuesString or PropertyValuesBinary columns, and the length of the value. So if we had a binary property called "Test" and a string value called "Age," the PropertyNames column would say:

Test:B:0:40:Age:S:0:2

PropertyValuesString would say something like "21" for the age (causing the length of "2" in PropertyNames), and PropertyValuesBinary would have some binary value for test that was 40 bytes long.


Let's assume we're only going to return values for specific properties, namely those we set up in Listing 12.1. "Happy" and "HairColor" are two data items associated with a particular user's profile data. Our profile database table in SQL Server has three columns: Name (nvarchar), Happy (bit), and HairColor (nvarchar). We'll match up these database values with our properties, as shown in Listing 12.5, our complete GetPropertyValues() method. Depending on your needs, you may want to store additional data such as a DateTime that indicates when the profile data was updated or accessed. These are handy when implementing other members in the provider, as we'll see later.

Listing 12.5. Our GetPropertyValues() method

C#

public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context,
 SettingsPropertyCollection ppc)
{
  if ((bool)context["IsAuthenticated"] == false) throw new Exception("Anonymous profile
 data is not supported by this Profile Provider.");
  SettingsPropertyValueCollection settings = new SettingsPropertyValueCollection();
  SqlConnection connection = new SqlConnection("my connection string");
  Connection.Open();
  SqlCommand command = new SqlCommand("SELECT Happy, HairColor FROM ProfileData WHERE Name
 = @Name", connection);
  command.Parameters.AddWithValue("@Name", context["UserName"].ToString());
  SqlDataReader reader = command.ExecuteReader();
  bool dataAvailable = false;
  if (reader.Read()) dataAvailable = true
  foreach (SettingsProperty property in ppc)
  {
    SettingsPropertyValue value = new SettingsPropertyValue(ppc[property.Name]);
    switch (property.Name)
    {
      case "Happy":
        if (dataAvailable) value.PropertyValue = reader.GetBoolean(0);
        else value.PropertyValue = false;
        break;
      case "HairColor":
        if (dataAvailable) value.PropertyValue = reader.GetString(1);
        else value.PropertyValue = "";
        break;
      default:
        throw new Exception("This profile provider doesn't process the \"" + property.Name
        + "\" profile property.");
    }
    settings.Add(value);
  }
  reader.Close();
  connection.Close();
  return settings;
}


VB.NET

Public Overrides Function GetPropertyValues(context As SettingsContext,_
ppc As SettingsPropertyCollection) As SettingsPropertyValueCollection
  If CBool(context("IsAuthenticated")) = False Then
    Throw New Exception("Anonymous profile data is not supported by this Profile Provider.")
  End If
  Dim settings As New SettingsPropertyValueCollection()
  Dim connection As New SqlConnection("my connection string")
  Connection.Open()
  Dim command As New _
SqlCommand("SELECT Happy, HairColor FROM ProfileData WHERE [email protected]",_
connection)
  command.Parameters.AddWithValue("@Name", _
context("UserName").ToString())
  Dim reader As SqlDataReader = command.ExecuteReader()
  Dim dataAvailable As Boolean = False
  If reader.Read() Then
    dataAvailable = True
  End If
  Dim property As SettingsProperty
  For Each property In  ppc
    Dim value As New SettingsPropertyValue(ppc([property].Name))
    Select Case [property].Name
      Case "Happy"
        If dataAvailable Then
          value.PropertyValue = reader.GetBoolean(0)
        Else
          value.PropertyValue = False
        End If
      Case "HairColor"
        If dataAvailable Then
          value.PropertyValue = reader.GetString(1)
        Else
          value.PropertyValue = ""
        End If
      Case Else
        Throw New _
Exception("This profile provider doesn't process the """ + _
[property].Name + """ profile property.")
    End Select
    settings.Add(value)
  Next property
  reader.Close()
  connection.Close()
  Return settings
End Function

At first glance, it looks like there's a lot going on here, but there's not. The method starts by checking the value of the "IsAuthenticated" item in the context hash, and if it's false, it throws an exception. Remember that we decided not to persist anonymous data with our provider.

Next we declare a SettingsPropertyValueCollection object, which is where we'll put all the values we want to return.

The next bit is typical SQL code that looks for the profile data from our database, based on the "UserName" string passed in via the settings context. It's possible that the user may not have any profile data stored yet, so we create a test with the SqlDataReader's Read() method.

Finally we loop through the SettingsProperty objects passed in via the SettingsPropertyCollection parameter. First we create the SettingsPropertyValue object, based on the SettingsProperty that we're going to add to the returning SettingsPropertyValueCollection. For each object, we'll run a switch (Select in VB.NET) block to determine which property we're dealing with. For each match, we'll assign the value from the database if our Read() test was successful; otherwise we'll assign a default value. If the property name doesn't match any of our cases, we'll throw an exception indicating that our provider doesn't handle the particular profile property. As long as one of the items in the switch block is called, we'll add the new SettingsPropertyValue object to our SettingsPropertyValueCollection. When we're done, we close our data connection and return the new collection. This method now acts as the plumbing between our Profile property and our data store.

SetPropertyValues() should do nearly the same thing, only in reverse. It acts as the plumbing when the Save() method of the HttpProfile object is called. Perhaps the biggest difference is that we'll delete the user's old profile data before saving the new data. This method has no return value, but it again has the SettingsContext object and this time a SettingsPropertyValueCollection as parameters. The code is shown in Listing 12.6.

Listing 12.6. Our SetPropertyValues() method

C#

public override void SetPropertyValues(SettingsContext context,
 SettingsPropertyValueCollection ppvc)
{
  SqlConnection connection = new SqlConnection("my connection string");
  connection.Open();
  SqlCommand command = new 
SqlCommand("DELETE FROM ProfileData WHERE Name = @Name", connection);
  command.Parameters.AddWithValue("@Name", context["UserName"].ToString());
  command.ExecuteNonQuery();
  command.CommandText = "INSERT INTO ProfileData "
+ "(Name, Happy, HairColor) VALUES (@Name, @Happy, @HairColor)";
  foreach (SettingsPropertyValue propertyvalue in ppvc)
  {
    switch (propertyvalue.Name)
    {
      case "Happy":
        command.Parameters.AddWithValue("@Happy", (bool)propertyvalue.PropertyValue);
        break;
      case "HairColor":
        command.Parameters.AddWithValue("@HairColor", (string)propertyvalue.PropertyValue);
        break;
      default:
        throw new Exception("This profile provider doesn't process the \"" + propertyvalue
.Name
        + "\" profile property.");
    }
  }
  command.ExecuteNonQuery();
  connection.Close();
}


VB.NET

Public Overrides Sub SetPropertyValues(context As SettingsContext, ppvc As
 SettingsPropertyValueCollection)
  Dim connection As New SqlConnection("my connection string")
  connection.Open()
  Dim command As New _
SqlCommand("DELETE FROM ProfileData WHERE Name = @Name", connection)
  command.Parameters.AddWithValue("@Name", context("UserName").ToString())
  command.ExecuteNonQuery()
  command.CommandText = "INSERT INTO ProfileData (Name, Happy, HairColor) VALUES (@Name,
 @Happy, @HairColor)"
  Dim propertyvalue As SettingsPropertyValue
  For Each propertyvalue In  ppvc
    Select Case propertyvalue.Name
      Case "Happy"
        command.Parameters.AddWithValue("@Happy", CBool(propertyvalue.PropertyValue))
      Case "HairColor"
        command.Parameters.AddWithValue("@HairColor", CStr(propertyvalue.PropertyValue))
      Case Else
        Throw New Exception("This profile provider doesn't process the """ + propertyvalue
.Name + """ profile property.")
    End Select
  Next propertyvalue
  command.ExecuteNonQuery()
  connection.Close()
End Sub

This method is a bit more straightforward. As before, our user is identified by the "UserName" value in the SettingsContext hash. First we delete any profile data for the user. We could also look for existing data, and if it exists, perform an UPDATE instead of an INSERT, but this requires an extra step if the data does not already exist. Next we create the command that will insert the data and add parameters to our command object by looping through the SettingsPropertyValueCollection. We get the new value from the SettingsPropertyValue objects' PropertyValue properties. (Is that enough use of the word "property?") When we're done, we execute the command and clean up our connection.

The remaining methods to implement in the provider get or delete profile data. These methods act as the plumbing under the static methods of the ProfileManager class. Most of these methods use ProfileInfo objects grouped in ProfileInfoCollection objects.

The ProfileInfo class does not contain any of the actual profile data, but it does contain several properties that describe it: IsAnonymous (Boolean), LastActivityDate (DateTime), LastUpdatedDate DateTime), Size (Int32), and UserName (String). The class is little more than a container class, so you're free to populate whichever proper ties you want. Instances of the class can be added to a ProfileInfoCollection object in the same way as most other collections, using its Add() method. Listing 12.7 shows a simple example of manipulating these two objects.

Listing 12.7. Using ProfileInfo with ProfileInfoCollection

C#

ProfileInfoCollection pic = new ProfileInfoCollection();
ProfileInfo info = new ProfileInfo();
info.UserName = "Jeff";
info.LastUpdatedDate = DateTime.Now;
pic.Add(info);


VB.NET

Dim pic As New ProfileInfoCollection()
Dim info As New ProfileInfo()
info.UserName = "Jeff"
info.LastUpdatedDate = DateTime.Now
pic.Add(info)

Let's look at the other eight methods required for a profile provider. If your requirements don't call for these methods, as used via ProfileManager, you can add just one line of code to these methods:

throw new NotImplementedException();

Visual Studio 2005 actually generates this line for you in each of the methods when you declare your intention to inherit from the base class.

DeleteProfiles() has two overloads, one that takes a string array of user names as a parameter, and one that takes a ProfileInfoCollection. These methods are intended to delete the profiles of those users indicated. Both return an integer indicating the number of deleted profiles.

DeleteInactiveProfiles() takes two parameters, a ProfileAuthenticationOption enumeration (whose values are All, Anonymous, or Authenticated) and a DateTime indicating the cut-off date for inactive profiles. The method returns an integer indicating the number of deleted profiles. Implementing this method requires that your profile data use some kind of date stamp that is updated any time the profile is accessed. It also requires that you differentiate between the profiles of anonymous and authenticated users. GetNumberOfInactiveProfiles() works just like your DeleteInactiveProfiles(), except that it doesn't delete the profile data.

GetAllProfiles() returns a ProfileInfoCollection and limits the number of ProfileInfo objects returned by providing a paging mechanism. Its first parameter is a ProfileAuthenticationOption value, followed by integers for the page index and page size, and an integer output parameter that indicates the total number of records. In the case of most data stores, it's probably a good idea to implement some kind of server-side logic (such as a stored procedure in SQL Server) to page the results and send them back to the ASP.NET application.

Output parameters rest in a method signature like other parameters, but the difference is that they actually must have a value assigned to them before the method is finished executing. They're marked in C# with the out keyword, and ByRef in VB.NET. You need a variable declared when calling a method with an output value to hold the output value. Imagine you have the following method:

C#
public void OutputTest(out int totalRecords)
{
  totalRecords = 32;
}


VB.NET
Public Sub OutputTest(ByRef totalRecords As Integer)
   totalRecords = 32
End Sub

Before calling this method, you'll have an integer declared to pass in a parameter, and when the method is finished, the value of that integer will have changed.

int x = 0;
OutputTest(out x);
Trace.Write(x.ToString()); ' x now has a value of 32


GetAllInactiveProfiles() works almost the same as GetAllProfiles(), except that its second parameter is a DateTime indicating the cut-off for inactive profiles. As with DeleteInactiveProfiles(), this requires that you've put some kind of activity date stamp in your profile data.

FindProfilesByUserName() and FindInactiveProfilesByUserName() also work similarly, except that they take a string parameter to search for profiles by name.

More specifics about this and the other ASP.NET providers are available in the .NET SDK documentation, including full-blown sample implementations. Simply search for the provider's base class, where you'll find links to the sample implementations. The documentation has details on the method signatures, return values, and the intended function of each member.



     Python   SQL   Java   php   Perl 
     game development   web development   internet   *nix   graphics   hardware 
     telecommunications   C++ 
     Flash   Active Directory   Windows