Profile Tips and Tricks

Practical Guidance for Managing User Profiles in ASP.NET 2.0

ASP.NET Under the Hood

LANGUAGES: VB.NET | C#

ASP.NET VERSIONS: 2.0

 

Profile Tips and Tricks

Practical Guidance for Managing User Profiles in ASP.NET 2.0

 

By Michele Leroux Bustamante

 

Greetings ASP.NET architects and developers! This installment of ASP.NET Under the Hood answers a reader s question regarding best practices for implementing ASP.NET profiles. What do you want to hear about? Send your requests to [email protected].

 

Q. Profiles are not a mystery to me, but I m not sure what the best approach is for implementing them. For example, I d like to get a handle on some best practices for collecting profile data while registering users, promoting anonymous users, saving changes to profiles, and so on. Can you write about this in your column, or provide me with some related samples?

 

A. Profiles are definitely a convenient feature in ASP.NET 2.0. With profiles, you can collect personalization settings from users and save those settings in a database in just a few short steps. Because ASP.NET 2.0 provides us with a database schema to store customized profile data, and a strongly-typed object model to store and retrieve that data, set up time is truly minimal. In this article, I ll provide a very quick overview of how profiles work, and then cut right to looking at some implementation scenarios for optimizing how profile data is saved; for supporting and promoting anonymous users; for the registration process; for accessing profile settings at the right moment during the round trip; for handling caching; and for creating a reusable HTTP module to handle common personalization practices.

 

Profiles in a Nutshell

The first step to working with profiles is to configure your database to support the ASP.NET application server schema by running aspnet_regsql.exe. The wizard will walk you through the steps to generate the required membership, roles, profiles, and related tables (usually in a database named aspnetdb). Once you ve initialized your database, you can create a profile schema in the web.config of your ASP.NET application. This is done inside the section within the element. Here s a simple example of a profile schema:

 

 

   

   

 

 

This lets you collect two string elements to store the user s preferred Theme and Culture settings. By default, these properties are stored as string values. You can access values at run time using the global Profile object, which is a strongly-typed class generated from your specific schema. Thus, you can write code like this to save and retrieve values:

 

Profile.Theme = "Default";

Profile.Culture = "en-US";

string theme = Profile.Theme;

string culture = Profile.Culture;

 

The strongly-typed Profile object inherits ProfileBase, which encapsulates core functionality for saving and retrieving values.

 

Optimizing Profile.Save

By default, Profile property values are automatically saved at the end of each HTTP request. ASP.NET uses ProfileModule to intercept the HttpApplication.EndRequest event where it checks to see if automatic save is enabled in the section, as shown here:

 

...

 

The attribute automaticSaveEnabled is true by default, and, when enabled, ProfileModule calls Profile.Save to persist the latest state of the user s Profile data at the end of each request. Because all profile property values are stored in a single field, it is necessary to save all dirty or non-default property values each time the Profile is saved. That means if any non-default values exist in the user s Profile, the entire set of properties is resaved at the end of each request even if no changes have been made.

 

Directly before the Profile is automatically saved, ProfileModule fires a ProfileAutoSaving event. You can intercept this event in the Global.asax and suppress profile saving by setting the argument s ContinueWithProfileAutoSave property to false:

 

void Profile_ProfileAutoSaving(object sender,

 ProfileAutoSaveEventArgs e)

{

 e.ContinueWithProfileAutoSave=false;

}

 

This cancels the act of saving the Profile, which is not entirely useful. In fact, intercepting the ProfileAutoSaving event implies that you are able to determine if it is appropriate to save the profile, or not. A better solution would be to control this from the user interface in charge of Profile modifications. Thus, it is better to suppress automatic Profile saving altogether, forget about intercepting the ProfileAutoSaving event, and explicitly call Profile.Save each time the user modifies their Profile:

 

...

// save new profile settings

Profile.Culture = this.EditProfileControl1.Culture;

Profile.Theme = this.EditProfileControl1.Theme;

Profile.Save();

 

This reduces the overhead of unnecessary calls to issue the ProfileAutoSaving event, not to mention unnecessary Profile-saving activities.

 

Handling Anonymous Users

Supporting anonymous users is a handy feature of the profile provider model. This allows you to save preferences for users that have not yet become registered users of your application. For example, you may allow anonymous users to indicate their preferred culture or site theme even before they register. You have to explicitly indicate anonymous support for each profile property to which anonymous users have access by setting the allowAnonymous attribute to true (the default is false). Only properties with anonymous access enabled can be set when the user has not been authenticated.

 

