Extreme ASP.NET

A New Solution to an Old State Storage Problem

Fritz Onion

Code download available at:ExtremeASPNET0604.exe(120 KB)

Contents

Profile Fundamentals
Serialization
User-Defined Types as Profile Properties
Optimizing Profile
Going the Custom Route
Conclusion

State management in Web applications is a contentious issue. Should you store user data per session or should you persist it across sessions? You can easily store information temporarily while someone navigates your site by using session state. That's typically an in-memory data store indexed by a unique session key assigned to each new user and that lasts only for the duration of the "session" with the client. Storing information across sessions is typically done by building your own back-end data store indexed by some user identifier (typically obtained after a user logs onto the site).

There's a grey area between these two levels of per-client storage. What if you want to store data across sessions for anonymous users as well? Wouldn't it be convenient if you could store data on behalf of clients in a persistent store without having to implement an entire back-end data store? This niche is now filled quite nicely by the Profile feature of ASP.NET 2.0.

Profile provides a simple way of defining database-backed user profile information. With a few configuration file entries, you can quickly build a site that stores user preferences (or any other data, for that matter) into a database, all with a simple type-safe interface for the developer. Profile looks and feels much like Session state, but unlike Session state, Profile is persistent across sessions. Profile is tied into the ASP.NET 2.0 membership system so that data for authenticated clients is stored in association with their real identities instead of with some arbitrary identifier. Anonymous clients have an identifier generated for them stored as a persistent cookie, so that on subsequent access from the same machine they will retain their preferences.

As an ASP.NET developer, it is important to understand the options you have for storing state and when each of these options is most appropriate. In this column I'll explore Profile and where it fits in the state management spectrum.

Profile Fundamentals

The first step when using Profile is to declare the properties you would like to store on behalf of each user in your Web.config file under the <profile> element. The example in Figure 1 shows three property declarations, one each for the user's favorite color, favorite number, and favorite HTTP status code.

Figure 1 Property Declarations

<configuration> <system.web> <profile enabled="true"> <properties> <add name="FavoriteColor" defaultValue="blue" type="System.String" allowAnonymous="true" /> <add name="FavoriteNumber" defaultValue="42" type="System.Int32" allowAnonymous="true" /> <add name="FavoriteHttpStatusCode" type="System.Net.HttpStatusCode" allowAnonymous="true" serializeAs="String" defaultValue="OK" /> </properties> </profile> </system.web> </configuration>

When ASP.NET compiles your site, it creates a new class that derives from ProfileBase with type-safe accessors to the properties you declared. These accessors use the profile provider to save and retrieve these properties to and from whatever database the provider is configured to interact with. Figure 2 shows what the generated class looks like for the properties declared in Figure 1.

Figure 2 Generated Class

public class ProfileCommon : ProfileBase { public virtual HttpStatusCode FavoriteHttpStatusCode { get { return ((HttpStatusCode)(this.GetPropertyValue( "FavoriteHttpStatusCode"))); } set { this.SetPropertyValue("FavoriteHttpStatusCode", value); } } public virtual int FavoriteNumber { get { return ((int)(this.GetPropertyValue( "FavoriteNumber"))); } set { this.SetPropertyValue("FavoriteNumber", value); } } public virtual string FavoriteColor { get { return ((string)(this.GetPropertyValue( "FavoriteColor"))); } set { this.SetPropertyValue("FavoriteColor", value); } } public virtual ProfileCommon GetProfile(string username) { return ((ProfileCommon)(ProfileBase.Create(username))); } }

The second thing that happens is that ASP.NET adds a property declaration for a property named Profile to each generated Page-derived class in your site. This is a type-safe accessor to the current Profile class (which is part of the HttpContext):

