Cutting Edge

Personalization and User Profiles in ASP.NET 2.0

Dino Esposito

Contents

What Personalization Means to You
Managing Personalization Data
Managing User Profiles
Data Storage
Pages and User Profiles
Handling Anonymous Users
Internal Implementation
Conclusion

Personalization is growing to be more and more of an essential ingredient in many types of Web apps, including portals and shopping sites. Without it, it's quite difficult to serve your customers efficiently. But building a personalization layer in your presentation tier can be a challenge. You need a persistent storage medium that is easy to manage and does not consume too many resources, you need an API to let page authors interact with the personalization store, and you need a data model that is easy to use and that provides type safety. Finally, you must provide both administrators and end users with tools to manage stored data and settings. As my March 2004 column demonstrated, building a personalization layer in ASP.NET 1.x applications is definitely possible, but is tricky nonetheless. Thankfully, ASP.NET 2.0, provides new facilities to help you build your personalization layer with ease.

What Personalization Means to You

ASP.NET personalization features include user profiles, themes, error pages, and lots more. The ASP.NET user profile is designed for persistent storage of structured data through a type-safe API. To make use of personalization features, your application defines its own model of personalized data, and the ASP.NET runtime does the rest by parsing and compiling that model into a class. Each member of the personalized data class corresponds to a piece of information specific to the current user. Loading and saving personalized data is completely transparent to end users and doesn't even require the page author to know much, if anything, about the internal plumbing. Figure 1 shows an example of the kind of personalized site I will be discussing here.

Figure 1 Sample Page Consuming Profile Information

Figure 1** Sample Page Consuming Profile Information **

Themes in ASP.NET personalization assign a set of styles and visual attributes to the elements of the site that can be customized. These elements include control properties, page stylesheets, images, and templates. Custom error pages provide users with important information when an unhandled exception causes an error and stops the application. Next month I'll discuss themes and error pages, but for now let's look at user profiles.

Managing Personalization Data

While applying themes and attaching custom error pages is mostly a matter of editing the Web.config file with administrative privileges, managing the user profile of the currently logged-in user requires additional capabilities. In particular, a system-level infrastructure needs to retrieve and load the user's data when the request begins its processing. At the end of the request, the current settings should be persisted on the server to be retrieved with the next request issued by the same user.

Page settings can include any relevant, user-specific information such as a list of favorite links, the name of the selected theme, position of UI panels, and configurable strings. When necessary, an administrator can access the physical storage and edit user settings (this ability, however, depends on the provider in use; the default SqlProfileProvider stores data as opaque text or image blobs, so direct editing of the data is not easily done). To allow individual users to edit preferences, you need to provide a user interface. The most common way to do this is to add a few links in fixed areas of the page that when clicked cause the page to postback and an edit panel containing current values and controls to be displayed. A similar approach is implemented by My MSN® and SharePoint®-powered sites. The edit panels can be custom controls, or better yet, user controls. I'll return to this topic later in this column and provide a concrete example.

Managing User Profiles

A user profile is a collection of values that the ASP.NET 2.0 runtime groups as public fields of a dynamically generated class. The class is derived from a system-provided class and is extended with the addition of a few new members. The class doesn't have to be marked as Serializable, however its contents are persisted to the storage medium as individual properties. The storage occurs on a per-user basis and is preserved until an administrator deletes it.

The data storage is hidden from the user and, to some extent, from the programmers using the personalization APIs. The user doesn't need to know how and where the data is stored; the programmer simply needs to indicate what type of profile data provider she wants to use. The profile provider determines the store to use—typically, a database system like SQL Server™—and the layout of the data in the store. However, custom providers and custom storage models can also be used.

In ASP.NET 2.0 Beta 2, which I'm using for all examples in this column, the default profile provider utilizes SQL Server Express Edition, a free edition of SQL Server 2005. The physical storage medium is a local file named aspnetdb.mdf, usually located in the App_Data folder of the Web application.

When the application runs and a page is displayed, ASP.NET dynamically creates a profile object that contains properly typed data and assigns the current settings for the logged user to the properties defined in the data model. The profile object is added to the current HttpContext object and made available to pages through the Profile property. Assuming that the profile model has been defined to store a list of links as a collection, the following code snippet shows how to retrieve the list of favorite links that are created by a given user:

If Not IsPostBack Then
    ' Add some default links to the Links property
    If Profile.Links.Count = 0 Then
        Profile.Links.Add("https://www.contoso.com")
        Profile.Links.Add("https://www.northwind.com")
    End If
End If

