Storing User Information with ASP.NET 2.0 Profiles

 

Stephen Walther
Superexpert

August 2004

Updated March 2005

Applies to:
   Microsoft ASP.NET 2.0
   Microsoft Visual Studio 2005

Summary: Many ASP.NET applications need to track user preferences across multiple visits. In ASP.NET 1.x, adding this functionality was a manual process. With the new Profile object in ASP.NET 2.0, this becomes a simple process. Stephen Walther examines this object, and shows you how to use it to track user preferences, create a shopping cart, and more. (44 printed pages)

Contents

Introduction
User Profiles Overview
Defining User Profiles
Using Profile Groups
Using Complex Profile Properties
Inheriting a Profile
Migrating Anonymous Profile Settings
Configuring Profile Providers
Managing Profiles and Generating Profile Reports
Conclusion
Related Books

Introduction

Microsoft ASP.NET 2.0 supports a new object called the Profile object. This new object enables you to automatically store user information across multiple visits to a Web application. You can store any type of information within a user profile including both simple data types such as strings and integers and complex types such as custom objects. For example, you can use profiles to store user last names, shopping carts, user preferences, or Web site usage statistics.

In this article, you'll learn how to define user profiles for an application. We'll also examine how you can configure profiles to work with different providers. Finally, you'll learn how to manage and generate reports on user profiles.

User Profiles Overview

The Profile object is similar to the Session object, but better. Like the Session object, the Profile object is scoped to a particular user. In other words, each user of a Web application automatically has their own profile.

However, unlike the Session object, the Profile object is persistent. When you add an item to the Session object, the item disappears after you leave the Web site. When you modify the state of the Profile object, in contrast, the modifications are saved between visits to the Web site.

The Profile object uses the provider model to store information. By default, the contents of a user profile are automatically saved to a Microsoft SQL Server Express database located in the App_Data folder of your Web application. However, as we'll see later in this article, you can store profile information by using other data providers such as the full version of Microsoft SQL Server or even an Oracle database.

Furthermore, unlike the Session object, the Profile object is strongly typed. The Session object is simply a collection of items. In contrast, the Profile object exposes strongly typed properties.

Using strongly typed properties has several advantages. For example, you get full Microsoft IntelliSense when using the Profile object in Microsoft Visual Web Developer. When you type Profile followed by a dot, IntelliSense pops up with a list of all of the properties you have defined for the profile.

Defining User Profiles

You define a user profile within either the Machine.Config file or the application root Web.Config file. You cannot create a Web.Config file containing a profile section in an application subfolder. This means that you cannot define more than one profile for a single application.

The Web.Config file in Listing 1 contains a simple profile definition. This profile defines three properties named FirstName, LastName, and PageVisits.

Listing 1. Web.Config

<configuration>
 <system.web>
   <authentication mode="Forms" />
      
           <anonymousIdentification enabled="true" />
        
   <profile>
               <properties>
                  <add 
        name="FirstName"  
        defaultValue="??"
        allowAnonymous="true" />
      <add 
        name="LastName" 
        defaultValue="??"
        allowAnonymous="true" />
      <add 
        name="PageVisits"
        type="Int32" 
        allowAnonymous="true"/>
               </properties>
           </profile>
 </system.web>
</configuration>

This profile will be used with both anonymous and authenticated users. Therefore, the Web.Config file in Listing 1 includes an <anonymousIdentification> element, that automatically generates a unique ID for anonymous users. Notice also that all of the profile properties include an allowAnonymous attribute. This attribute is required when the profile properties are used with anonymous users.

The default data type for profile properties is the System.String data type. Since a type attribute is not provided for the FirstName and LastName profile properties, these properties are assumed to be String properties. The PageVisits property is assigned the type Int32 since it is used to represent an integer value.

Finally, notice that both the FirstName and LastName properties have defaultValue attributes. You can use the defaultValue attribute when working with simple data types such as strings and integers. You cannot provide default values for complex types.

After you define a profile, the next time a page is requested, a class that corresponds to the profile definition is automatically generated within your application. The automatically generated class is stored in the Temporary ASP.NET Files Directory (the same place the class files are stored for dynamically generated ASP.NET pages). The class is exposed by the Profile property of the HttpContext object.

After you define a profile in the Web.Config file, you can assign values to profile properties within any ASP.NET page like this:

[Visual Basic .NET]

Profile.FirstName = "Bill"

[C#]

Profile.FirstName = "Bill";

Any profile properties defined in the Web.Config file are exposed as properties of the Profile object.

The page in Listing 2 demonstrates how you can use a profile to persist user information. This page displays the values of the FirstName, LastName, and PageVisits properties. It also includes a form for modifying the values of the FirstName and LastName properties (see Figure 1). Finally, the Page_Load method updates the PageVisits profile property every time the page is requested.

ms379605.userprofiles_fig01(en-US,VS.80).gif

Figure 1. Using a simple profile

Listing 2. Simple.aspx (Visual Basic .NET)

<%@ Page Language="VB" %>
<script runat="server">

    Sub Page_Load()
        Profile.PageVisits += 1
    End Sub
    
    Sub UpdateProfile(ByVal s As Object, ByVal e As EventArgs)
        Profile.FirstName = txtFirstName.Text
        Profile.LastName = txtLastName.Text
    End Sub
    
</script>

<html>
<head>
    <title>Simple</title>
</head>
<body>
    <form id="form1" runat="server">
    <b>Name:</b> <%= Profile.FirstName %> <%= Profile.LastName %>
    <br />
    <b>Page Visits:</b> <%= Profile.PageVisits %>
    
    <hr />
    
    <b>First Name:</b>
    <asp:TextBox ID="txtFirstName" Runat="Server" />
    <br />
    <b>Last Name:</b>
    <asp:TextBox ID="txtLastName" Runat="Server" />
    <br />
    <asp:Button 
        Text="Update Profile" 
        OnClick="UpdateProfile" 
        Runat="server" />

    </form>
</body>
</html>

Listing 2. Simple.aspx (C#)

<%@ Page Language="C#" %>
<script runat="server">

    void Page_Load() {
        Profile.PageVisits ++;
    }
    
    void UpdateProfile(Object s, EventArgs e) {
        Profile.FirstName = txtFirstName.Text;
        Profile.LastName = txtLastName.Text;
    }
    
</script>

<html>
<head>
    <title>Simple</title>
</head>
<body>
    <form id="form1" runat="server">
    <b>Name:</b> <%= Profile.FirstName %> <%= Profile.LastName %>
    <br />
    <b>Page Visits:</b> <%= Profile.PageVisits %>
    
    <hr />
    
    <b>First Name:</b>
    <asp:TextBox ID="txtFirstName" Runat="Server" />
    <br />
    <b>Last Name:</b>
    <asp:TextBox ID="txtLastName" Runat="Server" />
    <br />
    <asp:Button ID="Button1" 
        Text="Update Profile" 
        OnClick="UpdateProfile" 
        Runat="server" />

    </form>
</body>
</html>

If you request the page in Listing 2 multiple times, you'll notice that the value of PageVisits increments. If you shutdown your Web browser, and request the page one week in the future, then the PageVisits property will retain its previous value. Profile properties are automatically saved for each user.

Using Profile Groups

You can define no more than one profile for an application. If you need to work with several profile properties, you might find it easier to work with the properties if you organize them into groups.

For example, the Web.Config file in Listing 3 contains a profile with two groups: Address and Preferences.

Listing 3. Web.Config

<configuration>
<system.web>
      
   <anonymousIdentification enabled="true" />
        
   <profile>
               <properties>
   <group name="Address">
                  <add 
         name="Street"  
         allowAnonymous="true" />
                 <add 
         name="City"  
         allowAnonymous="true" />
   </group>
   <group name="Preferences">
      <add 
         name="ReceiveNewsletter" 
         type="Boolean"
         defaultValue="false"
         allowAnonymous="true" />
   </group>
              </properties>
        </profile>
</system.web>
</configuration>

When you define a profile by using groups, you use the group names when setting and reading profile properties. For example, you would use the following syntax to assign values to the three profile properties defined by the profile in Listing 3:

[Visual Basic .NET]

Profile.Address.City = "Modesto"
Profile.Address.Street = "111 King Arthur Ln"
Profile.Preferences.ReceiveNewsletter = False

[C#]

Profile.Address.City = "Modesto";
Profile.Address.Street = "111 King Arthur Ln";
Profile.Preferences.ReceiveNewsletter = false;

A profile definition can only contain one level of groups. In other words, you cannot nest additional groups below a profile group.

Using Complex Profile Properties

To this point, we have declared profiles that contain properties with simple data types such as strings and integers. You also can declare complex types when creating profile properties.

For example, imagine that you want to store a shopping cart in a profile. If you store a shopping cart in the Profile object, then the shopping cart will be automatically available every time a user returns to your Web site.

Listing 4 contains a profile definition that has a property named ShoppingCart. Notice that the type attribute refers to a class named ShoppingCart (we'll create this class in a moment). The type attribute should refer to a valid class name.

Also notice that the definition includes a serializeAs attribute. This attribute is used to persist the ShoppingCart class using the binary serializer rather than the XML serializer.

Listing 4. Web.Config

<configuration>
<system.web>

  <anonymousIdentification enabled="true" />
  
  <profile>
    <properties>
    <add 
       name="ShoppingCart"
       type="ShoppingCart"
       serializeAs="Binary"
       allowAnonymous="true" />
    </properties>
  </profile>
</system.web>
</configuration>

Listing 5 contains the code for a simple shopping cart. This shopping cart has methods for adding and removing items from the shopping cart. It also contains a property that represents all of the items in the shopping cart and a property that represents the total price of all of the items in the shopping cart.

Listing 5. ShoppingCart (Visual Basic .NET)

Imports Microsoft.VisualBasic

<Serializable()> _
Public Class ShoppingCart
    Public _CartItems As New Hashtable()

    ' Return all the items from the Shopping Cart
    Public ReadOnly Property CartItems() As ICollection
        Get
            Return _CartItems.Values
        End Get
    End Property

    ' The sum total of the prices
    Public ReadOnly Property Total() As Decimal
        Get
            Dim sum As Decimal
            For Each item As CartItem In _CartItems.Values
                sum += item.Price * item.Quantity
            Next
            Return sum
        End Get
    End Property

    ' Add a new item to the shopping cart
    Public Sub AddItem(ByVal ID As Integer, _
      ByVal Name As String, ByVal Price As Decimal)
        Dim item As CartItem = CType(_CartItems(ID), CartItem)
        If item Is Nothing Then
            _CartItems.Add(ID, New CartItem(ID, Name, Price))
        Else
            item.Quantity += 1
            _CartItems(ID) = item
        End If
    End Sub

    ' Remove an item from the shopping cart
    Public Sub RemoveItem(ByVal ID As Integer)
        Dim item As CartItem = CType(_CartItems(ID), CartItem)
        If item Is Nothing Then
            Return
        End If
        item.Quantity -= 1
        If item.Quantity = 0 Then
            _CartItems.Remove(ID)
        Else
            _CartItems(ID) = item
        End If
    End Sub

End Class

<Serializable()> _
Public Class CartItem

    Private _ID As Integer
    Private _Name As String
    Private _Price As Decimal
    Private _Quantity As Integer = 1

    Public ReadOnly Property ID() As Integer
        Get
            Return _ID
        End Get
    End Property

    Public ReadOnly Property Name() As String
        Get
            Return _Name
        End Get
    End Property

    Public ReadOnly Property Price() As Decimal
        Get
            Return _Price
        End Get
    End Property

    Public Property Quantity() As Integer
        Get
            Return _Quantity
        End Get
        Set(ByVal value As Integer)
            _Quantity = value
        End Set
    End Property

    Public Sub New(ByVal ID As Integer, _
      ByVal Name As String, ByVal Price As Decimal)
        _ID = ID
        _Name = Name
        _Price = Price
    End Sub
End Class

Listing 5. ShoppingCart (C#)

using System;
using System.Collections;

[Serializable]
public class ShoppingCart
{
    public Hashtable _CartItems = new Hashtable();

    // Return all the items from the Shopping Cart
    public ICollection CartItems
    {
        get { return _CartItems.Values; }
    }

    // The sum total of the prices
    public decimal Total
    {
        get 
        {
            decimal sum = 0;
            foreach (CartItem item in _CartItems.Values)
                sum += item.Price * item.Quantity;
            return sum;
        }
    }

    // Add a new item to the shopping cart
    public void AddItem(int ID, string Name, decimal Price)
    {
        CartItem item = (CartItem)_CartItems[ID];
        if (item == null)
            _CartItems.Add(ID, new CartItem(ID, Name, Price));
        else
        {
            item.Quantity++;
            _CartItems[ID] = item;
        }
    }

    // Remove an item from the shopping cart
    public void RemoveItem(int ID)
    {
        CartItem item = (CartItem)_CartItems[ID];
        if (item == null)
            return;
        item.Quantity--;
        if (item.Quantity == 0)
            _CartItems.Remove(ID);
        else
            _CartItems[ID] = item;
    }

}

[Serializable]
public class CartItem
{
    private int _ID;
    private string _Name;
    private decimal _Price;
    private int _Quantity = 1;

    public int ID
    {
        get { return _ID; }
    }

    public string Name
    {
        get { return _Name; }
    }

    public decimal Price
    {
        get { return _Price; }
    }

    public int Quantity
    {
        get { return _Quantity; }
        set { _Quantity = value; }
    }

    public CartItem(int ID, string Name, decimal Price)
    {
        _ID = ID;
        _Name = Name;
        _Price = Price;
    }
}

If you add the code in Listing 5 to the App_Code folder in your application, then the shopping cart will be automatically compiled.

There's one thing in particular that you should notice about the code in Listing 5. Notice that both the ShoppingCart and CartItem classes are marked as serializable. This is important since these objects must be serialized when they are stored by the Profile object.

Finally, the page in Listing 6 displays a list of products that you can add to the shopping cart (see Figure 2). Notice that the shopping cart is loaded from the Profile object in the BindShoppingCart method. This method binds a GridView control to the shopping cart items represented by the CartItems property of the ShoppingCart class.

ms379605.userprofiles_fig02(en-US,VS.80).gif

Figure 2. Storing a shopping cart in a profile

The AddCartItem method adds a new product to the shopping cart. Notice that the AddCartItem method includes code that checks whether or not the ShoppingCart object exists in the Profile. You must instantiate any objects stored in the Profile object yourself. They are not instantiated automatically.

Finally, the RemoveCartItem method removes a product from the shopping cart. This method simply calls the RemoveItem method on the ShoppingCart object stored in the Profile object.

Listing 6 - Products.aspx (Visual Basic .NET)

<%@ Page Language="VB" %>

<script runat="server">

    Sub Page_Load()
        If Not IsPostBack Then
            BindShoppingCart()
        End If
    End Sub
        
    Sub BindShoppingCart()
        If Not Profile.ShoppingCart Is Nothing Then
            CartGrid.DataSource = Profile.ShoppingCart.CartItems
            CartGrid.DataBind()
            lblTotal.Text = Profile.ShoppingCart.Total.ToString("c")
        End If
    End Sub
   
    Sub AddCartItem(ByVal s As Object, ByVal e As EventArgs)
        Dim row As GridViewRow = ProductGrid.SelectedRow

        Dim ID As Integer = CInt(ProductGrid.SelectedDataKey.Value)
        Dim Name As String = row.Cells(1).Text
        Dim Price As Decimal = CDec(row.Cells(2).Text)
        
        If Profile.ShoppingCart Is Nothing Then
            Profile.ShoppingCart = New ShoppingCart
        End If
        Profile.ShoppingCart.AddItem(ID, Name, Price)
        BindShoppingCart()
    End Sub
    
    Sub RemoveCartItem(ByVal s As Object, ByVal e As EventArgs)
        Dim ID As Integer = CInt(CartGrid.SelectedDataKey.Value)
        Profile.ShoppingCart.RemoveItem(ID)
        BindShoppingCart()
    End Sub
</script>

<html>
<head>
    <title>Products</title>
</head>
<body>
    <form id="form1" runat="server">

    <table width="100%">
    <tr>
        <td valign="top">
    <h2>Products</h2>    
    <asp:GridView
        ID="ProductGrid"
        DataSourceID="ProductSource"
        DataKeyNames="ProductID"
        AutoGenerateColumns="false"
        OnSelectedIndexChanged="AddCartItem"
        ShowHeader="false"
        CellPadding="5"
        Runat="Server">
        <Columns>
            <asp:ButtonField 
                CommandName="select"
                Text="Buy" />
            <asp:BoundField
                DataField="ProductName" />
            <asp:BoundField
                DataField="UnitPrice" 
                DataFormatString="{0:c}" />
        </Columns>
    </asp:GridView>



        
    <asp:SqlDataSource
        ID="ProductSource"
        ConnectionString=
"Server=localhost;Database=Northwind;Trusted_Connection=true;"
        SelectCommand= 
          "SELECT ProductID,ProductName,UnitPrice FROM Products"
        Runat="Server" />
        </td>
        <td valign="top">
        <h2>Shopping Cart</h2>
        <asp:GridView
            ID="CartGrid"
            AutoGenerateColumns="false"
            DataKeyNames="ID"
            OnSelectedIndexChanged="RemoveCartItem"
            CellPadding="5" 
            Width="300"
            Runat="Server">
            <Columns>
            <asp:ButtonField
                CommandName="select"
                Text="Remove" />
            <asp:BoundField
                DataField="Name" 
                HeaderText="Name" />
            <asp:BoundField
                DataField="Price" 
                HeaderText="Price" 
                DataFormatString="{0:c}" />
            <asp:BoundField
                DataField="Quantity" 
                HeaderText="Quantity" />
            </Columns>
        </asp:GridView>
        <b>Total:</b> 
        <asp:Label ID="lblTotal" Runat="Server" />
        </td>
     </tr>
     </table>
    </form>
</body>
</html>

Listing 6. Products.aspx (C#)

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Globalization" %>
<script runat="server">

    void Page_Load() {
        if (!IsPostBack)
            BindShoppingCart();
    }
        
    void BindShoppingCart() 
    {
        if (Profile.ShoppingCart != null) 
        {
            CartGrid.DataSource = Profile.ShoppingCart.CartItems;
            CartGrid.DataBind();
            lblTotal.Text = Profile.ShoppingCart.Total.ToString("c");
        }
    }
   
    void AddCartItem(Object s, EventArgs e) 
    {
        GridViewRow row = ProductGrid.SelectedRow;

        int ID = (int)ProductGrid.SelectedDataKey.Value;
        String Name = row.Cells[1].Text;
        decimal Price = Decimal.Parse(row.Cells[2].Text, 
          NumberStyles.Currency);
        
        if (Profile.ShoppingCart == null)
            Profile.ShoppingCart = new ShoppingCart();
       
        Profile.ShoppingCart.AddItem(ID, Name, Price);
        BindShoppingCart();
    }
    
    void RemoveCartItem(Object s, EventArgs e) 
    {
        int ID = (int)CartGrid.SelectedDataKey.Value;
        Profile.ShoppingCart.RemoveItem(ID);
        BindShoppingCart();
    }
</script>

<html>
<head>
    <title>Products</title>
</head>
<body>
    <form id="form1" runat="server">

    <table width="100%">
    <tr>
        <td valign="top">
    <h2>Products</h2>    
    <asp:GridView
        ID="ProductGrid"
        DataSourceID="ProductSource"
        DataKeyNames="ProductID"
        AutoGenerateColumns="false"
        OnSelectedIndexChanged="AddCartItem"
        ShowHeader="false"
        CellPadding="5"
        Runat="Server">
        <Columns>
            <asp:ButtonField 
                CommandName="select"
                Text="Buy" />
            <asp:BoundField
                DataField="ProductName" />
            <asp:BoundField
                DataField="UnitPrice" 
                DataFormatString="{0:c}" />
        </Columns>
    </asp:GridView>



        
    <asp:SqlDataSource
        ID="ProductSource"
        ConnectionString=
"Server=localhost;Database=Northwind;Trusted_Connection=true;"
        SelectCommand=
          "SELECT ProductID,ProductName,UnitPrice FROM Products"
        Runat="Server" />
        </td>
        <td valign="top">
        <h2>Shopping Cart</h2>
        <asp:GridView
            ID="CartGrid"
            AutoGenerateColumns="false"
            DataKeyNames="ID"
            OnSelectedIndexChanged="RemoveCartItem"
            CellPadding="5" 
            Width="300"
            Runat="Server">
            <Columns>
            <asp:ButtonField
                CommandName="select"
                Text="Remove" />
            <asp:BoundField
                DataField="Name" 
                HeaderText="Name" />
            <asp:BoundField
                DataField="Price" 
                HeaderText="Price" 
                DataFormatString="{0:c}" />
            <asp:BoundField
                DataField="Quantity" 
                HeaderText="Quantity" />
            </Columns>
        </asp:GridView>
        <b>Total:</b> 
        <asp:Label ID="lblTotal" Runat="Server" />
        </td>
     </tr>
     </table>
    </form>
</body>
</html>

Inheriting a Profile

You also can define a profile by inheriting the profile from an existing class. This option is valuable when you need to use the same profile with multiple applications.

For example, the class in Listing 7 represents a number of user properties. Notice that the class derives from the ProfileBase class (found in the System.Web.Profile namespace).

Listing 7. UserInfo (Visual Basic .NET)

Imports Microsoft.VisualBasic
Imports System.Web.Profile

Public Class UserInfo
    Inherits ProfileBase

    Private _FirstName As String
    Private _LastName As String

    Public Property FirstName() As String
        Get
            Return _FirstName
        End Get
        Set(ByVal value As String)
            _FirstName = value
        End Set
    End Property

    Public Property LastName() As String
        Get
            Return _LastName
        End Get
        Set(ByVal value As String)
            _LastName = value
        End Set
    End Property

End Class

Listing 7. UserInfo (C#)

using System;
using System.Web.Profile;

public class UserInfo : ProfileBase
{
    private string _FirstName;
    private string _LastName;

    public string FirstName 
    {
        get { return _FirstName; }
        set { _FirstName = value; }
    }
    public string LastName
    {
        get { return _LastName; }
        set { _LastName = value; }
    }
}

The Web.Config file in Listing 8 contains a profile that inherits from the UserInfo class. All of the properties exposed in the UserInfo class are exposed by the profile generated by this definition.

Listing 8. Web.Config

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

Migrating Anonymous Profile Settings

You can use the Profile object with both anonymous and authenticated users. However, when the same user switches between an anonymous and authenticated state, the Profile object exhibits potentially confusing behavior.

When the Profile object is used with an anonymous user, the user profile is associated with a randomly generated identifier assigned to the user. This random identifier is stored in a browser cookie. Whenever the same user returns to the application, the profile settings are automatically reloaded for the user.

If the anonymous user is authenticated, any profile properties associated with the user are lost. A new profile is generated for the user. Profile information is now associated with the authenticated username and not the randomly generated identifier.

The best way to understand how all of this works is to take a look at a sample page. The Web.Config file in Listing 9 defines a profile with a single property named FavoriteColor.

Listing 9. Web.Config

<configuration>
<system.web>

   <authentication mode="Forms" />
      
           <anonymousIdentification enabled="true" />
        
   <profile>
   <properties>
                  <add 
         name="FavoriteColor"
         allowAnonymous="true" 
         defaultValue="Red" />
               </properties>
           </profile>
</system.web>
</configuration>

The page in Listing 10 contains both a login and logout button. It also contains a form for updating the FavoriteColor profile property.

Listing 10. Anonymous.aspx (Visual Basic .NET)

<%@ Page Language="VB" %>

<script runat="server">

    Sub Login(ByVal s As Object, ByVal e As EventArgs)
        FormsAuthentication.SetAuthCookie("Bill", False)
        Response.Redirect(Request.Path)
    End Sub

    Sub Logout(ByVal s As Object, ByVal e As EventArgs)
        FormsAuthentication.SignOut()
        Response.Redirect(Request.Path)
    End Sub

    Sub UpdateProfile(ByVal s As Object, ByVal e As EventArgs)
        Profile.FavoriteColor = txtFavoriteColor.Text
    End Sub
    
    Sub Page_PreRender()
        lblUsername.Text = Profile.UserName
        lblFavoriteColor.Text = Profile.FavoriteColor
    End Sub
        
</script>

<html>
<head>
    <title>Anonymous</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:Button ID="Button1"
        Text="Login"
        OnClick="Login"
        Runat="Server" />
    <asp:Button ID="Button2"
        Text="Logout"
        OnClick="Logout"
        Runat="Server" />
    <hr />
    <asp:TextBox    
        id="txtFavoriteColor"
        Runat="Server" />
    <asp:Button ID="Button3"
        Text="Update Profile"
        OnClick="UpdateProfile"
        Runat="Server" />
    <hr />
    <b>Username:</b>
    <asp:Label  
        id="lblUsername"
        Runat="Server" />
    <br />
    <b>Favorite Color:</b>
    <asp:Label
        id="lblFavoriteColor"
        Runat="Server" />    
        
    </form>
</body>
</html>

Listing 10. Anonymous.aspx (C#)

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

<script runat="server">

    void Login(Object s, EventArgs e)
    {
        FormsAuthentication.SetAuthCookie("Bill", false);
        Response.Redirect(Request.Path);
    }

    void Logout(Object s, EventArgs e)
    {
        FormsAuthentication.SignOut();
        Response.Redirect(Request.Path);
    }

    void UpdateProfile(Object s, EventArgs e)
    {
        Profile.FavoriteColor = txtFavoriteColor.Text;
    }
    
    void Page_PreRender()
    {
        lblUsername.Text = Profile.UserName;
        lblFavoriteColor.Text = Profile.FavoriteColor;
    }
        
</script>

<html>
<head>
    <title>Anonymous</title>
</head>
<body>
    <form id="form1" runat="server">

    <asp:Button
        Text="Login"
        OnClick="Login"
        Runat="Server" />
    <asp:Button ID="Button1"
        Text="Logout"
        OnClick="Logout"
        Runat="Server" />
    <hr />
    <asp:TextBox    
        id="txtFavoriteColor"
        Runat="Server" />
    <asp:Button
        Text="Update Profile"
        OnClick="UpdateProfile"
        Runat="Server" />
    <hr />
    <b>Username:</b>
    <asp:Label  
        id="lblUsername"
        Runat="Server" />
    <br />
    <b>Favorite Color:</b>
    <asp:Label
        id="lblFavoriteColor"
        Runat="Server" />    
        
    </form>
</body>
</html>

When you first open the page in Listing 10, the value of the UserName profile property is a randomly generated identifier (see Figure 3). If you click the Login button, then you will be authenticated with the UserName Bill.

ms379605.userprofiles_fig03(en-US,VS.80).gif

Figure 3. Using anonymous and authenticated profiles

The page in Listing 10 includes a form for updating the value of the FavoriteColor profile property. Notice that if you login and logout, a distinct profile is generated for the two states. If you login and logout, a new randomly generated identifier is created.

There are many situations in which you'll want to carry over profile property values from an anonymous to an authenticated state. If you need to migrate profile property values, then you can take advantage of the MigrateAnonymous event of the ProfileModule class. This event must be handled in the Global.asax file. The Global.asax in Listing 11 illustrates how you can migrate the FavoriteColor profile property.

Listing 11. Global.asax (Visual Basic .NET)

<%@ Application Language="VB" %>
<script runat="server">

    Sub Profile_MigrateAnonymous(ByVal s As Object, _
      ByVal e As ProfileMigrateEventArgs)
        Dim anonProfile As ProfileCommon = _
          Profile.GetProfile(e.AnonymousId)
        Profile.FavoriteColor = anonProfile.FavoriteColor
    End Sub
       
</script>

Listing 11. Global.asax (C#)

<%@ Application Language="C#" %>
<script runat="server">

    void Profile_MigrateAnonymous(Object s, 
      ProfileMigrateEventArgs e)
    {
        ProfileCommon anonProfile = 
          Profile.GetProfile(e.AnonymousId);
        Profile.FavoriteColor = anonProfile.FavoriteColor;
    }
       
</script>

The GetProfile() method of the Profile class is used to get the anonymous profile. This method accepts a profile identifier and returns the corresponding profile. The ProfileMigrateEventArgs object contains the anonymous identifier.

Configuring Profile Providers

By default, profiles are stored in a Microsoft SQL Server 2005 Express database stored in the App_Data folder of your application. This is fine while you are developing an ASP.NET application. Most likely, however, when you are ready to go live with your Web application, you'll want to store your application’s profile information in another database such as an instance of the full version of Microsoft SQL Server located somewhere on your network.

Profiles use the provider model. The provider model enables you to specify where information is stored by modifying configuration settings within the Machine.Config or Web.Config file.

ASP.NET 2.0 ships with one profile provider: the SqlProfileProvider. If you are feeling ambitious, you can create your own provider by deriving a new class from the base ProfileProvider class. For example, you can create a custom profile provider that works with an Oracle or MySQL database. The easiest option, and the one explored here, is to store your profile information in a Microsoft SQL Server database.

There are two steps required for using a particular Microsoft SQL Server database to store profile information. You must setup your SQL Server database and you must make a change to your configuration file.

The ASP.NET 2.0 framework includes a utility for configuring a SQL database to store profile information. This utility is named aspnet_regsql and it is located in your Windows\Microsoft.NET\Framework\[version] folder. When you execute this utility, you get the ASP.NET SQL Server Setup Wizard illustrated in Figure 4.

ms379605.userprofiles_fig04(en-US,VS.80).gif

Figure 4. Using the ASP.NET SQL Server setup

The SQL Server Setup Wizard guides you through the steps necessary for adding the required tables and stored procedures to the database where you want to store your application’s profile information.

After your SQL Server database is configured, you need to modify either your Web.Config or Machine.Config file to to point to a SQL Server database named MyDatabase located on a server in your network named MyServer. The configuration file in Listing 12 contains the necessary configuration settings.

Listing 12. Web.Config

<configuration>

<connectionStrings>

<add

name="myConnectionString"

connectionString=

"Server=MyServer;Trusted_Connection=true;database=MyDatabase" />

</connectionStrings>

<system.web>

<anonymousIdentification enabled="true" />

<profile defaultProvider="MyProfileProvider">

<providers>

<add

name="MyProfileProvider"

type="System.Web.Profile.SqlProfileProvider"

connectionStringName="myConnectionString" />

</providers>

<properties>

<add

name="FirstName"

allowAnonymous="true" />

<add

name="LastName"

allowAnonymous="true" />

</properties>

</profile>

</system.web>

</configuration>

In Listing 12, the profile declaration includes a defaultProvider attribute. This attribute points to a profile provider named the MyProfileProvider. This provider is defined in the <providers> section of the <profile> tag. The MyProfileProvider, in turn, uses a connection string named MyConnectionString to connect to the database which contains the profile information. The MyConnectionString connection string is defined in the <connectionStrings> section at the top of the Web.Config file.

Managing Profiles and Generating Profile Reports

The Profile object automatically saves profile information for users. This feature is both a blessing and curse. It is a blessing since you do not need to write all the logic to save the data yourself. It is a curse because potentially a huge amount of unused data could accumulate in your database.

Fortunately, the ASP.NET 2.0 framework includes a class that you can use to manage profiles: the ProfileManager class. This class has a number of utility methods that you can use to manage profiles and generate reports on profiles. Here is a list of the more important methods included in this class:

  • DeleteInactiveProfiles. Enables you to delete all profiles older than a specified date.
  • DeleteProfile. Enables you to delete a profile associated with a specified username.
  • DeleteProfiles. Enables you to delete a set of profiles.
  • FindInactiveProfilesByUserName. Returns a collection of ProfileInfo objects that represent profiles that have been inactive since a specified date and match a specified name.
  • FindProfilesByUserName. Returns a collection of ProfileInfo objects that represent profiles that match a specified username.
  • GetAllInactiveProfiles. Returns a collection of ProfileInfo objects that represent profiles that have been inactive since a specified date.
  • GetAllProfiles. Returns a collection of ProfileInfo objects that represent all profiles.
  • GetNumberOfInactiveProfiles. Returns an integer that represents the number of profiles that have been inactive since a specified date.
  • GetNumberOfProfiles. Returns an integer that represents the total number of profiles.

None of these methods return a complete profile, although many of the methods return a collection of ProfileInfo objects. The ProfileInfo object represents the following profile properties:

  • IsAnonymous. A Boolean value that represents whether or not the profile is associated with an anonymous or authenticated user.
  • LastActivityDate. A date and time that represents the last time the profile was accessed.
  • LastUpdatedDate. A date and time that represents the last time the profile was updated.
  • Size. An integer value that represents the size of the profile as stored by the profile provider.
  • UserName. A string that represents the user associated with the profile.

Several of the ProfileManager methods include additional parameters that support paging. For example, one overloaded version of the GetAllProfiles method includes parameters for the page index, page size, and total records. The paging parameters are useful when you want to page through profile information in a Web page.

The ProfileManager class can be used both within an ASP.NET page and outside of an ASP.NET page. For example, you might want to create a console application that automatically executes once a day and cleans up inactive profiles. The console application in Listing 14 deletes all profiles older than 7 days. You could schedule this console application by using Windows Scheduled Tasks.

Listing 14. DeleteInactiveProfiles (Visual Basic .NET)

Imports System.Web.Profile

Public Class DeleteInactiveProfiles
    
    Public Shared Sub Main()
      Dim deleted As Integer
      deleted = 
        ProfileManager.DeleteInactiveProfiles( 
          ProfileAuthenticationOption.All, 
          DateTime.Now.AddDays(-7))
      Console.WriteLine("Deleted " & deleted & " profiles" )
    End Sub
      
End Class

Listing 14. DeleteInactiveProfiles (C#)

using System;
using System.Web.Profile;

public class DeleteInactiveProfiles
{    
    public static void Main()
    {
      int deleted = 0;
      deleted = 
        ProfileManager.DeleteInactiveProfiles(
        ProfileAuthenticationOption.All, 
        DateTime.Now.AddDays(-7));
      Console.WriteLine("Deleted " + 
        deleted.ToString() + " profiles" );
    }
        
}

You can compile the code in Listing 14 by executing the following command from the command line:

[Visual Basic .NET]

C:\WINDOWS\Microsoft.NET\Framework\v2.0.40607\vbc 
  /r:System.Web.dll DeleteInactiveProfiles.vb

[C#]

C:\WINDOWS\Microsoft.NET\Framework\v2.0.40607\csc 
  DeleteInactiveProfiles.cs

You can also use the ProfileManager class to generate reports on the information contained in profiles. For example, imagine that you want to create a user survey and store the survey results in the Profile object. Imagine, furthermore, that you want to create a page that generates a report on all of the survey results.

The Web.Config file in Listing 15 contains three properties: SurveyCompleted, FavoriteLanguage, and FavoriteEnvironment.

Listing 15. Web.Config

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
    
   <system.web>

      
              <anonymousIdentification enabled="true" />

      <profile>
      <properties>
                      <add 
            name="SurveyCompleted"
            type="Boolean"
            allowAnonymous="true" />
                      <add 
            name="FavoriteLanguage"
            allowAnonymous="true" />
                     <add 
            name="FavoriteEnvironment"
            allowAnonymous="true" />
      </properties>
              </profile>
 </system.web>
</configuration>

The page in Listing 16 displays a simple user survey. The page consists of two Panel controls. The first panel contains the two survey questions. When the user completes the survey, the first panel is hidden and the second panel is displayed. The second Panel contains text that thanks the user for completing the survey.

Listing 16. Survey.aspx (Visual Basic .NET)

<%@ Page Language="VB" %>

<script runat="server">

    Sub SaveSurvey(ByVal s As Object, ByVal e As EventArgs)
        Profile.FavoriteLanguage = rdlLanguage.SelectedItem.Text
        Profile.FavoriteEnvironment = rdlEnvironment.SelectedItem.Text
        Profile.SurveyCompleted = True
    End Sub
    
    Sub Page_PreRender()
        If Profile.SurveyCompleted Then
            pnlSurvey.Visible = False
            pnlSurveyCompleted.Visible = True
        Else
            pnlSurvey.Visible = True
            pnlSurveyCompleted.Visible = False
        End If
    End Sub
    
</script>

<html>
<head>
    <title>Survey</title>
</head>
<body>
    <form id="form1" runat="server">
    
    <asp:Panel ID="pnlSurvey" Runat="Server">
    What is your favorite programming language?
    <br />
    <asp:RadioButtonList 
        id="rdlLanguage"
        runat="Server">
        <asp:ListItem Text="VB.NET" Selected="True" />    
        <asp:ListItem Text="C#" />
        <asp:ListItem Text="J#" />    
    </asp:RadioButtonList>
    <p>&nbsp;</p>
    What is your favorite development environment?
    <br />
    <asp:RadioButtonList 
        id="rdlEnvironment"
        runat="Server">
        <asp:ListItem Text="VS.NET" Selected="True" />    
        <asp:ListItem Text="Web Matrix" />
        <asp:ListItem Text="Notepad" />    
    </asp:RadioButtonList>
    <p>&nbsp;</p>    
    <asp:Button
        Text="Submit Survey"
        Onclick="SaveSurvey"
        Runat="Server" />
    </asp:Panel>
    <asp:Panel ID="pnlSurveyCompleted" Runat="Server">
    Thank you for completing the survey!
    </asp:Panel>
    </form>
</body>
</html>

Listing 16. Survey.aspx (C#)

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

<script runat="server">

    void SaveSurvey(Object s, EventArgs e)
    {    
        Profile.FavoriteLanguage = rdlLanguage.SelectedItem.Text;
        Profile.FavoriteEnvironment = rdlEnvironment.SelectedItem.Text;
        Profile.SurveyCompleted = true;
    }
    
    void Page_PreRender()
    {    
        if (Profile.SurveyCompleted) 
        {
            pnlSurvey.Visible = false;
            pnlSurveyCompleted.Visible = true;
        }
        else
        {
            pnlSurvey.Visible = true;
            pnlSurveyCompleted.Visible = false;
        }
    }
    
</script>

<html>
<head>
    <title>Survey</title>
</head>
<body>
    <form id="form1" runat="server">
    
    <asp:Panel ID="pnlSurvey" Runat="Server">
    What is your favorite programming language?
    <br />
    <asp:RadioButtonList 
        id="rdlLanguage"
        runat="Server">
        <asp:ListItem Text="VB.NET" Selected="True" />    
        <asp:ListItem Text="C#" />
        <asp:ListItem Text="J#" />    
    </asp:RadioButtonList>
    <p>&nbsp;</p>
    What is your favorite development environment?
    <br />
    <asp:RadioButtonList 
        id="rdlEnvironment"
        runat="Server">
        <asp:ListItem Text="VS.NET" Selected="True" />    
        <asp:ListItem Text="Web Matrix" />
        <asp:ListItem Text="Notepad" />    
    </asp:RadioButtonList>
    <p>&nbsp;</p>    
    <asp:Button ID="Button1"
        Text="Submit Survey"
        Onclick="SaveSurvey"
        Runat="Server" />
    </asp:Panel>
    <asp:Panel ID="pnlSurveyCompleted" Runat="Server">
    Thank you for completing the survey!
    </asp:Panel>
    </form>
</body>
</html>

The page in Listing 17 displays the survey results (see Figure 5). This page contains a GridView control that displays the contents of the ProfileInfo objects returned by the GetAllProfiles method of the ProfileManager class. When you click the Select link that appears next to each row in the GridView, you can see the actual responses to the survey questions. The GetProfile() method is called on the Profile class to return the actual profile that corresponds to a user identifier.

Click here for larger image.

Figure 5. Displaying survey results

Listing 17. SurveyResults.aspx (Visual Basic .NET)

<%@ Page Language="VB" %>
<script runat="server">

    Sub Page_Load()
        ResultsGrid.DataSource = _ 
ProfileManager.GetAllProfiles(ProfileAuthenticationOption.All)
        ResultsGrid.DataBind()
    End Sub
    
    Sub DisplayProfileDetails(ByVal s As Object, ByVal e As EventArgs)
        Dim SelectedProfile As ProfileCommon
        SelectedProfile = Profile.GetProfile(ResultsGrid.SelectedValue)
        lblLanguage.Text = SelectedProfile.FavoriteLanguage
        lblEnvironment.Text = SelectedProfile.FavoriteEnvironment
    End Sub
    
</script>

<html>
<head>
    <title>Survey Results</title>
</head>
<body>
    <form id="form1" runat="server">
    <h2>Survey Results</h2>
    <asp:GridView 
        id="ResultsGrid"
        DataKeyNames="UserName"
        AutoGenerateSelectButton="true"
        OnSelectedIndexChanged="DisplayProfileDetails"
        SelectedRowStyle-BackColor="LightYellow"
        Runat="Server" />
    <p>&nbsp;</p>
    <h2>Survey Details</h2>
    <b>Favorite Language:</b>
    <asp:Label  
        id="lblLanguage"
        Runat="Server" />
    <br />
    <b>Favorite Environment:</b>
    <asp:Label  
        id="lblEnvironment"
        Runat="Server" />

    </form>
</body>
</html>

Listing 17. SurveyResults.aspx (C#)

<%@ Page Language="C#" %>
<script runat="server">

    void Page_Load()
    {    
        ResultsGrid.DataSource = 
ProfileManager.GetAllProfiles(ProfileAuthenticationOption.All);
        ResultsGrid.DataBind();
    }
    
    void DisplayProfileDetails(Object s, EventArgs e)
    {
        ProfileCommon SelectedProfile = 
            Profile.GetProfile(ResultsGrid.SelectedValue.ToString());
        lblLanguage.Text = SelectedProfile.FavoriteLanguage;
        lblEnvironment.Text = SelectedProfile.FavoriteEnvironment;
    }
    
</script>

<html>
<head>
    <title>Survey Results</title>
</head>
<body>
    <form id="form1" runat="server">
    <h2>Survey Results</h2>
    <asp:GridView 
        id="ResultsGrid"
        DataKeyNames="UserName"
        AutoGenerateSelectButton="true"
        OnSelectedIndexChanged="DisplayProfileDetails"
        SelectedRowStyle-BackColor="LightYellow"
        Runat="Server" />
    <p>&nbsp;</p>
    <h2>Survey Details</h2>
    <b>Favorite Language:</b>
    <asp:Label  
        id="lblLanguage"
        Runat="Server" />
    <br />
    <b>Favorite Environment:</b>
    <asp:Label  
        id="lblEnvironment"
        Runat="Server" />
    


    </form>
</body>
</html>

Conclusion

When building Web applications, I still expend far too much time and energy performing brain-dead tasks. One of these tasks is writing the necessary code to store and retrieve user information from the database. The Profile object does not introduce any new functionality that wasn't available in the ASP.NET 1.0 Framework. However, this new feature shields us from the tedious chore of writing code to save and retrieve information from a database. In other words, the Profile object will free us to concentrate on the more interesting aspects of building a Web application.

 

About the author

Stephen Walther wrote the best-selling book on ASP.NET, ASP.NET Unleashed. He was also the architect and lead developer of the ASP.NET Community Starter Kit, a sample ASP.NET application produced by Microsoft. He has provided ASP.NET training to companies across the United States, including NASA and Microsoft, through his company Superexpert (http://www.superexpert.com).

© Microsoft Corporation. All rights reserved.