public partial class Default_aspx : Page { protected ProfileCommon Profile { get { return ((ProfileCommon)(this.Context.Profile)); } } //... }

This lets you interact with your profile properties in a very convenient way. For example, here's a snippet of code that sets the profile properties based on fields in a form:

void enterButton_Click(object sender, EventArgs e) { Profile.FavoriteColor = colorTextBox.Text; Profile.FavoriteNumber = int.Parse(numberTextBox.Text); Profile.FavoriteHttpStatusCode = Enum.Parse(typeof(HttpStatusCode), statusCodeTextBox.Text); }

If you look in the database used by the profile provider (by default a local SQL Server™ 2005 Express database in your application's App_Data directory), you will see a table called aspnet_Profile with five columns that have the following names:

UserId PropertyNames PropertyValuesString PropertyValuesBinary LastUpdatedDate

For the previous example, these columns were populated with the following values:

405A7333-2C8D-4E63-AB56-BA54398D47DF FavoriteColor:S:0:3:FavoriteNumber:S:3:2:FavoriteHttpStatusCode:S:5:16: red42MovedPermanently <empty> 2006-1-1 09:00:00.000

You can see that, by default, the profile provider uses string serialization with property names (and string lengths), carefully stored on a per-user basis. In the example the user was anonymous, so a GUID was generated and used to index the property values in the aspnet_Profile table. The UserId column is actually a foreign key reference to the UserId column of the aspnet_Users table where the Membership system keeps user information (anonymous users are also stored in this table).

Serialization

As you saw, the default serialization method for properties stored in Profile is to write them out as strings, storing the property names and substring indices in the PropertyNames column. You can control how your properties are serialized by changing the serializeAs attribute on the add element in Web.config. These attributes can have one of the four following values: Binary, ProviderSpecific, String, or Xml.

The default is actually ProviderSpecific, which might better be called TypeSpecific since the type of the object will determine the format of its serialization. What ProviderSpecific means with the default SQL Provider implementation is that the property will be written as a simple string if it is either a string already, or a primitive type (int, double, float, and so forth). Otherwise it defaults to XML serialization, which makes sense because it will work with most types (even custom ones) without any modification to the type definition itself. So ProviderSpecific could have named StringForPrimitiveTypesAndStringsOtherwiseXml, which is obviously quite a mouthful, so something shorter was in order.

This can lead to some confusing behavior if you're not looking out for it. For example, you should consider the following two Profile property definitions:

<add name="TestCode" type="System.Net.HttpStatusCode" defaultValue="OK" /> <add name="TestDate" type="DateTime" defaultValue="1/1/2006"/>

After using integer and string profile properties, adding an enum and a DateTime in this manner seems reasonable. Because the default serialization is ProviderSpecific, you now know that these two types will be serialized with the XmlSerializer, so specifying default values as simple strings is not going to fly (as you will find out quickly once you try accessing the properties). You have two ways of dealing with this problem. One is to specify the XML-serialized value directly in the configuration file (taking care to escape any angle brackets):

<add name="TestCode" type="System.Net.HttpStatusCode" defaultValue="&lt;HttpStatusCode&gt;OK&lt;/HttpStatusCode&gt;" /> <add name="TestDate" type="DateTime" defaultValue="&lt;dateTime&gt;2006-01-01&lt;/dateTime&gt;" />

The other (and perhaps more appealing) option is to change the serialization from ProviderSpecific (which you know turns into XML) to String. String serialization only works for types that have TypeConversions defined for strings, which is fine here as both enums and the DateTime class have string conversions defined. (I will discuss how to write your own string conversions in the next section.) If you look carefully at Figure 1, you will notice that it specifies a serializeAs attribute of String for the FavoriteHttpStatusCode, so that a simple string default value of "OK" could be used:

<add name="TestCode" type="System.Net.HttpStatusCode" serializeAs="String" defaultValue="OK" /> <add name="TestDate" type="DateTime" serializeAs="String" defaultValue="2006-01-01" />

The other option for serialization is Binary, which will use the BinaryFormatter to serialize the property, and with the default SQL provider, will write the binary data into the PropertyValuesBinary column in the database. This is a useful option if you want to make it difficult to tweak the profile values directly in the database, or if you are storing types whose entire state is not properly persisted using XmlSerializer (classes with private data members that are not accessible through public properties fall into this category, for example). Before you can use the binary option, the type being stored must be marked as Serializable or must implement the ISerializable interface. Keep in mind that selecting the binary serialization option makes it difficult to specify a default value, so it is typically used only for complex types for which a default value doesn't make sense anyway. If you do need to specify a default value for binary serialization, you can binary serialize the instance you want for a default value, base64 encode the resulting byte array, and use the base64 encoded representation for the value of the defaultValue attribute.

User-Defined Types as Profile Properties

One of the advantages of the Profile architecture is that it is generic enough to store arbitrary types, and as you have seen, supports several different persistence models. This means that it is quite straightforward to write your own classes to store user data and store the entire class in Profile. Suppose that you wanted to provide a shopping cart for your users. You might write a class to store an individual item containing a description and a cost, and another class that keeps a list of all of the items in the current cart as well as exposing a property that calculates the total cost of all items in the cart (see Figure 3).

Figure 3 Shopping Cart Class

namespace MsdnMag { [Serializable] public class ShoppingCart { private List<Item> _items = new List<Item>(); public Collection<Item> Items { get { return new Collection<Item>(_items); } } public float TotalCost { get { float sum = 0F; foreach (Item i in _items) sum += i.Cost; return sum; } } } [Serializable] public class Item { private string _description; private float _cost; public Item() : this("", 0F) { } public Item(string description, float cost) { _description = description; _cost = cost; } public string Description { get { return _description; } set { _description = value; } } public float Cost { get { return _cost; } set { _cost = value; } } } }

Note that these classes are marked with the [Serializable] attribute in anticipation of using binary serialization. You can then add a profile property of type ShoppingCart to your collection, and you have a fully database-backed per-client persistent shopping cart implemented!

<profile enabled="true"> <properties> <add name="ShoppingCart" type="MsdnMag.ShoppingCart" allowAnonymous="true" serializeAs="Binary" /> </properties> </profile>

Using the shopping cart in your application is as simple as accessing the ShoppingCart property in Profile, and adding new instances of the Item class as needed (the sample available for download has a complete interface for users to shop using this class as the storage mechanism).

Profile.ShoppingCart.Items.Add( new Item("Chocolate covered cherries", 3.95F));

Optimizing Profile

You may be wondering at this point what sort of cost is incurred by leveraging Profile to store your per-client data, especially if you start using complex classes like the ShoppingCart that may end up storing significant amounts of information on behalf of each user. Those of you who have taken advantage of the SQL Server-backed session state feature introduced in ASP.NET 1.0 may be especially leery, since by default each request for a page incurred two round-trips to the state database to retrieve and then flush session state from and to the database. The good news is that by default, the Profile persistence mechanism is reasonably efficient. Unlike out-of-process Session state, it performs lazy retrieval of the profile data on behalf of a user (loading on demand only), and only writes the profile data back out if it has changed.

Unfortunately, if you are storing anything besides strings, DateTime classes, or primitive types, it becomes impossible for the ProfileModule to determine whether the content has actually changed, and it is forced to write the profile back to the data store every time it is retrieved. This is obviously true for custom classes as well, so be aware that adding any types beside string, DateTime, and primitives will force Profile to write back to the database and the end of each request that accesses Profile. Internally there is a dirty flag used to keep track of whether a property in Profile has changed or not. You can explicitly set the IsDirty property for a profile property to false. If you do this for all properties associated with a specific provider instance, then when that provider instance is asked to save the profile data, it will see that all the properties passed to it are not dirty and it will skip communication with the database. This approach relies on knowledge of the underlying SettingsBase, SettingsProperty, and SettingsPropertyValue types (all in System.Configuration). For a profile property called "Nickname", you could force it to not be considered dirty with code like the following:

Profile.PropertyValues["Nickname"].IsDirty = false;

Note that you can disable automatic profile saving using the automaticSaveEnabled attribute on the <profile/> element in the configuration file (this attribute defaults to true). You can set automaticSaveEnabled to false to stop ProfileModule from storing the Profile on your behalf automatically. It is up to you to call Profile.Save if you want to store data back to the database. Alternatively, you can hook the ProfileModule's ProfileAutoSaving event. If you set the ContinueWithProfileAutoSave property on the event argument to false, then the ProfileModule will not call Profile.Save.

As you saw earlier, it is possible to specify String, Binary, or Xml as the serialization mechanism for your properties. If you are storing your own custom classes like the ShoppingCart example, you can take steps to reduce the amount of space used to store instances of your class in one of two ways: by writing your own TypeConverter for the class to support conversion to string format, or by implementing the ISerializable interface to control the format of the binary data used by the BinaryFormatter. Figure 4 shows the default serialization of the ShoppingCart class with four items in it, in XML.

Figure 4 XML-Serialized Shopping Cart with Four Items

<?xml version="1.0" encoding="utf-16"?> <ShoppingCart xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="https://www.w3.org/2001/XMLSchema"> <Items> <Item> <Description>Chocolate covered cherries</Description> <Cost>3.95</Cost> </Item> <Item> <Description>Toy Train Set</Description> <Cost>49.95</Cost> </Item> <Item> <Description>XBox 360</Description> <Cost>399.95</Cost> </Item> <Item> <Description>Wagon</Description> <Cost>24.95</Cost> </Item> </Items> </ShoppingCart>

By default, you cannot use the serializeAs="String" option for custom types, since there is no way to convert the types to and from a string format in a lossless way. You can provide such a conversion yourself by implementing a TypeConverter for your class. This involves creating a class that inherits from TypeConverter, implementing the conversion methods, and then annotating your original class with the TypeConverter attribute that associates it with your conversion class. You must also decide how to persist your class as a string (and then parse it from a string) which can be a non-trivial task, so make sure it's worth the effort before taking this tack. As an example, Figure 5 shows a TypeConverter class for the Item class that represents items in the shopping cart. In this case I chose to use a non-printable character as a delimiter. Since the Item class consists of two pieces of state that are easily rendered as strings, the parsing becomes trivial using the Split method of the string class. The converter class is then associated with the Item class using the TypeConverter attribute.

Figure 5 Type Converter Class for Item

public class ItemTypeConverter : TypeConverter { private const char _delimiter = (char)10; public override object ConvertFrom( ITypeDescriptorContext context, CultureInfo culture, object value) { string sValue = value as string; if (sValue != null) { string[] vals = sValue.Split(_delimiter); return new Item(vals[0], float.Parse(vals[1])); } else return base.ConvertFrom(context, culture, value); } public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { if (destinationType == typeof(string)) { Item i = value as Item; return string.Format("{0}{1}{2}", i.Description, _delimiter, i.Cost); } else return base.ConvertTo(context, culture, value, destinationType); } public override bool CanConvertFrom( ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) return true; else return base.CanConvertFrom( context, sourceType); } public override bool CanConvertTo( ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) return true; else return base.CanConvertTo( context, destinationType); } } [Serializable] [TypeConverter(typeof(ItemTypeConverter))] public class Item { ...

With these classes in place, the Item class can be used with string serialization in a profile property. For the shopping cart to be completely serializable as a string, you also need to write a type converter for the ShoppingCart class, a sample of which can be found in the downloadable samples accompanying this column. The advantage of controlling the persistence in this way is that the serialization of the same shopping cart filled with four items now only takes 79 characters!

Going the Custom Route

When you find yourself spending a lot of time trying to make an architecture do what you want, it is important to make sure that all this work is less than doing it entirely yourself. Profile is a great example of a feature that is convenient and easy to use, but may be too constraining as your design evolves. Let's look at the features Profile specifically provides.

