Home > Articles > Programming > ASP .NET

  • Print
  • + Share This
This chapter is from the book

This chapter is from the book

Profile

Profile provides a simple way of defining database-backed user profile information. With just a few configuration file entries, you can quickly build a site that stores user preferences (or any other data, for that matter) into a database, all with a simple type-safe interface for the developer. In many ways, Profile looks and feels much like Session state, but unlike Session state, Profile is persistent across sessions and is also tied into the Membership provider, so authenticated clients have data stored associated with their real identities instead of some arbitrary identifier. Anonymous clients will have an identifier generated for them, stored as a persistent cookie so that subsequent access from the same machine will retain their preferences as well. In addition, Profile is retrieved on demand and written only when modified, so unlike out-of-process Session state storage, you only incur a trip to the database when you actually use Profile, not implicitly with each request.

Fundamentals

The first step in using Profile is to declare the properties you would like to store on behalf of each user in your web.config file under the <profile> element. Your first decision is whether you want to allow anonymous clients to store profile data or only authenticated clients. If you elect to support anonymous clients, you must enable anonymous identification by adding the anonymousIdentification element in your web.config file with its enabled attribute set to true. This will cause ASP.NET to generate a unique identifier (a GUID) to associate with each anonymous user via a persistent cookie. If the user is authenticated, the membership identifier for that user will be used directly and no additional cookie will be created. You also have control over whether individual properties are stored on behalf of anonymous users through the allowAnonymous attribute of the add element. Listing 4-22 shows a sample web.config file with anonymous identification enabled, and three property declarations, one each for the user's favorite color, favorite number, and favorite HTTP status code. Note that all properties in this example allow anonymous access.

Listing 4-22. Defining three Profile properties in web.config

<configuration>
  <system.web>
     <anonymousIdentification enabled="true" />

     <profile enabled="true">
        <properties>
          <add name="FavoriteColor" defaultValue="blue"
                    type="System.String"
                    allowAnonymous="true" />

          <add name="FavoriteNumber" defaultValue="42"
                   type="System.Int32"
                   allowAnonymous="true" />

          <add name="FavoriteHttpStatusCode"
                    type="System.Net.HttpStatusCode"
                    allowAnonymous="true" serializeAs="String"
                    defaultValue="OK" />
        </properties>
     </profile>
  </system.web>
</configuration>

When ASP.NET compiles your site, it creates a new class that derives from ProfileBase with type-safe accessors to the properties you declared. These accessors use the Profile provider to save and retrieve these properties to and from whatever database the provider is configured to interact with. Listing 4-23 shows what the generated class would look like for the three profile properties defined in Listing 4-22.

Listing 4-23. Generated ProfileCommon Class

public class ProfileCommon : ProfileBase {
    public virtual HttpStatusCode FavoriteHttpStatusCode {
        get {
            return ((HttpStatusCode)(this.GetPropertyValue(
                       "FavoriteHttpStatusCode")));
        }
        set {
            this.SetPropertyValue("FavoriteHttpStatusCode",
                        value);
        }
    }

    public virtual int FavoriteNumber {
        get {
            return ((int)(this.GetPropertyValue(
                              "FavoriteNumber")));
        }
        set {
            this.SetPropertyValue("FavoriteNumber", value);
        }
    }

    public virtual string FavoriteColor {
        get {
            return ((string)(this.GetPropertyValue(
                                  "FavoriteColor")));
        }
        set {
            this.SetPropertyValue("FavoriteColor", value);
        }
    }

    public virtual ProfileCommon GetProfile(string username)
    {
        return ((ProfileCommon)(ProfileBase.Create(
                          username)));
    }
}

The second thing that happens is ASP.NET adds a property declaration to each generated Page class in your site named Profile, which is a type-safe accessor to the current Profile class (which is part of the HttpContext), as shown in Listing 4-24.

Listing 4-24. Type-safe property added to Page-derived class for profile access

public partial class Default_aspx : Page
{
   protected ProfileCommon Profile {
      get {
          return ((ProfileCommon)(this.Context.Profile));
      }
   }
  //...
}

