A good web site will remember me. Perhaps the site will remember my favorite color scheme, or my preferred shipping address. Either way, if a site makes me feel like I’m not just another IP address on the web, I’m more likely to return in the future. Adding a personal touch for your end-users can make a difference.
Personalization requires a bit of work, however. We need to track users and save their preferences between visits. We need database tables, SQL queries, and some well designed classes. Fortunately, ASP.NET 2.0 saves us from all the grunge work by giving us built-in profiling features we can use.
A Simple Profile
The Profile provider in ASP.NET stores and retrieves information about our site’s users. The default profile provider keeps data in SQL Server tables. All we need to provide is a definition of the user profile. The profile definition lives in the application’s web.config file. The following is a simple profile with name and age properties.
<configuration>
<system.web>
<anonymousIdentification enabled="true"/>
<trust level="Medium"/>
<profile >
<properties>
<add name="Name" allowAnonymous="true" />
<add name="Age" allowAnonymous="true" type="System.Int16"/>
</properties>
</profile>
<compilation debug="true"/>
</system.web>
</configuration>
Using the profile we’ve defined is straightforward. The following is a content page that will read the current user’s profile settings into TextBox controls, and update the profile if the user clicks an update button. Notice we don’t need to use any database access code or explicitly load or save profile settings – ASP.NET takes care of all this work in the background.
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
nameTextBox.Text = Profile.Name;
ageTextBox.Text = Profile.Age.ToString();
}
}
protected void updateProfileButton_Click(object sender,
EventArgs e)
{
Profile.Name = nameTextBox.Text;
Profile.Age = Int16.Parse(ageTextBox.Text);
}
</script>
<asp:Content ID="Content1" ContentPlaceHolderID="main" runat="Server">
<hr />
<asp:TextBox runat="server" ID="nameTextBox" /><br />
<asp:TextBox runat="server" ID="ageTextBox" /><br />
<asp:Button runat="server" ID="updateProfileButton"
Text="Save Preferences"
OnClick="updateProfileButton_Click" />
</asp:Content>
The above sample works even for anonymous users. We will take a look later in the article how the system tracks unnamed users, but for now let’s introduce the software behind the curtain.
The Profile Provider
The ASP.NET team implemented the profile management features using a provider model. A base ProfileProvider class defines the interface, or contract, that all profile providers must implement. ASP.NET 2.0 ships with an implementation of the ProfileProvider that uses SQL Server – the SqlProfileProvider class. SqlProfileProvider implements the ProfileProvider methods (like FindProfilesByUserName) by executing stored procedures in SQL Server.
The provider model is extensible. If you don’t want to use SQL Server, but want to use an XML file or a different relational database engine, you can define your own class derived from the ProfileProvider class and implement all the Profile features using a different data store. The beauty of the provider model is how you can plug your own implementation of a feature into the system, and the runtime and application continue to work without knowing the details of the implementation. Later in the article, we will discuss the provider configuration. For now, let’s take a closer look at configuring Profile properties in web.config.
Profiles, Users, Values
Let’s extend our Profile settings with the following web.config.
<properties>
<add name="Name" allowAnonymous="true" />
<add name="Age" allowAnonymous="true" type="System.Int16"/>
<group name="UI">
<add name="MasterPage" defaultValue="layout1.master"/>
<add name="Theme" defaultValue="Red"/>
</group>
</properties>
</profile>
The above configuration demonstrates several features. First, Profile properties use the String type by default. We can change the default with the type attribute, as we do with the Age property (typed as Int16). We can define a default value for a profile property with the defaultValue attribute. The provider will use the default value when a property value does not yet exist in the data store.
The Unidentified User
By default, Profile properties are only available for authenticated users. If we want a property to be available for an anonymous user, we need to add allowAnonymous=”true” to the property. Without the attribute, the runtime will throw an exception if the current user is anonymous and we write to the property.
In order for allowAnonymous to work, we need to configure ASP.NET to track anonymous users. We configure tracking using the anonymousIdentification section of web.config. Anonymous user tracking is off by default, but enabled=”true” will tell the ASP.NET runtime to load an AnonymousIdentificationModule into the HttpModule request pipeline. When an anonymous user makes a request, this module creates a globally unique identifier using the System.Guid class, and writes the GUID into a persistent cookie named .ASPXANONYMOUS. The GUID will be the anonymous user’s “name”. Using a persistent cookie means the user’s GUID will be available between visits. The cookie name, cookie timeout, and other characteristics of anonymous user tracking are configurable in the anonymousIdentification configuration element.
Groupings
We can create a hierarchy of profile properties using the group element. Grouping allows us to categorize properties, which can be helpful when there are a large number of profile properties. We can specify more than one property group, but we cannot nest a group beneath another group element. As you may have noticed in our first code sample, the ASP.NET compiler generates a class with strongly typed profile properties. A grouping adds an additional layer in the generated class, as can be seen in the following code snippet.
Profile.UI.Theme = "Reddish";
Profile.UI.MasterPage = "layout1.master";
Profiles At Compilation and Runtime
Where does the strongly typed Profile come from? The ASP.NET compiler parses web.config to uncover the profile schema defined inside, and then code-generates a class in the Temporary ASP.NET Files directory. The following class is an excerpt of the class generated from the web.config in this article.
public virtual short Age
{
get
{
return ((short)(this.GetPropertyValue("Age")));
}
set
{
this.SetPropertyValue("Age", value);
}
}
// other properties ...
}
The above class (ProfileCommon) will compile into the App_Code assembly, meaning the class is visible to all of our web forms, user controls, master pages, and to any classes in the App_Code directory. We can use the Profile object from a base page class in App_Code:
using System.Web;
using System.Web.UI;
public class BasePage : Page
{
public BasePage()
{
PreInit += new EventHandler(BasePage_PreInit);
}
void BasePage_PreInit(object sender, EventArgs e)
{
ProfileCommon profile = HttpContext.Current.Profile
as ProfileCommon;
if (!String.IsNullOrEmpty(profile.UI.MasterPage))
{
MasterPageFile = profile.UI.MasterPage;
}
if (!String.IsNullOrEmpty(profile.UI.Theme))
{
Theme = profile.UI.Theme;
}
}
}
The above class sets the layout and look of a page based on a user’s preferences. Notice we can pull the Profile object out of the current HttpContext object. There is another HttpModule in the pipeline, the ProfileModule, which creates the Profile object during the AcquireRequestState event of the request processing lifecycle. The ProfileModule places the ProfileCommon object into the current HttpContext so the object is available for the duration of the request.
Notice we need to coerce the Profile object into a ProfileCommon reference (the as ProfileCommon in the code). Since the ProfileCommon class is generated by ASP.NET from web.config, so it’s not possible for the HttpContext class to know the definition of ProfileCommon. HttpContext has to return a ProfileBase reference. The ProfileCommon class derives from ProfileBase. All we need to do is cast the ProfileBase reference to a ProfileCommon reference.
Why doesn’t our web form code need to use the same type cast? The answer is that the ASP.NET compiler code-generates a Profile property into our web form. If you poke around at the generated code in the Temporary ASP.NET Files directory (with debug enabled), you’ll find each web form has a property defined like the following:
get {
return ((ProfileCommon)(this.Context.Profile));
}
}
Our web form also pulls the profile object from the current HttpContext and casts the value to a ProfileCommon reference. Since the ASP.NET compiler is kind enough to generate this code, we don’t need to cast the reference in our web form code.
If we want to use the ProfileBase object in a class library outside the web application, however, we will not be able to see the definition for the ProfileCommon class. You can still use a ProfileBase reference from a class library project and read write properties using the GetPropertyValue and SetPropertyValue methods of ProfileBase. Another solution might be to use Profile inheritance, which we will look at later in the article.
Provider Configuration
The machine.config file configures the default Profile provider for all web sites on the computer. The machine.config file is located in the framework installation config directory, typically Windows\Microsoft.NET\Framework\v2.0.xxxx\Config. Inside is the following.
<providers>
<add name="AspNetSqlProfileProvider"
connectionStringName="LocalSqlServer"
applicationName="/"
type="System.Web.Profile.SqlProfileProvider />
</providers>
</profile>
The LocalSqlServer connection string points to a SQL Server 2005 Express database in the application’s App_Data folder. You can create a database on a different instance of SQL Server, or on a remote SQL Server, using the ASP.NET Sql Server Registration Tool (aspnet_regsql.exe). With a new database in place, you can either replace the LocalSqlServer connection string in your application’s web.config file, or reconfigure the provider to use a different connection string name. The following replaces the LocalSqlServer connection string to point to a different server.
<connectionStrings>
<remove name="LocalSqlServer"/>
<add name="LocalSqlServer"
connectionString="server=goa;database=aspnetdb;integrated security=sspi;"
/>
</connectionStrings>
...
</configuration>
The following configures a the SqlProfileProvider using a connection string named MySqlServer.
<providers>
<add name="MyProfileProvider"
connectionStringName="MySqlServer"
applicationName="/"
type="System.Web.Profile.SqlProfileProvider" />
</providers>
</profile>
For the configuration to work, you’ll need to create a MySqlServer connection string in web.config. Note also that the account logging into SQL Server will need permission to access the profile related tables and stored procedures. ASP.NET creates several roles in the database, including the aspnet_Profile_BasicAccess role, which grants all the permissions required for creating, updating, and deleting profiles.
Serialization
The runtime serializes primitive Profile properties (like strings, integers, and DateTime instances) as strings. We can override serialization behavior with the serializeAs attribute and select the XmlSerializer (serializeAs="Xml") or the BinarySerializer (serializeAs="Binary"). The default serializer is the XmlSerializer. Let’s say we want to keep track of a user’s favorite Pet with the following class in App_Code:
public class Pet
{
public Pet()
{
// default ctor required for serializer
}
public Pet(string name)
{
_name = name;
}
private string _name = String.Empty;
public string Name
{
get { return _name; }
set { _name = value; }
}
}
Notice we need a default constructor for our class to work with serialization. We can add a favorite Pet into the profile by first adding a new property into web.config:
If we put the following code into a web form’s Page_Load method, the runtime will serialize the Pet object into the database as XML (the default).
{
Profile.Pet = new Pet("Beaker The Cat");
}
You can look in the dbo.aspnet_Profile table to see serialized profiles. String and XML properties will be in the PropertyValuesString column, while objects serialized into binary will appear in the PropertyValuesBinary column. Side note: you’ll never want to store sensitive information into a user’s profile without encrypting the data first. Information in the profile columns is easy to extract.
Looking at the PropertyValuesString column in the aspnet_Profile table for our user's record, we will find the following (amongst the other property values).
<Pet
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>Beaker The Cat</Name>
</Pet>
If we want to use a binary serializer with Pet objects, we could change the property configuration as follows:
serializeAs="Binary"/>
The above configuration will throw an error: “The type for the property 'Pet' cannot be serialized using the binary serializer, since the type is not marked as serializable”. We need to make one small adjustment to the Pet class:
[Serializable]
public class Pet
{
// ...
}
Our pet objects will now appear as binary data in the PropertyValuesBinary column.
Profile Inheritance
The Profile object can inherit from an existing class. For example:
using System.Web.Profile;
using System.Collections.Generic;
public class CustomProfile : ProfileBase
{
[SettingsAllowAnonymous(true)]
public string Name
{
get { return base["Name"] as string; }
set { base["Name"] = value; }
}
[SettingsAllowAnonymous(true)]
public int Age
{
get { return (int)base["Age"]; }
set { base["Age"] = value; }
}
[SettingsAllowAnonymous(true)]
public List<Pet> Pets
{
get { return base["Pets"] as List<Pet>; }
set { base["Pets"] = value; }
}
}
A Profile class has to derive from ProfileBase. The public properties in the CustomProfile class forward calls to the ProfileBase indexer ([] in C#, () in VB). The base class will interact with the Profile provider to retrieve the values we need. Notice the class even uses a generic collection to store a list of all the user’s pets. To use the class, we just need to reconfigure our profile settings in web.config and use the inherits attribute.
<properties>
<add name="LuckyNumber" allowAnonymous="true"
type="System.Int32" defaultValue="7" />
</properties>
</profile>
Notice we can still augment the properties of our CustomProfile class with additional properties in web.config. The following code puts the new Profile to work.
Profile.Pets = new List<Pet>();
Profile.Pets.Add(new Pet("Dolly the Dolphin"));
Profile.Pets.Add(new Pet("Bessie the Cow"));
One good reason to inherit from a custom base class is that we can add additional logic to the Profile object. For instance, we can add validation logic to the Age property to ensure the user provides us with a sensible age, and not a value like 443.
Migrating Anonymous User Settings
If your web application allows anonymous users to set Profile properties, and your web application includes not only authenticated but anonymous users, then there is a sticky scenario to handle. At some point an anonymous user might login and become an authenticated user. After a user authenticates, their Profile changes, because the Profile system keys Profile information by username.
At the point an anonymous user changes to an authenticated user, the Profile module gives us a chance to migrate the user’s anonymous profile to the new authenticated profile with a MigrateAnonymous event. We can handle this global event in global.asax with the following code.
ProfileMigrateEventArgs e)
{
ProfileCommon anonProfile =
Profile.GetProfile(e.AnonymousID);
Profile.Age = anonProfile.Age;
Profile.LuckyNumber = anonProfile.LuckyNumber;
Profile.Pets = anonProfile.Pets;
AnonymousIdentificationModule.ClearAnonymousIdentifier();
}
It is important to tell the AnonymousIdentificationModule to clear the anonymous identifier (the cookie). Without this call the event will continue to fire with each page the authenticated user visits.
The MigrateAnonymous event isn’t perfect, however, because it will fire each time an anonymous user logs in – even if we migrated their anonymous profile on a previous visit. If you want to prevent a user from overwriting their authenticated profile with inadvertent changes to their anonymous profile, you should consider adding a boolean flag to the profile, and migrate the profile settings only when the flag is false (then set the flag to true).
The ProfileManager
discussion of Profiles would be complete without mentioning the ProfileManager class. The ProfileManager class is useful for managing profiles. Since the class is a static class, all the members are available to call without creating an instance of the class. The class is useful for managing profiles (ProfileManager.DeleteInactiveProfiles, for example), and for reporting on profiles. The GetAllProfiles method, for instance, includes an overload which accepts paging parameters (pageSize, pageIndex). The DeleteInactiveProfiles method will clean up profile records that have not been touched since a certain date.
Conclusions
Hopefully this article has demonstrated the flexibility of the ASP.NET Profile features. The ProfileProvider and company will save us time in developing a framework to store user specific data between visits. Use the feature to build a shopping cart, personalize your web application, and more.
Questions? Comments? Leave them here.