To support anonymous identification you must also add the element:

 

 

   

  

 

 

Anonymous users are identified by a unique GUID, usually stored in a cookie or query string parameter that can be provided with each request. This identifier is created by AnonymousIdentificationModule during the HttpApplication.PostAuthenticateRequest event (if the user has not yet been authenticated). If anonymous identification is supported, a Profile is created for the anonymous user and is associated with this identifier. Profile properties are saved with this anonymous profile, and their values can be transparently mapped to a user account, once created.

 

ProfileModule fires a MigrateAnonymous event if the request is authenticated and an anonymous profile is still present, which gives you an opportunity to map the anonymous profile properties to the authenticated user s profile. You can intercept this event in the Global.asax and find the anonymous profile through the ProfileMigrateEventArgs parameter:

 

void Profile_MigrateAnonymous(object sender,

 ProfileMigrateEventArgs e)

{

 ProfileCommon anonProfile =

   Profile.GetProfile(e.AnonymousID);

 Profile.Theme = anonProfile.Theme;

 Profile.Culture = anonProfile.Culture;

 ProfileManager.DeleteProfile(e.AnonymousID);

 AnonymousIdentificationModule.ClearAnonymousIdentifier();

}

 

This is the place to map each anonymous property value to the new Profile. There is one catch to this. MigrateAnonymous is also triggered after an existing user is authenticated, and in this case you don t want to overwrite their Profile with the anonymous Profile. You can check the LastActivityDate and LastUpdateDate of the authenticated user Profile to see if they have been set or not:

 

if (Profile.LastActivityDate == DateTime.MinValue &&

 Profile.LastUpdateDate == DateTime.MinValue)

{

 ... migrate Profile

}

 

Regardless, it is good practice to delete the anonymous Profile and clear the associated anonymous identifier during the MigrateAnonymous event.

 

Profiles and User Registration

ASP.NET 2.0 supplies us with a CreateUserWizard server control that encapsulates the process of collecting the required information to create a user account and associate the new user with the current request thread. Why not collect their Profile preferences at the same time? You can quite easily add one or more steps to the CreateUserWizard flow to collect those preferences after the user has been successfully created.

 

The first step in CreateUserWizard flow is to collect the required information to create a new user (see Figure 1). Without writing a line of code, the Create Account button will trigger a call to the Membership API, which creates the new user account. This also triggers the MigrateAnonymous event on the postback, so you can transfer any applicable anonymous profile settings to the authenticated user s profile.

 

Figure 1: The first step of the CreateUserWizard.

 

You can add another step to the wizard to collect Profile data (see Figure 2). Because the first step has already created the user account, and the user is now authenticated, any information collected here will be saved to the user s Profile.

 

Figure 2: Extending the CreateUserWizard to collect Profile data.

 

If the current Profile is anonymous (that is, another user was not logged in when CreateUserWizard is kicked off) you can initialize controls with that anonymous Profile:

 

if (Profile.IsAnonymous)

{

 ... initialize controls from current Profile

}

 

Because CreateUserWizard authenticates the new user prior to collecting Profile settings, any selection can be saved in the currently active Profile.

 

Accessing Profile Settings During the Round Trip

You may need to access Profile settings very early in the request cycle (to dynamically control run-time behavior or content presentation). I ll discuss some concrete examples of this related to culture selection, dynamic theme selection, and dynamic master page selection.

 

Localized ASP.NET applications rely on the thread s culture settings to load the correct satellite assembly resources and to handle culture-aware formatting, such as dates, times, and sort order. When the user Profile is used to store culture preferences, you ll need to dynamically set the thread s UICulture and Culture properties for each request. It is also important to apply these settings after the runtime has applied any defaults from the section. The HttpApplication.PreRequestHandlerExecute event is an excellent place to initialize the thread s CurrentCulture and CurrentUICulture properties:

 

void application_PreRequestHandlerExecute(

 object sender, EventArgs e)

{

 if (!String.IsNullOrEmpty(System.Web.Profile.Culture))

 {

   Thread.CurrentThread.CurrentUICulture =

     new CultureInfo(Profile.Culture);

   Thread.CurrentThread.CurrentCulture =

     CultureInfo.CreateSpecificCulture(

      System.Web.Profile.Culture);

 }

}

 

Dynamically assigning themes and master pages requires a slightly different approach. During the HttpApplication.PreRequestHandlerExecute event the Page handler has not yet been initialized. That means that the runtime has yet to apply web.config or Page configuration settings for the theme or master page properties. So, instead of applying Profile settings during PreRequestHandlerExecute, it is better to hook the PreInit event for the Page:

 