This lets you interact with your profile properties in a very convenient way. For example, Listing 4-25 shows a snippet of code that sets the profile properties based on fields in a form.

Listing 4-25. Setting profile properties

void enterButton_Click(object sender, EventArgs e)
{
  Profile.FavoriteColor = colorTextBox.Text;
  Profile.FavoriteNumber  = int.Parse(numberTextBox.Text);
  Profile.FavoriteHttpStatusCode = (HttpStatusCode)
                   Enum.Parse(typeof(HttpStatusCode),
                           statusCodeTextBox.Text);
}

If you look in the database used by the Profile provider (by default a local SQL Server 2005 Express database in your application's App_Data directory), you will see a table called aspnet_Profile with 5 columns:

UserId
PropertyNames
PropertyValuesString
PropertyValuesBinary
LastUpdatedDate

In the example shown in Listings 4-22, 4-23, and 4-25 these columns were populated with the following values:

405A7333-2C8D-4E63-AB56-BA54398D47DF
FavoriteColor:S:0:3:FavoriteNumber:S:3:2:FavoriteHttpStatusCode:S:5:16:
red42MovedPermanently
2006-1-1 09:00:00.000

So you can see that by default the Profile provider uses a string serialization with property names and string lengths carefully stored as well on a per-user basis. In our example the user was anonymous, so a GUID was generated and used to index the property values in the aspnet_Profile table. The UserId column is actually a foreign key reference to the UserId column of the aspnet_Users table, where the membership system keeps user information (anonymous user information is also stored in this table).

Migrating Anonymous Profile Data

If your application supports both anonymous and authenticated clients, you may find that clients are frustrated when they store data as an anonymous user only to find it disappear when they log in and become authenticated. You can take steps to migrate their anonymous data to their authenticated identity by using the MigrateAnonymous event of the ProfileModule. This event, which you would typically add as a handler in global.asax, is triggered when an anonymous client with profile information transitions to an authenticated user. Listing 4-26 shows a sample global.asax file with a handler for this event transferring all profile state to the new profile data store for the newly authenticated client.

Listing 4-26. Sample global.asax file migrating anonymous profile data

<%@ Application Language="C#" %>

<script runat="server">
  void Profile_MigrateAnonymous(object sender,
                                ProfileMigrateEventArgs e)
  {
    ProfileCommon prof = Profile.GetProfile(e.AnonymousID);
    Profile.FavoriteColor = prof.FavoriteColor;
    Profile.FavoriteNumber = prof.FavoriteNumber;
    Profile.FavoriteHttpStatusCode = prof.FavoriteHttpStatusCode;
  }
</script>

Note that the anonymous identifier previously associated with the client is available through the ProfileMigrateEventArgs parameter, and the actual profile for that identity is accessible using the static GetProfile method of the Profile class. In most cases it would be wise to prompt the user before migrating her anonymous data, since that user may have profile data already associated with her account and might elect to not have the data she entered as an anonymous client overwrite the data that was stored previously on her behalf.

Managing Profile Data

Once you start using Profile in a live site, you will quickly discover that the number of entries in your profile database can grow without bound, especially if you have enabled anonymous storage. To deal with this, there is a class called ProfileManager which you can use to periodically clean up the profile database. Listing 4-27 shows the core static methods of this class, which tie into the current Profile provider.

Listing 4-27. The ProfileManager class

public static class ProfileManager
{
  public static int DeleteInactiveProfiles(
         ProfileAuthenticationOption authenticationOption,
         DateTime userInactiveSinceDate);
  public static bool DeleteProfile(string username);

  public static ProfileInfoCollection FindProfilesByUserName(...);
  public static ProfileInfoCollection GetAllProfiles(...);
  public static int GetNumberOfInactiveProfiles(...);
  public static int GetNumberOfProfiles(...);

    //...
}

This class is accessible both in an ASP.NET Web application as well as in any .NET application that links to the System.Web.dll assembly. You can use the static methods in this class to build an administrative page in your site that lets the administrator clean up the profile database from time to time, perhaps giving her the option of specifying an inactive lower bound above which all profiles should be deleted (using the last parameter to DeleteInactiveProfiles method). If you prefer to automate the process, you could also write a Windows service that ran continuously on the server, deleting inactive profiles periodically, or perhaps a command line program that was run as part of a batch script periodically. Whichever technique you use is unimportant, but making sure you have a plan to clear out unused profile data from time to time is critical, especially if you allow anonymous clients to store data.

Storing Profile Data

The default Profile provider in ASP.NET 2.0 stores profile data in a local SQL Server 2005 Express database located in the App_Data directory of your application. For most production sites, this will be insufficient, and they will want to store the data in a full SQL Server database along with the rest of the data for their application. You can change the default database used by the Profile provider class by changing the value of the LocalSqlServer connection string in your web.config file. Prior to doing this, you must ensure that the target database has the profile and membership tables installed, which you can do using the aspnet_regsql.exe utility. Running this utility without any parameters brings up a user interface which walks you through installing the schema into an existing database, or creating a new default database, aspnetdb, to store all of ASP.NET 2.0's application services (membership, roles, profiles, Web part personalization, and the SQL Web event provider).

You can also use the command line parameters to install the database in an automated fashion (for example, if you are writing an install script for your application). For instance, to install all of the ASP.NET 2.0 application services into a new database named aspnetdb on the local machine (using Windows credentials to access the database), you would run the command:

aspnet_regsql -A all -C server=.;database=aspnetdb;trusted_connection=yes

Then, to change your ASP.NET application to use this new database to store Profile data (along with all other Application Service data), you would remove the LocalSqlServer connection string and then add it with a connection string pointing to your new database, as shown in Listing 4-28.

Listing 4-28. Changing the default database for Profile storage

<configuration>
 <connectionStrings>
  <remove name="LocalSqlServer" />
  <add name="LocalSqlServer"
       connectionString=
               "Server=.;Database=aspnetdb;trusted_connection=yes"/>
</connectionStrings>
<!--...-->

Serialization

As you saw earlier, the default serialization for properties stored in Profile is to write them out as strings, storing the property names and substring indices in the PropertyNames column. You can control how your properties are serialized by changing the serializeAs attribute on the add element in web.config. This attribute can be one of four values:

Binary
ProviderSpecific
String
Xml

The default is ProviderSpecific, which might better be called TypeSpecific since the type of the object will determine the format of its serialization. ProviderSpecific with the default SQL Provider implementation writes the property as a simple string if it is either a string already or a primitive type (int, double, float, etc.). Otherwise it defaults to XML serialization, which is a natural fallback because it will work with most types (even custom ones) without any modification to the type definition itself. So what ProviderSpecific really means is StringForPrimitiveTypesAndStringsOtherwiseXml, which is obviously quite a mouthful, so it's understandable they went with something shorter. This can lead to some confusing behavior if you're not aware of it, however. For example, consider the two Profile property definitions shown in Listing 4-29.

Listing 4-29. Sample Profile property definitions with invalid default values

<add name="TestCode" type="System.Net.HttpStatusCode"
     defaultValue="OK" /> <!-- defaultValue invalid -->
<add name="TestDate" type="DateTime"
     defaultValue="1/1/2006"/> <!-- defaultValue invalid -->

After using integer and string profile properties, adding an enum and a DateTime in this manner seems reasonable. Because the default serialization is ProviderSpecific, we now know that these two types will be serialized with the XmlSerializer, so specifying default values as simple strings is not going to fly (as you will find out quickly once you try accessing the properties). You have two ways of dealing with this problem. One is to specify the XML-serialized value directly in the configuration file (taking care to escape any angle brackets), as shown in Listing 4-30.

Listing 4-30. Specifying XML-serialized default values

<add name="TestCode" type="System.Net.HttpStatusCode"
     defaultValue=
        "&lt;HttpStatusCode&gt;OK&lt;/HttpStatusCode&gt;" />
<add name="TestDate" type="DateTime"
     defaultValue=
            "&lt;dateTime&gt;2006-01-01&lt;/dateTime&gt;" />

The other (and perhaps more appealing) option is to change the serialization from ProviderSpecific (which we know turns into XML) to String. String serialization only works for types that have TypeConversions defined for strings, which in our case is true since both enums and the DateTime class have string conversions defined (we discuss how to write your own string conversions in the next section). If you look carefully at Listing 4-22, you will notice that it specifies a serializeAs attribute of String for the HttpStatusCode so that a simple string default value of "OK" could be used, as shown in Listing 4-31.

Listing 4-31. Specifying string default values

<add name="TestCode" type="System.Net.HttpStatusCode"
     serializeAs="String"
     defaultValue="OK" />
<add name="TestDate" type="DateTime"
     serializeAs="String"
     defaultValue="2006-01-01" />

The other option for serialization is Binary, which will use the BinaryFormatter to serialize the property. With the default SQL provider, this will write the binary data into the database's PropertyValuesBinary column. This is a useful option if you want to make it difficult to tweak the profile values directly in the database, or if you are storing types whose entire state is not properly persisted using the XmlSerializer (classes with private data members that are not accessible through public properties fall into this category, for example). Before you can use the binary option, the type being stored must be marked with the Serializable attribute or must implement the ISerializable interface. Keep in mind that selecting the binary serialization option makes it impossible to specify a default value, so it is typically used only for complex types for which a default value doesn't usually make sense anyway. If you ever do need to specify a default value for binary serialization, it is technically possible by base64 encoding a serialized instance of the type and using the resulting string in the defaultValue property.

User-Defined Types as Profile Properties

One of the advantages of the Profile architecture is that it is generic enough to store arbitrary types and, as we have seen, it supports several different persistence models. This means that it is quite straightforward to write your own classes to store user data and then store the entire class in Profile. Suppose, for example, we wanted to provide a shopping cart for users to let them collect items to purchase. We might write a class to store an individual item containing a description and a cost, and another class that keeps a list of all of the items in the current cart as well as exposing a property that calculates the total cost of all items in the cart, as shown in Listing 4-32.

Listing 4-32. Sample ShoppingCart class definition

namespace PS
{
    [Serializable]
    public class ShoppingCart
    {
        private List<Item> _items = new List<Item>();

        public Collection<Item> Items
        {
            get { return new Collection<Item>(_items); }
        }

        public float TotalCost
        {
            get
            {
                float sum = 0F;
                foreach (Item i in _items)
                    sum += i.Cost;
                return sum;
            }
        }
    }

    [Serializable]
    public class Item
    {
        private string _description;
        private float  _cost;

        public Item() : this("", 0F) { }

        public Item(string description, float cost)
        {
            _description = description;
            _cost = cost;
        }

        public string Description
        {
            get { return _description;  }
            set { _description = value; }
        }

        public float Cost
        {
            get { return _cost;  }
            set { _cost = value; }
        }
    }
}

Note that our classes are marked with the [Serializable] attribute in anticipation of using binary serialization (although the XmlSerializer will work fine with these classes as well, so we have both options at our disposal). We can then add a profile property of type ShoppingCart to our collection, and we have a fully database-backed per-client persistent shopping cart implemented!

<profile enabled="true">
  <properties>
    <add name="ShoppingCart" type="PS.ShoppingCart"
         allowAnonymous="true" />
  </properties>
</profile>

Using the shopping cart in our application is as simple as accessing the ShoppingCart property in Profile and adding new instances of the Item class as needed (the sample available for download has a complete interface for users to shop using this class as the storage mechanism).

Profile.ShoppingCart.Items.Add(
         new Item("Chocolate covered cherries", 3.95F));

Optimizing Profile

You may be wondering at this point what sort of cost is incurred by leveraging Profile to store your per-client data, especially if you start using complex classes like the ShoppingCart, which may end up storing significant amounts of information on behalf of each user. Those of you who have taken advantage of the SQL Server-backed Session state feature introduced in ASP.NET 1.0 may be especially leery, since by default each request for a page incurred two round trips to the state database to retrieve and then flush session from and to the database. The good news is that by default, the profile persistence mechanism is reasonably efficient. Unlike out-of-process Session state, it performs lazy retrieval of the profile data on behalf of a user (loading on demand only), and only writes the profile data back out if it has changed.

Unfortunately, if you are storing anything besides strings, DateTime classes, or primitive types, it becomes impossible for the ProfileModule to determine whether the content has actually changed, and it is forced to write the profile back to the data store every time it is retrieved. This is obviously true for custom classes as well, so be aware that adding any types beside string, DateTime, and primitives will force Profile to write back to the database at the end of each request that accesses Profile. Internally there is a dirty flag used to keep track of whether a property in Profile has changed or not. You can explicitly set the IsDirty property for a profile property to false. If you do this for all properties associated with a specific provider instance, then when that provider instance is asked to save the profile data, it will see that all the properties passed to it are not dirty and it will skip communication with the database. This approach relies on knowledge of the underlying SettingsBase, SettingsProperty, and SettingsPropertyValue types (all in System.Configuration). For a profile property called Nickname, you could force it to not be considered dirty, as shown in Listing 4-33.

Listing 4-33. Setting the IsDirty attribute for a property in a custom class

Profile.PropertyValues["Nickname"].IsDirty = false;

Note that you can disable automatic profile saving using the automaticSaveEnabled attribute on the <profile/> element in the configuration file (this attribute defaults to true). You can set automaticSaveEnabled to false to stop ProfileModule from storing the Profile on your behalf automatically. It is then up to you to call Profile.Save if you want to store data back to the database. Alternatively, you can hook the ProfileModule's ProfileAutoSaving event. If you set the ContinueWithProfileAutoSave property on the event argument to false, then the ProfileModule will not call Profile.Save.

As you saw earlier, it is possible to specify String, Binary, or Xml as the serialization mechanism for your properties. If you are storing your own custom classes like our ShoppingCart example, you can take steps to reduce the amount of space used to store instances of your class in one of two ways: writing your own TypeConverter for the class to support conversion to string format, or implementing the ISerializable interface to control the format of the binary data used by the BinaryFormatter. Listing 4-34 shows the default serialization of our ShoppingCart class with four items in it in XML format. The equivalent binary serialization occupies 678 bytes of space.

Listing 4-34. XML-serialized shopping cart with four items (590 characters)

<?xml version="1.0" encoding="utf-16"?>
<ShoppingCart xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 <Items>
<Item>
  <Description>Chocolate covered cherries</Description>
  <Cost>3.95</Cost>
</Item>
<Item>
  <Description>Toy Train Set</Description>
  <Cost>49.95</Cost>
</Item>
<Item>
  <Description>XBox 360</Description>
  <Cost>399.95</Cost>
</Item>
<Item>
  <Description>Wagon</Description>
  <Cost>24.95</Cost>
</Item>
 </Items>
</ShoppingCart>

By default, you cannot use the serializeAs="String" option for custom types, since there is no way to convert the types to and from a string format in a lossless way. You can provide such a conversion yourself by implementing a TypeConverter for your class. This involves creating a class that inherits from TypeConverter, implementing the conversion methods, and then annotating your original class with the TypeConverter attribute that associates it with your conversion class. You must also decide on how to persist your class as a string (and then parse it from a string), which can be a nontrivial task, so make sure it's worth the effort before taking this approach. As an example, here is a TypeConverter class for the Item class that represents items in our shopping cart. In this case I chose to use a non-printable character as a delimiter, and since the Item class consists of two pieces of state which are easily rendered as strings, the parsing becomes trivial using the Split method of the string class. The converter class is then associated with the Item class using the TypeConverter attribute, both of which are shown in Listing 4-35.

Listing 4-35. Writing a custom type converter

    public class ItemTypeConverter : TypeConverter
    {
        private const char _delimiter = (char)10;

        public override object
             ConvertFrom(ITypeDescriptorContext context,
                    CultureInfo culture, object value)
        {
             string sValue = value as string;
             if (sValue != null)
             {
                 string[] vals = sValue.Split(_delimiter);
                 return new Item(vals[0],
                             float.Parse(vals[1]));
             }
             else
                 return base.ConvertFrom(context,
                                      culture, value);
         }
         public override object
                ConvertTo(ITypeDescriptorContext context,
                             CultureInfo culture,
                          object value, Type destinationType)
         {
             if (destinationType == typeof(string))
             {
                 Item i = value as Item;
                 return string.Format("{0}{1}{2}",
                         i.Description, _delimiter, i.Cost);
             }
             else
             {
                 return base.ConvertTo(context, culture,
                         value, destinationType);
             }
         }
         public override bool CanConvertFrom(
                              ITypeDescriptorContext context,
                              Type sourceType)
         {
             if (sourceType == typeof(string))
                 return true;
             else
                 return base.CanConvertFrom(
                               context, sourceType);
         }
         public override bool CanConvertTo(
                              ITypeDescriptorContext context,
                              Type destinationType)
         {
             if (destinationType == typeof(string))
                 return true;
             else
                 return base.CanConvertTo(
                             context, destinationType);
         }
     }

     [Serializable]
     [TypeConverter(typeof(ItemTypeConverter))]
     public class Item
     {
       ...

With these classes in place, our Item class can be used with string serialization in a profile property. Note that for our shopping cart to be completely serializable as a string, we also need to write a type converter for our ShoppingCart class, a sample of which can be found in the downloadable samples for this book. The advantage of controlling the persistence in this way is that the serialization of the same shopping cart filled with four items now only takes 79 characters!

Going the Custom Route

Any time you find yourself spending a lot of time trying to make an architecture do what you want, it is important to step back and make sure that the work necessary to customize the architecture to do what you want is less than what it would take to do it entirely yourself. Profile is a great example of a feature that is convenient and easy to use but that may be too constraining as your design evolves. Let's look at what features Profile specifically gives us.

  • Support for anonymous and authenticated clients
  • Anonymous users identified through a new cookie (or alternatively through embedded id with URL mangling, including support for autodetect cookieless mode)
  • Arbitrary type storage, strongly typed through configuration file
  • Per-client persistent data store
  • Management class for cleaning up unused profile data

One of the drawbacks to using Profile to store client data is that it stores all of the data in one column (or two columns if you are using both string and binary serialization) of the database table. This means that it is practically impossible to make modifications to the profile data without going through the profile API. It's also impractical to generate any reports from the data or otherwise collect information from the database directly.

If you find yourself wanting more control over the storage of per-client state in your application, you have two choices: build a custom Profile provider or forget Profile and just write data yourself. Building a custom Profile provider gives you the ability to retarget where Profile actually writes the data, but because of the nature of the provider interface, it doesn't really make it any easier to write property values to specific columns in a table. For more information and samples on building custom Profile providers, take a look at the ASP.NET provider model toolkit (http://msdn.microsoft.com/asp.net/downloads/providers/default.aspx).

If you decide to forget Profile and just write the serialization of client data yourself, be aware that you can still leverage the identification features of Profile even if you aren't using the storage features. Specifically, there is a UserName property on the ProfileBase class that will contain either the name of the current authenticated user or the GUID that was generated for an anonymous user. You can use this UserName property as a unique index into a custom database table of your own construction to easily store and retrieve user data. Just make sure that Profile and anonymousIdentification are enabled in your application, and you can use the same client identification mechanism as Profile.

<anonymousIdentification enabled="true"/>
        <profile enabled="true" />

By writing your own client persistence backend using the unique identifier provided by Profile, you gain several unique advantages over the generic profile implementation.

  • The ability to write stored procedures against client data
  • The ability to retrieve only the portions of data you need for a client at any given time (instead of relying on Profile to just load the whole chunk into memory)
  • The ability to cache per-client data across requests for efficiency
  • Complete control over the serialization, and the ability to map onto existing tables instead of creating new data stores that you may already have in place

The sample available for download contains an alternate implementation of the shopping cart described earlier, using a custom database table to store cart items and leveraging the unique client identifier available through the ProfileBase class. In general, you may even consider starting out by using Profile for convenience to get things started, and then later migrate some of the profile data into custom tables with a separate data access layer. In this sense, Profile fills a convenient role as an easy way to store per-client data, with an obvious path forward to factoring data out into a more strongly typed schema.

  • + Share This
  • 🔖 Save To Your Account