Inside the ASP.NET user profile mechanism

This article was originally published in VSJ, which is now part of Developer Fusion.
For a long time, web applications didn’t require a rich set of personalization features. Most applications looked the same to any connected user, and HTTP cookies were available to those applications needing to save some user-related data and preferences. An HTTP cookie is a small amount of information sent to the client through response headers. Browsers optionally save this information in an isolated region of the client’s hard disk, and send it back with any requests directed at the same originating URL. Internally, a cookie is made of key/value pairs where every piece of information is expressed as raw text. Cookies have two main drawbacks: their limited size, which is seldom greater than 8k, and the control that end users exercise over them. Cookies can in fact be disabled by users, thus making totally ineffective any application features built on top of them.

Modern and functionally richer web applications need a reliable layer of personalization so that they seem friendlier, more functional and appealing to use. For some categories of applications such as ecommerce sites and portals, personalization is essential. For others, it is a way to improve the visual appearance. In either case, an effective personalization layer can’t be based on cookies, and can’t be left to each team of developers for a custom implementation.

The profile API in ASP.NET 2.0 is Microsoft’s answer to the demand for an effective personalization layer built into the ASP.NET framework. With the profile API, developers have no need to resort to the session state or cookies to persist user-specific information across sessions and regardless of possible server failures. Developers just write their pages to use a new HTTP context object – the Profile object – and the object does the magic of reading and writing user information to a persistent store of choice. In this article, we’ll look at the internal implementation of the ASP.NET Profile object, its relationship with anonymous identification, and the role of profile providers.

The user profile explained

In the abstract, the ASP.NET user profile is a collection of typed properties grouped into a dynamically generated class. While cookies are a name/value collection of text-based data, a user profile is a class with any number of typed properties that the developer is free to define on a per-application basis. In other words, to store a colour and a number in a cookie you create two untyped entries in a collection. You write the information as raw text with the serialization and deserialization process:
HttpCookie cookie = new
	HttpCookie(“UserSettings”);
cookie.Values[“BackgroundColor”]
	= color.Name;
cookie.Values[“RowsToDisplay”]
	= rowCount.ToString();
Response.Cookies.Add(cookie);
To store the same pieces of information in the ASP.NET Profile object you set properties on the Profile objects, as below:
Profile.BackgroundColor = color;
Profile.RowsToDisplay = rowCount;
In this example, the BackgroundColor and RowsToDisplay properties are strongly typed and defined by the developer, and shared by all sessions in the application. Each application may have its own profile object with a different set of members, but who’s in charge of defining the layout of the profile class? The layout of the user profile is defined in the configuration file (web.config), and consists of a list of typed properties. Each property can be of any .NET common language runtime (CLR) type, including collections, arrays, and user-defined types. In the web.config file, you just store the description of the profile object as you want it to be. You list property names and types, and also may optionally leave there suggestions on how the ASP.NET runtime has to serialize and deserialize property values. Here’s a quick example of the profile section in the web.config file:
<profile>
	<properties>
	<add name=”Theme” type=”String” />
	<add name=”Rows” type=”Integer” />
	<add name=”Favorites”
		type=”System.Collections.
	Specialized.StringCollection” />
	</properties>
</profile>
The <profile> section is located under <system.web>. The <properties> section lists all properties that form the user profile’s data model. The contents of this portion of the web.config file are parsed when the application starts up, and originate a dynamic class. The dynamically created profile class is named ProfileCommon and derives from ProfileBase. Here’s an example:
namespace ASP {
	public class ProfileCommon :
		ProfileBase {
		public virtual string Theme {
			get {(string)
			GetPropertyValue(“Theme”);}
			set {SetPropertyValue(
				“Theme”, value);}
		}
		// Other properties in the
		// web.config are implemented
		// here
		public virtual ProfileCommon
			GetProfile(string username) {
			object o =
			ProfileBase.Create(username);
			return (ProfileCommon) o;
		}
	}
}
The ProfileBase class is a .NET Framework class that incorporates all the logic required to manage the I/O of user profiles. An instance of the ProfileCommon class is exposed out of the HTTP context through the Profile property. All ASP.NET applications have a Profile object, but each application will be bound to a distinct profile object. The definition of the ProfileCommon class is based on the contents of the <profile> section, and may differ on a per-application basis.

All profile classes, though, share a certain behaviour – the behaviour provided by the parent ProfileBase class. This class provides methods to retrieve and update the profile and manage the persistence of the user profile. An instance of the profile object is associated with each authenticated user that happens to use the application. As the user interacts with the application, the profile may be updated. At the end of each request, the state of the profile is persisted to the data store. When a new request comes in from the same user, the profile is retrieved from the data store and attached to the context of the request. This behaviour is built into the ProfileBase class.

Managing the persistence