  • Anonymous and authenticated clients
  • Anonymous users identified through a new cookie (or alternatively through an embedded ID with URL mangling, including support for auto-detect cookieless mode)
  • Arbitrary type storage, strongly typed through the use of a configuration file
  • A per-client persistent data store
  • A management class for cleaning up unused profile data

One of the drawbacks to using Profile to store client data is that it stores all of the data in one column of the database table (or in two if you are using both string and binary serialization). This means that it is practically impossible to make modifications to the profile data without going through the Profile API. It's also impractical to generate any reports from the data or otherwise collect information from the database directly.

If you want more control over the storage of per-client state in your application, you have two choices: build a custom profile provider or forget Profile and just write data yourself. Building a custom profile provider gives you the ability to change the location that Profile actually writes the data to, but because of the nature of the provider interface, it doesn't really make it any easier to write property values to specific columns in a table. For more information and samples on building custom profile providers, take a look at the ASP.NET provider model toolkit. Also see the ASP.NET 2.0 Table Profile Provider sample.

If you decide to forgo Profile and write the serialization of client data yourself, be aware that you can still exploit the identification features of Profile even if you aren't using the storage features. Specifically, there is a UserName property on the ProfileBase class that will contain either the name of the current authenticated user or the GUID that was generated for an anonymous user. You can use this UserName property as a unique index into a custom database table to easily store and retrieve user data. Just make sure that Profile and anonymousIdentification are enabled in your application, and you can use the same client identification mechanism as Profile:

<anonymousIdentification enabled="true"/> <profile enabled="true" />

Writing your own client persistence back end using the unique identifier provided by Profile gives you several unique advantages:

  • The ability to write stored procedures against client data.
  • The ability to retrieve only the portions of data you need for a client at any given time instead of relying on Profile to just load the whole chunk into memory. Note, though, that each property's <add /> element in the configuration file includes a provider attribute, and with that attribute you can slice up your Profile properties across multiple providers. For example, you could map frequently used properties to one provider instance and infrequently used properties to a different provider instance. If you never access the infrequently used properties on a page at run time, the Profile feature will only load information associated with the "frequent use" provider. This is another example of how to take advantage of the lazy-load feature of Profile.
  • The ability to cache per-client data across requests for efficiency (you could add this by deriving from SqlProfileProvider and overriding GetPropertyValues and SetPropertyValues to add per-user caching semantics).
  • Complete control over the serialization and can map onto existing tables instead of creating new data stores that you may already have in place.

The sample available for download on the MSDN®Magazine Web site contains an alternate implementation of the shopping cart described earlier, using a custom database table to store cart items and exploiting the unique client identifier available through the ProfileBase class. You may even consider using Profile to get things started, and then later migrate some of the profile data into custom tables with a separate data access layer. In this sense, the use of Profile is an easy way to store per-client data, with an obvious path forward to factoring data out into a more strongly typed schema.

Conclusion

Profile fills a hole in state management for Web applications written with ASP.NET. Falling somewhere between in-process session state and a full-blown custom per-client data store, Profile should prove extremely useful in almost any Web application. Like any general purpose solution, however, you give up some performance and flexibility over a completely custom implementation, so be aware of its capabilities as you integrate it into your designs.

Send your questions and comments for Fritz to  xtrmasp@microsoft.com.

Fritz Onion is a cofounder of Pluralsight, a Microsoft .NET training provider, where he heads the Web development curriculum. Fritz is the author of Essential ASP.NET (Addison Wesley, 2003) and the upcoming Essential ASP.NET 2005 (Addison Wesley, 2006). Reach him at pluralsight.com/fritz.