This code assumes a Links property in the profile object that references a collection type. When the page is loaded, Links and other properties are initialized to contain the most recently stored values; when the page is unloaded their current contents are stored to the persistent medium.

Your first step on the way to building a personalization layer is to define the layout of the profile object you want to use. You do this by adding a section to the app's Web.config file. In the end, the data model is a block of XML data that describes properties and related types. It consists of a list of properties, their names, and the corresponding managed type to be used to represent the property.

You add properties to the profile storage medium through name/value pairs. You define each pair by adding a new property tag to the <properties> section of the configuration file. The <properties> section is itself part of the larger <profile> section, which also includes provider information. The <profile> section, in turn, is located under <system.web>. The following code shows an example of a user profile section:

<profile>
   <properties>
      <add name="BackColor" type="string" />
      <add name="ForeColor" type="string" />
      <add name="Links" 
           type="System.Collections.Specialized.StringCollection" 
           serializeAs="Xml" />
   </properties>
</profile>

All the properties defined through an <add> tag become members of the dynamically created profile class. The type attribute indicates the type of the property. If no type information is set, the type defaults to System.String. Any managed type is acceptable. Figure 2 details the attributes supported by the <add> element. Of all of them, only the name attribute is mandatory. As you can guess from the table in Figure 2, there's a clear relationship between user accounts and profile data. In fact, a personalization layer is mostly useful in applications that require users to register and log in. However, the ASP.NET user profile API supports anonymous users as well and provides the tools to migrate anonymous settings to a registered account (more on this in a moment).

Figure 2 The <add> Element for the <profile> Section

Attribute Description
allowAnonymous False by default, enables the infrastructure to store values for anonymous users.
customProviderData Contains initialization data, if any, for a custom profile provider.
defaultValue Indicates the default value of the property, if any.
name Mandatory attribute, indicates the name of the property.
provider Indicates the name of the provider to use to read and write the values of profile properties.
readOnly False by default, indicates whether the property value is read-only.
serializeAs Indicates how to serialize the value of the property. Possible values are Xml, Binary, String, and ProviderSpecific.
type Indicates the type of property. Defaults to System.String and accepts any CLR type.

Scalar values such as strings, numbers, and dates are stored as pure string values without further manipulation. The personalization engine, though, fully supports more advanced scenarios such as using collections or custom types. Nonscalar values such as collections and arrays must be serialized to fit in a data storage medium. The serializeAs attribute I mentioned earlier in this column specifies how. Acceptable values for serialization are String, Xml, Binary, and ProviderSpecific. If the serializeAs attribute is not present on the <properties> definition, the String type is assumed. A collection is normally serialized as XML or in a binary format. The XmlSerializer is used when XML is selected, and the BinaryFormatter is used when binary is selected.

You can use a custom type with the ASP.NET personalization layer as long as it can be serialized by the relevant serialization engine (XmlSerializer or BinaryFormatter). You simply author a class and compile it down to an assembly. You then reference the assembly-qualified type name in the configuration file:

<properties>
   <add name="ShoppingCart" 
        type="My.Namespace.DataContainer, MyAssem" 
        serializeAs="Binary" />
</properties>

The assembly that contains the custom type must be available to the ASP.NET application either in the application's Bin directory or in the Global Assembly Cache (GAC).

While defining the profile object, you can also group multiple properties under a common container property. The <group> element allows you to create sub-objects:

<properties>
   ...
   <group name="Font">
      <add name="Name" type="string" defaultValue="verdana" />
      <add name="SizeInPoints" type="int" defaultValue="8" />
   </group>
</properties>  

In this case, the font properties have been declared children of the Font group. This means that any access to Name or SizeInPoints passes through the Font name, as shown here:

Dim fontName as String = Profile.Font.Name

After creating the data model, you write your pages to read and edit the values in the profile object during execution. Before you can successfully test your first personalizable page, one more step is required—setup of the data storage.

Data Storage

To enable or disable profile support, you set the enabled attribute of the <profile> element in the Web.config file. If the property is true (the default setting), personalization features are enabled for all pages. Note that enabling the feature merely turns the functionality on; in no way does it create the infrastructure for user membership and data storage.

ASP.NET 2.0 comes with the ASP.NET Web Site Administration Tool (WSAT), which is fully integrated into the Visual Studio® 2005 IDE, as shown in Figure 3. You can use this tool to create a default database to store profile data. The default database is a SQL Server 2005 file named aspnetdb.mdf located in the App_Data special folder of the ASP.NET application. Tables and database schema are fixed. The same database contains tables to hold membership and roles information. The use of a membership database with users and roles is important because personalization is designed to be user-specific and because a user ID—either an app-specific logon or a Windows account—is necessary to access data.