Page authors don’t have to worry about profile persistence. The ASP.NET runtime environment knows how to retrieve the user’s profile and knows where to save the modified profile when the request terminates. The overall functionality is not surprising per se. On the other hand, isn’t it through a similar mechanism that session state has always been retrieved in web applications?

In ASP.NET, a special component named the “profile provider” is installed to receive I/O instructions from the ASP.NET runtime, and to save and read the profile of a given user. More interestingly, the default profile I/O provider can be unplugged and replaced with a custom one that uses a customized data store such as an Oracle database or an XML file. By default, however, the profile information goes to a fixed SQL Server Express database.

Any personalization data is persisted on a per-user basis and is permanently stored until someone with administrative privileges deletes it. The data storage is hidden from the user and, to some extent, from the programmer. The user doesn’t need to know how and where the data is stored; the programmer simply needs to indicate which profile provider he wants to use. Then, the personalization provider determines the data store to use. Let’s take a closer look at the request lifecycle as far as the profile management is concerned. Figure 1 provides a graphical view of the role played by the profile provider.

Figure 1
Figure 1: The role of the profile provider in the request lifecycle

The ASP.NET pipeline puts a special HTTP module in charge of the user profile functionality. The profile module kicks in once the request has been authenticated. Using the identity of the user as a key, the module interrogates the registered profile provider to get the profile object for the current user. The HTTP context object associated with the pending request initially exposes an empty instance of the ProfileCommon class with the application-specific layout – the HttpContext.Profile property. The profile provider deserializes any data found in the storage medium, be it a SQL Server database, an XML file, or even an HTTP cookie. Next, the provider stores any information in the profile property on the HttpContext object.

From now on, the code attached to the page can consume this data using the Profile member on the HttpContext class. Any property registered through the web.config data model can be addressed in a strongly-typed manner. The ProfileCommon class, as defined by the development team, is definitely part of the project and can be accessed directly and without intermediate filters. When the request is going to terminate, the profile HTTP module kicks in again and orders the profile provider to serialize the status of the profile object for the current user.

The default profile provider is named AspNetSqlProfileProvider, and uses SQL Server 2005 Express as the data storage. Providers are registered in the <providers> section of the configuration file under the main node <profile>, as in the following configuration script excerpted from the machine.config file:

<profile defaultProvider=
	”AspNetSqlProfileProvider”>
	<providers>
	<add name=”AspNetSqlProfileProvider”
connectionStringName=”LocalSqlServer”
			applicationName=”/”
		type=
”System.Web.Profile.SqlProfileProvider”
		/>
	</providers>
</profile>
The list of <add> nodes within the <providers> section details the currently registered providers. The name and type attributes are common to all types of providers and indicate the name and class information for the specified provider. Other properties are part of the provider’s specific configuration mechanism. The description attribute gives the expected behaviour of the provider, and the connectionStringName contains the information needed to set up a connection with the data store. For the default provider, it doesn’t have to contain a plain connection string, but only the name of an entry in the <connectionStrings> section of the configuration file where actual connection information is stored.

A custom profile provider is a class that inherits from the base class ProfileProvider and overrides a handful of methods. Once you have created a similar class, you register it with the application by adding a new <add> section to the above script. To select the default provider, you add a defaultProvider attribute to the <profile> section. The default provider stores profile information to the aspnetdb.mdf database that you can create on the development machine using the ASP.NET Web Site Administration Tool (WSAT), fully integrated in Visual Studio .NET 2005. The database is located in the App_Data special folder of the ASP.NET application and has a fixed set of tables and schema. Note that the same database contains tables to hold membership and roles information. You then deploy the database to the production machine as part of the application setup.

Inside the profile data model

When defining the profile data model in the web.config file, you can use a number of predefined attributes to tune the behaviour of the ProfileCommon class. The profile data model is the set of properties you want to define on the dynamically created ProfileCommon class. The <add> section under the <properties> section can be decorated with any combination of the attributes in Table 1. Note that Name and Type are the only mandatory attributes.

Table 1: Attributes for the profile data model

Attribute Description
Name Specifies the name of the property.
Type Specifies the type of the property. Use the “class, assembly” notation in case of user-defined types.
AllowAnonymous Indicates whether the property is usable by anonymous users.
DefaultValue Specifies the default value for the property.
Provider Specifies the name of the custom profile provider object to use to manage the property.
ReadOnly Indicates whether the property is read-only. (Default is false.)
SerializeAs Indicates how the property should be serialized to the data store: as a string, as a stream of bytes, as XML, or using the specified provider.
CustomProviderData Specifies any context string to pass to the provider (in case it is not the default one) for initialization purposes.

If the property is assigned a default value, and the property value is not modified during the request processing, then the property is not persisted to the data store. Likewise, a property marked as read-only is not included in any writing operation back to the data store. The value of a property may need serialization to be stored to a persistent data store. By using the SerializeAs attribute, you can decide how this should happen. The feasible options are listed in Table 2.

Table 2: Options for profile serialization

Attribute Description
Binary The value is serialized using binary serialization.
ProviderSpecific The provider has implicit knowledge of the type of the value and decides how to serialize into the data store. This is the default option.
String The value is serialized to a string.
Xml The value is serialized using XML serialization.

You can have multiple profile providers at work for the various profile properties. By default, all properties are saved and read using the currently selected profile provider. However, you can ask the ASP.NET runtime to process a particular property using a specific provider. If this is the case, you just set the Provider attribute to the name of the profile provider to use. The profile provider must be registered with the application.

The <properties> section also accepts a <group> element. The <group> element is meant to group a few related properties as if they are properties of an intermediate object.

<properties>
	:
	<group name=”Font”>
		<add name=”Name” type=”string”
			defaultValue=”verdana” />
		<add name=”SizeInPoints”
		type=”int” defaultValue=”8” />
	</group>
</properties>
To access any of the preceding properties, use the following syntax.
Response.Write(Profile.Font.Name);
Groups of properties help to better organize the attributes of the data model and can be nested to multiple levels.

Working with the profile object

As mentioned, a system component is responsible for automatically retrieving and saving the profile information. The code-beside attached to ASP.NET pages can access profile information through the Profile property on the HttpContext object. The same object is also mirrored through a property on the page class. The following two instructions are totally equivalent, but the second is a little bit faster:
Response.Write(
	Profile.BackgroundColor);
Response.Write(
	Context.Profile.BackgroundColor);
To be precise, the Page class has no Profile property defined and you won’t find it documented anywhere; yet the first line above works just fine. The point is that a protected Profile property is added to the dynamically created class that is used to serve an ASP.NET request. The ultimate page class used to serve a request has the following piece of code added:
protected ProfileCommon Profile {
	get {
		return ((ProfileCommon)(Context.Profile));
}}
As you can see, calling the Profile property on the page class returns the same information as the Context object, but requires an extra step.

Part of the request’s HTTP context, the profile object is common to all pages visited by the same user. However, each request is attached to an updated version of the object filled with fresh data as modified by previous requests made by the same user in any sessions. It is essential to note that each user has its own copy of the profile object. Any data stored in the profile has no predefined duration and is permanently stored until site administrators delete the information when convenient. A user profile can’t be deleted programmatically.

Note that the value associated with the Profile object is never null. When no value is present for a user, an empty instance of the ProfileCommon class is returned. There’s just one situation in which the Profile object is null – when the profile functionality is disabled. You enable and disable the profile functionality using the enabled attribute on the <profile> node of the web.config file:

<profile enabled=”false”> : </profile>
While the Profile object is never null by design, the same isn’t necessarily true for its child properties. If the profile is expected to contain (for example) a collection property, you have to check the property against null before use:
if (Profile.Links != null) DisplayLinks(Profile.Links);
To update the value of a property, you simply assign the new value to the property:
Profile.Links.Add(“http://www.solidqualitylearning.com”);
Profile.Links.Add(“http://www.vsj.com”);
Profile.BackgroundColor = Color.LightYellow;
Can you be sure that any modified value is automatically saved to the data store so that it can be retrieved on the next request made by the same user in the same, or another, session? By default, the profile is saved at the end of each request, as long as the profile object is dirty, that is there are pending changes and current values are different from default values of involved properties. You can disable the auto-saving feature by adding an attribute to the <profile> node, as below:
<profile automaticSaveEnabled=”false”> : </profile>
If automatic saving is disabled, you can request a save programmatically by invoking the Profile.Save method.

Anonymous profiles

The profile feature is strictly user-specific; however, ASP.NET also supports profiles for anonymous users. In this context, an anonymous user is simply a user who didn’t log in to the ASP.NET application; the feature has nothing to do with the IIS anonymous user and is independent of any type of ASP.NET authentication (Forms, Windows, Passport). To enable anonymous profiles, you set the following script in the web.config file.
<anonymousIdentification
	enabled=”true” />
In this way, a unique identity (typically, a GUID) is associated with each unauthenticated user so that the ASP.NET application can recognize and treat such users as regularly registered users. This approach formalizes a common practice that most developers use in their applications to come up with a simplified logic that doesn’t have to distinguish between authenticated and anonymous users. In this way, a role-based UI can be developed, and users will easily pass from one logical role to the next as they log in.

Anonymous users can manage their profile settings, but have access only to those properties explicitly flagged with the allowAnonymous attribute. Needless to say, no anonymous data is ever serialized to the profile data store. How can the application recognize a particular anonymous user when he’s back and apply all of his settings? For anonymous users, a cookie is generated and appended to the request. Name, domain, protection, and expiration of the cookie can be tuned in the configuration file.

Weren’t profiles a way to replace cookies? Correct, but in this case an alternative and faster mechanism is required to persist data that is less critical than the profile of registered users. However, if you want to avoid cookies, you can opt for a cookieless approach. Here’s the full schema of the <anonymousIdentification> section:

<anonymousIdentification
	enabled=”[true | false]”
	cookieless=”[UseUri | UseCookies |
		AutoDetect | UseDeviceProfile]”
	cookieName=”.ASPXANONYMOUS”
	cookiePath=””
	cookieProtection=”[None |
		Validation | Encryption | All]”
	cookieRequireSSL=”[true | false]”
	cookieSlidingExpiration=
		”[true | false]”
	cookieTimeout=”[DD.HH:MM:SS]”
	domain=”cookie domain”
/>
The cookieless attribute indicates whether cookies are used or not. The effects of all possible choices are shown in Table 3.

Table 3: Options for the cookieless attribute

Mode Description
AutoDetect Use cookies for anonymous profiles only if the requesting browser supports cookies
UseCookies Use cookies for anonymous profiles regardless of whether or not the browser supports cookies. This is the default option.
UseDeviceProfile Base the decision on the browser’s capabilities as listed in the device profile section of the configuration file.
UseUri Store the anonymous profile in the URL regardless of whether the browser supports cookies or not. Use this option if you want to go cookieless no matter what.

The anonymous identification feature is governed by an HTTP module that exposes a Creating event. By handling the event in the global.asax file, you can assign the anonymous user your own ID. If you don’t specify a custom ID, a GUID is used. Finally, note that anonymous identification in no way affects the identity of the account that is processing the request.

Migrating anonymous settings

What happens when an anonymous user decides to log in? The profile HTTP module fires an application-wide event named MigrateAnonymous. By handling this event in the global.asax file, an application can easily move all or some of the anonymous settings to the profile of the now registered user. In this case, the settings are then persisted to the data store. An anonymous user that attempts to log in is recognized by the .ASPXANONYMOUS cookie attached to the request. The following listing shows how to migrate anonymous settings to the user profile:
void Profile_MigrateAnonymous(
	object sender,
	ProfileMigrateEventArgs e) {
	// Load the profile of the
	// anonymous user
	ProfileCommon anonProfile;
	anonymProfile =
	Profile.GetProfile(e.AnonymousId);

	// Migrate the properties to the
	// new profile
	Profile.Theme = anonymProfile.Theme;
	:
}

Role-based profiles

The profile HTTP module may also raise other application-wide events such as Personalize. In particular, this event fires just before the profile data is retrieved. An analogous event – ProfileAutoSaving – is fired just before the profile data is persisted. In both cases, you can take control of the I/O process and alter the data being passed to the application or written to the store. The Personalize event is essential if you want to give users a different profile object based on their role. In this case, you write a global.asax handler for the Personalize event and check the role of the current user:
void Profile_Personalize(object sender,
	ProfileEventArgs e) {
	ProfileCommon profile = null;
	if (User == null)
		return;

	// Determine the profile based on
	// the role
	if (User.IsInRole(
		“Administrators”))
		profile = (ProfileCommon)
			ProfileBase.Create(
			“Administrator”);
	else if (User.IsInRole(“Users”))
		profile = (ProfileCommon)
			ProfileBase.Create(“User”);

	// Make the HTTP profile module use
	// THIS profile object
	if (profile != null)
		e.Profile = profile;
}
Based on the role, you explicitly create a new profile using the name of the role as the key to retrieve profile information from the data store. In other words, you have as many records in the profile store as there are roles supported in your application. Finally, you need to inform the HTTP profile module that you’re done, and there’s no need for it to read information from the data store. You do this by storing a non-null object in the Profile property of the ProfileEventArgs data structure.

Summary

For modern web pages, personalization is a key factor. In ASP.NET 2.0, the user profile API allows you to write pages that persist user preferences and parametric data from 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 interact with a new member of the HttpContext class – the Profile object. In turn, the Profile object controls a hidden mechanism based on the profile provider and HTTP module. Under the hood, the mechanism does the rest of the job. And it is indeed a job well done…


Dino Esposito is a Solid Quality Learning mentor, and author of the two-volume Programming Microsoft ASP.NET 2.0 (Microsoft Press, 2005). He is a regular speaker at Bearpark’s annual DevWeek conference – the 10th annual event is scheduled for 26 February–2 March 2007.

You might also like...

Comments

About the author

Dino Esposito United Kingdom

Dino Esposito is an instructor for Solid Quality Mentors, and a trainer and consultant based in Rome. He is the author of various books, including Windows Shell Programming, Instant DHTML Script...

Interested in writing for us? Find out more.

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“Debugging is anticipated with distaste, performed with reluctance, and bragged about forever.” - Dan Kaminsky