void application_PreRequestHandlerExecute(

 object sender, EventArgs e)

{

 HttpApplication app = sender as HttpApplication;

 if (app == null) return;

 Page p = app.Context.Handler as Page;

 if (p == null) return;

 p.PreInit += p_PreInit;

}

 

The PreInit event is fired after default settings have been applied, but before the Page control tree has been initialized. If you supply a new setting for Theme and MasterPageFile properties here, the control tree will be initialized with the correct values in place. The following code illustrates this hook:

 

private void p_PreInit(object sender, EventArgs e)

{

 Page p = sender as Page;

 if (!String.IsNullOrEmpty(Profile.Theme))

   p.Theme = Profile.Theme;

 if (!String.IsNullOrEmpty(Profile.Department))

   p.MasterPageFile = "~/" + Profile.Department + ".master";

}

 

In this example, the user s Profile includes a Department property that is directly mapped to a nested master page in the application (see Figure 3).

 


Figure 3: Nested master pages directly applied based on Profile settings.

 

Caching Based on Profile Settings

Output caching has a significantly positive affect on application performance but often requires special attention because of the number of cache key variables. For example, if content varies by culture, theme, and master page, all three values must be included in the cache key. That also means that very early in the request cycle, before the Page handler is loaded, we need access to the correct run-time values that influence the cache key.

 

To cache a page or user control based on custom values, you would apply an @OutputCache directive with a VaryByCustom attribute that identifies all values to combine in creating the cache key:

 

<%@ OutputCache Duration="86400" VaryByParam="None"

 VaryByCustom="browser+culture+theme+department" %>

 

To supply the runtime with values for the cache key, override the HttpApplication.GetVaryByCustomString method, as shown in Figure 4.

 

public override string GetVaryByCustomString(HttpContext ctx,

 string customstring)

{

 string culture = PersonalizationModule.ProfileTyped.Culture;

 if (string.IsNullOrEmpty(culture))

   culture = System.Threading.Thread.CurrentThread.CurrentCulture.Name;

 string theme = PersonalizationModule.ProfileTyped.Theme;

 string department = PersonalizationModule.ProfileTyped.Department;

  

 string browser = ctx.Request.Browser.Type;

 if (customstring.ToLower(System.Globalization.CultureInfo.InvariantCulture)

   == "browser+culture+theme")

 {

   return String.Format("{0}+{1}+{2}", browser, culture, theme);

 }

 if (customstring.ToLower(System.Globalization.CultureInfo.InvariantCulture)

   == "browser+culture+theme+department")

 {

   return String.Format("{0}+{1}+{2}+{3}",

     browser, culture, theme, department);

 }

 return null;

}

Figure 4: Implementation of GetVaryByCustomString for custom caching based on profile settings.

 

In this example, Profile settings play a key role in generating the cache key. The sneaky thing is that GetVaryByCustomString is invoked before the Profile has been initialized. Normally, the Profile is automatically initialized when you first access it, but at this time in the request cycle that is not the case. To work around this, the PersonalizationModule (to be discussed) has a static member, ProfileTyped, that creates and returns an instance of the initialized Profile:

 

ProfileCommon profile = ProfileBase.Create(

 HttpContext.Current.Request.IsAuthenticated ?

 HttpContext.Current.User.Identity.Name :

 HttpContext.Current.Request.AnonymousID,

 HttpContext.Current.User.Identity.IsAuthenticated)

 as ProfileCommon;

 

HTTP Modules and Personalization

Many of the features discussed in this article, such as dynamic culture, theme and master page assignment, and anonymous Profile migration, can be implemented directly in the Global.asax. These features can also be implemented in an HTTP module, which makes it easier to isolate, reuse, and maintain code related to application personalization and profiles.

 

In the sample code provided (available for download; see end of article for details), I ve implemented a PersonalizationModule that implements IHttpModule. During module initialization an event handler is provided for the HttpApplication.PreRequestHandlerExecute event (see Listing One) discussed earlier, where the code to initialize the thread s culture settings and the code to hook the Page.PreInit event is implemented.

 

The code within the PreRequestHandlerExecute and PreInit overrides remain the same as described earlier with one catch. The Profile type is not directly accessible from the module, so I added a Profile property to the module that returns the strongly typed ProfileCommon type (shown in Listing One). This makes it possible to write code directly against a Profile type:

 

Thread.CurrentThread.CurrentUICulture =

 new CultureInfo(Profile.Culture);

 