Figure 3 ASP.NET Web Site Administration Tool

Figure 3** ASP.NET Web Site Administration Tool **

Profile data has no predefined lifetime and is permanently stored. It is up to the Web site administrator to delete the information when convenient. Note that WSAT is just one option for setting up the profile infrastructure. It creates the database as a side effect of attempting to use the security features, which in turn automatically causes the creation of the necessary Profile database tables. If you're using a custom provider, the setup of your application is responsible for preparing any required storage infrastructure, be it a SQL Server table, an Oracle database, or whatever else.

Pages and User Profiles

A page designed to take advantage of profiles must place a call to an internal module that styles and moves elements accordingly. Figure 4 represents the skeleton of a Page class designed with personalization in mind. The Page class overrides OnLoad to automatically invoke InitProfile and InitEditor—two new protected and overridable methods. The InitProfile method ensures that all members of the page-specific profile object are properly initialized to respective default values. To set up default values for reference types, you can either place the XML serialized representation in the defaultValue attribute, or for binary serialization you can store the base64-encoded representation of the binary blob. Be aware that if no default value is specified, reference-type properties will be constructed using a parameterless constructor if one exists and is accessible. If no appropriate constructor exists and if a default value isn't supplied, the property will initially be null. So, if you manage reference types in your data model, you typically make use of a page-specific initialization step that assigns profile properties proper default values.

Figure 4 Skeleton of a Profile Page Class

Public Class ProfilePage
    Inherits System.Web.UI.Page

    Protected Overrides Sub OnLoad(ByVal e As EventArgs)
        If Not IsPostBack Then
            InitProfile()
            InitEditor()
        End If
        ApplyPagePersonalization()
    End Sub

    Protected Overridable Sub InitProfile()
    End Sub

    Protected Overridable Sub InitEditor()
    End Sub

    Protected Overridable Sub ApplyPagePersonalization()
    End Sub

    Protected Overridable Sub ShowEditor()
    End Sub

    Protected Overridable Sub CloseEditor()
    End Sub
End Class

If users are allowed to indicate their preferences, you should also give them a chance to edit values while working with the page. If you're familiar with Web Parts (in either SharePoint or ASP.NET 2.0), you know what I mean. Web Parts incorporate UI editors—visual panels with input controls to gather user's preferences. The UI editor is typically activated by clicking a menu item or a hyperlink. You should provide this infrastructure in any page that supports user profiles. The InitEditor method gives you a chance to properly initialize the input fields of the editor. ShowEditor and CloseEditor open and close the editor, respectively. The editor can be any control, or combination of controls, within the page—typically a user control, a panel, or a multi-view control.

Figure 5 UI Editor for User Preferences

Figure 5** UI Editor for User Preferences **

In Figure 1 you saw a sample page that makes use of profile information. As you can see, different users are served slightly different pages. Each page contains a link to edit preferences, as in Figure 5. In Figure 6, you see the markup to render the editor via a MultiView control. In ASP.NET 2.0, a MultiView control contains several mutually exclusive panels, only one of which is visible at a time. The sample multi-view control lists a view to invite users to edit and a view that contains the real editor. (See the source code accompanying this column for further details.)

Figure 6 MultiView Control Implements a UI Editor

<asp:MultiView runat="server" ID="Options" ActiveViewIndex="0">
     <asp:View runat="server" ID="MenuOptions">
          <asp:LinkButton runat="server" text="Click here to edit" 
              OnClick="OnShowEditor" /> 
     </asp:View>

     <asp:View runat="server" ID="ChangeOptions">
          <h2>Options</h2>
          <x:PageEditor runat="server" />
          <asp:LinkButton ID="LinkButton1" runat="server" text="Close" 
              OnClick="OnCloseEditor" /> 
     </asp:View>
</asp:MultiView>

Handling Anonymous Users

Although user profiles are designed primarily for authenticated users, anonymous users can also store profile data. In this case, though, a few extra requirements must be fulfilled. In particular, you have to turn on the anonymousIdentification feature, which is disabled by default:

<anonymousIdentification enabled="true" />

Anonymous user identification, a new feature of ASP.NET 2.0, assigns a unique, false identity to users who are not authenticated. Note that anonymous identification in no way affects the identity of the account that is processing the request. Likewise, it doesn't affect any other aspects of security and authentication. Anonymous identification is simply a method used for assigning an ID to unauthenticated users so they can be tracked as regular users for the purposes of personalization.

Individual profile properties should be explicitly enabled to work with unauthenticated users. If not, an exception will be thrown when the Profile object is first accessed. To support anonymous identification you mark properties in the data model with the special Boolean attribute named allowAnonymous. Properties that are not marked with the attribute are not made available to anonymous users, as shown here:

<anonymousIdentification enabled="true" />
<profile enabled="true">
   <properties>
      <add name="BackColor" type="System.Drawing.Color" 
           allowAnonymous="true" />
      ...
   </properties>
</profile>

Based on this code snippet, anonymous users can set the background color but not add new links.

Note that each anonymous user gets a unique anonymous ID the first time they visit a site. This ID is stored persistently in a cookie. Thus, if a second user opens up a browser on the same computer as the first, the second user will be seen by the site as having the same anonymous ID that was issued to the first user.

If at a certain point a hitherto anonymous user decides to create an account with the Web site, you might need to migrate all of the settings currently associated with the anonymous account to the user's new account. Note that this migration doesn't occur automatically and is not required in all cases. However, ASP.NET 2.0 provides system-level support for this possibility. When a user who has been using your application anonymously logs in, the application receives the MigrateAnonymous event. The event is not local to the page and should be handled in the global.asax file. The goal of a handler of this event is to copy the values of profile properties from the anonymous profile to the profile of the current user. The following pseudocode demonstrates how you can handle the migration of an anonymous profile:

Sub Profile_MigrateAnonymous( _
        ByVal sender As Object, ByVal e As ProfileMigrateEventArgs)
    Dim anon As ProfileCommon = Profile.GetProfile(e.AnonymousID)

    ' Migrate the properties to the new profile
    Profile.BackColor = anon.BackColor
    ...
End Sub

A typical handler performs two steps. First, it gets the profile information for the anonymous user. The profile information is represented by an object of type ProfileCommon. You get a reference to this object by calling the GetProfile method on the current profile object. You need to specify the fake ID generated for the anonymous user in order to retrieve the right set of data. The ID of the anonymous user is available through the AnonymousID property of the event data structure. Next, it simply copies each property value of interest to the profile of the currently logged-on user.

Afterwards, the developer might want to handle two additional cleanup tasks. First, you can use the ProfileManager to delete the profile data for the anonymous user to prevent it from cluttering up the database. Second, unless you need access to the AnonymousID after the user has logged in (and some developers may have a valid need for this), you should call AnonymousIdentificationModule.ClearAnonymousIdentifier. This will clear either the cookie or the cookie-less representation of the anonymous ID. Doing so prevents the MigrateAnonymous event from constantly firing on each page after the user has logged in. Note that since Anonymous Identification supports a cookieless mode of operation, this method is the only way to get rid of the anonymous identifier in the case of cookieless clients.

Internal Implementation

An HTTP module named ProfileModule lives behind the user profile feature in ASP.NET 2.0. The module is responsible for adding any profile data to the HTTP context of a request before the request begins its processing route. The module attaches itself to a couple of HTTP pipeline events: AcquireRequestState and EndRequest. The module kicks in after a request has been authorized and when the request is about to end.

The code that handles the AcquireRequestState event returns immediately if the personalization feature is off. Otherwise, the module fires a global event (the Personalize event) and then loads personalization data from the storage medium for the current user. Like any other event fired by a HTTP module, the Personalize event is handled in the global.asax file. When the Personalize event fires, the personalization data hasn't been loaded yet. So what's the purpose of the Personalize event?

ASP.NET manages profile data on a rigorous per-user basis. What if you have plenty of users with different names but all sharing the same profile data? Maintaining hundreds of nearly identical database entries doesn't sound like a smart approach. However, this seems to be the only possible approach using the standard mechanism. The standard profile engine, in fact, doesn't know how to handle roles. That's where the Personalize event is really handy. In Figure 7, you see a sample Personalize handler to override the process that creates the user profile object. The handler replaces the user profile with a profile that represents the role to which the particular user belongs. The static method Create on the ProfileBase class takes a name and creates an instance of the profile object specific to that user name. As you can see, the code in Figure 7 doesn't use the name of the logged-on user but checks if the user belongs to a role and uses the role name instead.

Figure 7 Handling the Personalize Global Event

Sub Profile_Personalize( _
        ByVal sender As Object, ByVal e As ProfileEventArgs)
    Dim profile As ProfileBase = Nothing

    ' Exit if it is the anonymous user
    If User Is Nothing Then Return

    ' Determine the profile based on the role. The profile database
    ' contains a specific entry for a given role.
    If User.IsInRole("Administrators") Then
        profile = ProfileBase.Create("Administrator")
    ElseIf User.IsInRole("Users") Then
        profile = ProfileBase.Create("User")
    ElseIf User.IsInRole("Guests") Then
        profile = ProfileBase.Create("Guest")
    End If

    ' Make the HTTP profile module use this profile object
    If profile IsNot Nothing Then e.Profile = profile
End Sub

The handler of the Personalize event receives data through the ProfileEventArgs class. The class has a read-write member named Profile. When the event handler returns, the profile HTTP module checks this member. If it is null, the module proceeds as usual, telling the runtime to delay load the Profile based on the user's identity the first time it is needed (this delay loading prevents the overhead of loading and deserializing the Profile on pages that never use it). If not, it simply binds the current value of the Profile member as the profile object of the page.

ProfileBase.Create actually returns an instance of a type derived from ProfileBase, ProfileCommon. The data model defined in the Web.config file is transformed into a C# or Visual Basic® .NET class by the ASP.NET runtime. The name of this dynamically created class is ProfileCommon and it inherits from ProfileBase, as shown in Figure 8.

Figure 8 ProfileCommon Class

Public Class ProfileCommon
    Inherits ProfileBase

    Public Sub New()
    End Sub

    Public Overridable Function GetProfile( _
            ByVal username As String) As ProfileCommon
        Return CType(ProfileBase.Create(username), ProfileCommon)
    End Function

    Public Overridable Property BackColor() As String
        Get
            Return CType(MyBase.GetPropertyValue("BackColor"), String)
        End Get
        Set(ByVal value As String)
            MyBase.SetPropertyValue("BackColor", value)
        End Set
    End Property
    ...
End Class

A dynamically created class like ProfileCommon is key to having type-safe access to profile properties. The ProfileBase class also contains a Save method that automatically persists the properties back to the storage medium when the request completes. The page author doesn't have to worry about loading and saving profile data as everything is taken care of by the runtime environment and governed by the HTTP module. The unique point of contact between pages and the profile infrastructure is the Profile property on the Page class.

The ASP.NET 2.0 profile API is composed of two distinct elements—the access layer and the storage layer. As I said, the access layer provides a strongly typed model to get and set property values and manage user identities. It guarantees that the data is retrieved and stored on behalf of the currently logged-on user.

The data storage employs ad hoc providers to perform any tasks that involve the storage and retrieval of values. ASP.NET 2.0 Beta 2 comes with a default profile provider, AspNetSqlProfileProvider, which uses SQL Server 2005 Express Edition as the data engine. The profile provider writes data into the storage medium of choice and is responsible for the final schema of the data. A profile provider must be able to either serialize the type (by using XML serialization and binary object serialization, for example) or know how to extract significant information from it. If necessary, you can also write custom providers.

AspNetSqlProfileProvider is good at building new applications and good for profile data that is inherently tabular. In many cases, though, you won't start an ASP.NET 2.0 application from scratch but will instead migrate an existing ASP or ASP.NET application. You often already have data to integrate with the ASP.NET profile layer. If this data doesn't get along with the relational model, or if it is already stored in a storage medium other than SQL Server, you can write a custom profile provider.

Profile providers push the idea that existing data stores can be integrated with the personalization engine using a thin layer of code. This layer of code abstracts the physical characteristics of the data store and exposes its content through a common set of methods and properties. A custom profile provider is a class that derives from ProfileProvider. You register it as follows:

<profile>
  <providers>
     <add name="MyProvider" 
          type="MyNamespace.MyProfileProvider, MyAssem" />
  </providers>
</profile> 

As a final note, consider that each property in the data model can be handled by a distinct provider through the use of the provider attribute in the <add> tag:

<properties>
   <add name="BackColor" type="string" provider="MyProvider" />
   ...
</properties>

As shown, the BackColor property is read and written through the MyProvider provider. Needless to say, the provider name must correspond to one of the entries in the <providers> section.

Conclusion

A personalization layer is a general-purpose tool for holding user-specific information. This is usually information that applies to the user, but not necessarily information entered by the user.

The ASP.NET user profile API allows you to write pages that persist user preferences and parametric data to a permanent medium in a totally automated way. As a programmer, you're in charge of setting up the personalization infrastructure, but you need not know anything about the internal details of storage. All you do is call a provider component using the methods of a well-known interface. Of course, the personalization capabilities of ASP.NET 2.0 pages don't end here. Next month, I'll be back with themes and custom error pages.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is an instructor and consultant based in Rome, Italy. Author of Programming ASP.NET and the new book Introducing ASP.NET 2.0 (both from Microsoft Press), he spends most of his time teaching classes on ASP.NET and ADO.NET and speaking at conferences. Get in touch with Dino at cutting@microsoft.com or join the blog at weblogs.asp.net/despos.