In the same module, I also captured the MigrateAnonymous event fired by the ASP.NET ProfileModule. You can see that in the Init method shown in Listing One, I iterate loaded modules looking for ProfileModule. Once found, I can hook the event and write code to migrate anonymous users.

 

To support custom caching implemented in the Global.asax I provided a static property to return the un-typed ProfileBase reference, initialized for the authenticated user. This simplifies the code in GetVaryByCustomString:

 

ProfileBase profile = PersonalizationModule.ProfileUntyped;

 

Although it is possible to isolate dynamic assignment of culture, theme, and master page into individual modules because all three derive their settings from the Profile it makes more sense to combine these behaviors into a single PersonalizationModule. Of course, your implementation may vary in terms of which settings are collected to affect culture, theme, master page and other settings ... but this should give you a good foundation to get started.

 

Conclusion

Hopefully this article has given you a sense of some common practices that are useful when working with Profiles. In particular, the combination of typical scenarios, such as registration, output caching, and round-trip timing for Profile access, as well as how to leverage HTTP modules to isolate your personalization functionality. In my example, I ve assumed you are using the ASP.NET database schema for profiles, but you can also achieve the same results by using a custom Profile provider. Enjoy!

 

If you have questions about this or other ASP.NET topics, drop me a line at [email protected]. Thanks for reading!

 

C# and VB.NET code examples are available for download.

 

Michele Leroux Bustamante is Chief Architect at IDesign Inc., Microsoft Regional Director for San Diego, Microsoft MVP for XML Web services, and a BEA Technical Director. At IDesign Michele provides training, mentoring, and high-end architecture consulting services, specializing in scalable and secure .NET architecture design, globalization, Web services, and interoperability with Java platforms. She is a board member for the International Association of Software Architects (IASA), a frequent conference presenter, conference chair of SD s Web Services track, and a frequently published author. She is currently writing a book for O Reilly on the Windows Communication Foundation. Reach her at http://www.idesign.net or http://www.dasblonde.net.

 

Additional Resources

IDesign: http://www.idesign.net

Michele s blog: http://www.dasblonde.net

Michele s WCF book: http://www.thatindigogirl.com

MSDN References:

http://msdn.microsoft.com/msdnmag/issues/06/04/ExtremeASPNET/

 

Begin Listing One reusable personalization code implemented in a custom HTTP module

public class PersonalizationModule: IHttpModule

{

 public static string CULTURE = "Culture";

 public static string THEME = "Theme";

 public static string DEPARTMENT = "Department";

 public ProfileCommon Profile

 {

   get

   {

     return HttpContext.Current.Profile as ProfileCommon;

   }

 }

   public static ProfileCommon ProfileTyped

   {

       get

       {

           ProfileCommon profile = ProfileBase.Create(

 HttpContext.Current.Request.IsAuthenticated ?

 HttpContext.Current.User.Identity.Name :

 HttpContext.Current.Request.AnonymousID,

 HttpContext.Current.User.Identity.IsAuthenticated)

           as ProfileCommon; return profile;

       }

   }

 public void Init(HttpApplication application)

 {

   application.PreRequestHandlerExecute +=

     new EventHandler(application_PreRequestHandlerExecute);

   for (int i = 0; i < application.Modules.Count; i++)

   {

     ProfileModule profileModule = application.Modules[i]

       as ProfileModule;

     if (profileModule != null)

     {

       profileModule.MigrateAnonymous += new

         ProfileMigrateEventHandler(

           profileModule_MigrateAnonymous);

     }

   }

 }

 void profileModule_MigrateAnonymous(object sender,

   ProfileMigrateEventArgs e)

 {

   if (IsNewProfile())

 {

     ProfileCommon anonProfile =

       Profile.GetProfile(e.AnonymousID);

     Profile.RegisteredSince = anonProfile.RegisteredSince;

     Profile.Culture = anonProfile.Culture;

     Profile.Theme = anonProfile.Theme;

     Profile.Department = anonProfile.Department;

     ProfileManager.DeleteProfile(e.AnonymousID);

     AnonymousIdentificationModule.ClearAnonymousIdentifier();

 }

 private bool IsNewProfile()

 {

   return (Profile.LastActivityDate == DateTime.MinValue &&

     Profile.LastUpdatedDate == DateTime.MinValue);

 }

 void application_PreRequestHandlerExecute(object sender,

   EventArgs e)

 {

   ...

 }

 private void p_PreInit(object sender, EventArgs e)

 {

   ...

 }

}

End Listing One

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish