Cutting Edge

Personalization in ASP.NET 1.1

Dino Esposito

Code download available at:CuttingEdge0403.exe(141 KB)

Contents

Designing a Personalization Layer
The Personalization HTTP Module
The Data Storage Medium
Reading Personalization Data
Writing Personalization Data
Using Personalization in an App
Summing It Up

Personalizable applications let users set their UI preferences and store those settings. Desktop applications typically store the user preferences in a local file (an XML or a configuration file) or in the registry. Web applications, on the other hand, generally use cookies to store user-specific data. Cookies are essentially a text-based key-value collection sent and received via HTTP. There are a few disadvantages to using cookies—chiefly their size limit (8KB), the user's ability to refuse cookies, and the inherently insecure nature of plain text (though you can encrypt or encode the cookie's value). Keeping personalized data on the server is a better option, but has its own issues.

This month I'll discuss a few ASP.NET 1.1 extensions that provide the current runtime with personalization capabilities. The API that I'm going to describe is inspired by the ASP.NET 2.0 personalization API, but it isn't a copy. The personalization API in the upcoming ASP.NET 2.0, code-named "Whidbey," is not overly complex, but it's too complex to be covered in depth here. A good introduction to personalization in ASP.NET Whidbey can be found at https://www.asp.net/whidbey/pdc.aspx.

Designing a Personalization Layer

The layer of code I'm going to build is designed to work with an application that employs an authentication mechanism to authorize its users. In ASP.NET 1.1, you can choose from among various authentication modes, including Passport and custom forms authentication (Jeff Prosise provides a great introduction to ASP.NET forms authentication in the May 2002 issue of MSDN® Magazine at ASP.NET Security: An Introductory Guide to Building and Deploying More Secure Sites with ASP.NET and IIS, Part 2.) If the personalization layer detects an anonymous user, it does nothing. ASP.NET 2.0 will provide a feature to handle situations in which both authorized and anonymous users can be admitted to a site.

The architecture of the personalization layer consists mainly of an HTTP module that the application must install and configure. The module wires up a couple of application-level events—AuthorizeRequest and EndRequest—to load and save personal data. Figure 1 illustrates the architecture of the API.

Figure 1 My Personalization API

Figure 1** My Personalization API **

Processing an ASP.NET request involves passing through several steps, each of which is characterized by an application event. All events fired during the processing of a request are associated with the HttpApplication object and can't be handled by page-level code. Page-level code is just part of the request cycle; consequently, no code in the page can wire up application events. In order to handle application events at the system level, you have to write an HTTP module.

The AuthorizeRequest event is fired when the request has been authenticated and the identity behind it authorized. At this time, the module uses the current user name as an index into the data store—a Microsoft® Access or SQL Server™ database—and retrieves any related personal information. This information must be made available to the page code in some way. In ASP.NET 1.1, this can be accomplished with the Items collection of the HttpContext object. In ASP.NET 2.0, a new property, named Profile, has been added to the HttpContext class. The Items collection on the HttpContext class is available to you to populate with user-defined data. Programming the Items collection is nearly identical to programming the Session state or the Cache object:

Context.Items["BackColor"] = "gainsboro"; string color = (string) Context.Items["BackColor"];

An instance of the HttpContext class travels with the request from start to finish and can be used to share information between HTTP modules and the page. In light of this, anything that the personalization module writes to the context is accessible to the page and vice versa. The page can retrieve personal data from the context, process it, and if needed, record changes. When the EndRequest event fires, the personalization module collects data from the Items collection and persists it to the medium.

My API supports both an Access and a SQL Server database to persist user-specific data, but you can use any data store. ASP.NET 2.0 goes beyond this, making the data provider generic and hiding it behind a provider interface. You can write (or use) any component and make it work with any store, as long as it exposes the methods of a particular interface. In the PDC build, ASP.NET 2.0 supplies built-in providers for Access (the default) and SQL Server. It is important to note that if you use a file-based medium (such as Access), you must ensure that the ASP.NET worker process has the proper permissions to write to the Web server file system and specifically to the data file. You can grant these permissions to a file or folder using the cacls.exe tool, which is a standard component of the Windows® operating system. You use the following command line, replacing [user] with the actual user account name:

cacls.exe [path] /E /G [user]:F

For ASP.NET applications, the user account must match the account of the process that physically operates on the database—typically ASPNET or NetworkService if you use Windows Server™ 2003. The same attribution of permissions can also be accomplished by selecting the Security tab in the Properties dialog box of the Access database file.

The Personalization HTTP Module

HTTP modules are classes that implement the IHttpModule interface to handle runtime events. In addition to the system events raised by the HttpApplication object, a module can also handle events raised by other HTTP modules. For example, the session state module, one of the built-in ASP.NET modules, supplies session state services to an application. It fires End and Start events that other modules can handle through the familiar Session_End and Session_Start signatures. HTTP modules have functionality similar to ISAPI filters—Win32® DLLs that extend IIS—but with a much simpler, object-oriented programming model. HTTP modules filter the raw data within a request and are configured on a per-application basis within the web.config file. All ASP.NET applications inherit a number of system HTTP modules configured in the machine.config file.

The IHttpModule interface defines only two methods—Init and Dispose. Init initializes a module and registers it to handle any application events of interest. The Dispose method disposes of the resources used by the module. Tasks you would typically perform within this method include releasing database connections and file handles. The IHttpModule interface methods have the following signatures:

void Init(HttpApplication app); void Dispose();

The Init method receives a reference to the HttpApplication object that is serving the request. You use this reference to wire your app to receive system events. The HttpApplication object also features a property named Context that provides access to the intrinsic properties of the ASP.NET application. In this way, you gain access to the Response, Request, and Session objects. Some details of the module's implementation are shown in Figure 2.

Figure 2 Personalization HTTP Module

namespace MsdnMag { public class PersonalizationModule : IHttpModule { public void Init(HttpApplication app) { app.AuthorizeRequest += new EventHandler(OnEnter); app.EndRequest += new EventHandler(OnLeave); } public void Dispose() { } private void OnEnter(object sender, EventArgs e) { HttpContext ctx = ((HttpApplication) sender).Context; LoadPersonalData(ctx); } private void OnLeave(object sender, EventArgs e) { HttpContext ctx = ((HttpApplication) sender).Context; SavePersonalData(ctx); } private void LoadPersonalData(HttpContext ctx) { if (!ctx.Request.IsAuthenticated) return; // Grab the name of the current authorized user string currentUser = ctx.User.Identity.Name; // Retrieve personalization info for the user // and store them in the Context.Items collection RetrievePersonalInfo(ctx, currentUser); } private void SavePersonalData(HttpContext ctx) { // Grab the name of the current authorized user string currentUser = ctx.User.Identity.Name; // Store personalization info for the user StorePersonalInfo(ctx, currentUser); } // More code here... ••• } }

The OnLeave method handles the AuthorizeRequest event. It grabs a reference to the current HTTP context and checks to see if the request is authenticated. If the request is anonymous, then the handler returns immediately. Otherwise, the method retrieves the name of the current user via the Identity property on the ASP.NET intrinsic User object:

string currentUser = ctx.User.Identity.Name;

The user name is used as the key to retrieve the particular record in the personalization database that contains the preferences of that user. When the request is about to terminate, the database record for that user will be updated. As mentioned, the module copies all the data retrieved after the request is authorized into the Items collection of the request context. From the same location, the module takes the data to be persisted back to the storage medium when the request has been served and the operation is close to completion. Let's move on to examine the characteristics of the data storage layer and how programmers can declaratively define the structure of the personalization data.

The Data Storage Medium

In ASP.NET 2.0, the programmer creates a new section in the web.config file in order to define the profile of the user. The user profile is a class that inherits from the HttpPersonalizationBase class. The new <personalization> section contains entries that define the name and type of custom properties to add to the class. The ASP.NET 2.0 personalization module parses the app's web.config file, figures out the final structure of the personalization class, and emits its source code. Finally, this source code is dynamically compiled to an assembly and an instance of the personalization class is attached to the Profile property, which makes its debut in the Whidbey HttpContext class. For example, this configuration script adds a BackColor property of type Color:

<personalization> <profile> <property name="BackColor" type="System.Drawing.Color" /> </profile> </personalization>

The user profile class of an application that contains the previous script in its web.config file looks like the following pseudocode:

public class HttpPersonalization : HttpPersonalizationBase { public Color BackColor; }

The overall mechanism is nearly identical to the dynamic compilation of ASP.NET pages. If the personalization assembly does not already exist, it is created when the first request is made to the application. Using a dynamically compiled assembly offers a double advantage—it is faster and provides strongly typed access to the personalization data.

The personalization mechanism discussed in this column is simpler and provides only weakly typed access to data. In other words, the personalization module reads desired personalization attributes from the application's web.config file, but doesn't create a class from them. Once retrieved, each value is copied into the HttpContext.Items collection which is an instance of a Hashtable class. As such, it is an untyped collection of objects. This provides the greatest flexibility but incurs a cost in runtime overhead.

To extend ASP.NET 1.1 with personalization, you have to perform two steps: retrieve custom configuration information from the web.config file and move it to and from the storage medium. In Whidbey, the <personalization> section is built into the new configuration schema; in ASP.NET 1.1, you must add it through a custom section handler. Here's a sample of the schema that I have designed for the custom section:

<personalization> <provider> <datastore>...</datastore> </provider> <property> <name>BackColor</name> <default>...</default> </property> <property> <name>Links</name> <default>...</default> </property> </personalization>

Under the <personalization> node, you first find a <provider> node with one <datastore> child. The contents of the <datastore> node indicates the type of storage medium you plan to use. It recognizes only two values: access and sqlserver. Note that the source code doesn't implement a strong check on the data store name. It checks for the "sqlserver" string; otherwise it defaults to using an Access database.

Next, you find a sequence of <property> nodes, each with at least a <name> child node and possibly more. If you haven't figured it out yet, this schema matches the standard XML format of a DataSet that contains two tables—Provider and Property. The Provider table has one record with one column: datastore. The Property table has records made of two columns—Name and Default. Because <personalization> is a custom section in ASP.NET 1.1, you have to provide a custom handler for it that plugs into the built-in configuration reading engine. A custom section handler is simply a class that implements the IConfigurationSectionHandler interface. The source code of the custom handler that I'm using here is shown in Figure 3.

Figure 3 Custom Section Handler

using System; using System.Data; using System.Xml; using System.Configuration; using System.Web.Configuration; namespace MsdnMag { public class PersonalizationHandler : IConfigurationSectionHandler { // ************************************************************** // Constructor(s) public PersonalizationHandler() { } // ************************************************************** // ************************************************************** // IConfigurationSectionHandler.Create public object Create(object parent, object context, XmlNode section) { // Create a temporary DataSet DataSet ds = new DataSet(); // Read this section into the DataSet // (Assuming the section represents the XML normal // form of a DataSet—what you'd get from WriteXml) XmlNodeReader nodereader = new XmlNodeReader(section); ds.ReadXml(nodereader); // By design, you have just one table return ds } // ************************************************************** } }

You register a custom section handler for a custom section using the following script:

<configuration> <configSections> <section name="personalization" type="MsdnMag.MyHandler, MsdnWebKit" /> </configSections> ••• <configuration>

The name attribute in the <section> section must match the name of the root node of the actual section in the web.config file. The ASP.NET framework will use the specified class (in the example, it is the MsdnMag.MyHandler class from the MsdnWebKit assembly) to process the XML subtree and pass the resulting object to the calling app. In this case, the resulting object would be a DataSet object (see Figure 3). Each record contained in the Property table references a personalization property to be stored in the Context.Items collection. Names and existing values are retrieved from a particular SQL Server or Access database as the Provider table dictates. How is personalization data actually read and written? Let's tackle reading first.

Reading Personalization Data

When the request is authorized, the code in Figure 4 runs to retrieve any information packed in the database for the current user. To minimize the I/O overhead, all the information is grouped into a single collection object that gets serialized and deserialized as needed. The helper database contains three fields: UserID (the key field), PropertyValue, and LastUpdatedData. UserID contains the name of the authenticated user. PropertyValue stores the collection of properties and values, and LastUpdatedData contains the time at which the last update occurred. This same pattern—collection data serialized to a unique stream of data—is used by ASP.NET 1.1 to store session state in out-of-process applications and by ASP.NET 2.0 to store personalization data.

Figure 4 Get All Information

// *************************************************************** // RetrievePersonalInfo: helper method that retrieves data private void RetrievePersonalInfo(HttpContext ctx, string currentUser) { // Get the property list from web.config DataSet ds = (DataSet) ConfigurationSettings.GetConfig("personalization"); DataTable provider = ds.Tables["provider"]; bool useSqlServer = false; if (provider.Rows[0]["datastore"].ToString() == "sqlserver") useSqlServer = true; // Get the name/value collection from the Access db Hashtable values = GetDataValues(currentUser, useSqlServer); if (values == null) return; // Fill the context using just read values and property // names in the web.config file DataTable data = ds.Tables["property"]; foreach (DataRow row in data.Rows) { string prop = row["Name"].ToString(); if (values[prop] != null) ctx.Items[prop] = values[prop]; else ctx.Items[prop] = null; } } // *************************************************************** // *************************************************************** // GetDataValues: helper method that reads data from the store private Hashtable GetDataValues(string currentUser, bool useSqlServer) { IDbConnection conn; if (useSqlServer) { string connString = SqlConnectionStringBase; conn = new SqlConnection(connString); } else { string db = String.Format(AccessConnectionStringBase, HttpContext.Current.Server.MapPath(AccessPersonalization_DB)); conn = new OleDbConnection(db); } string cmdText = String.Format(CmdQuery, currentUser); IDbCommand cmd = conn.CreateCommand(); cmd.CommandText = cmdText; cmd.Connection.Open(); string data = (string) cmd.ExecuteScalar(); if (data == null) { // Add a new record (only user name) cmd.CommandText = String.Format(CmdInsert, currentUser); cmd.ExecuteNonQuery(); cmd.Connection.Close(); return null; } cmd.Connection.Close(); // Base64 decoding and binary deserialization byte[] bits = Convert.FromBase64String(data); MemoryStream mem = new MemoryStream(bits); BinaryFormatter bin = new BinaryFormatter(); Hashtable values = (Hashtable) bin.Deserialize(mem); mem.Close(); // Read cookie return values; } // ***************************************************************

Ideally, you would use binary serialization, and possibly simple types, to minimize the amount of data serialized and ultimately enhance the performance of the operation. However, an Access database can't accept a binary field, so I encoded the array of bytes in Base64 before storing it in the database. Note, though, that this choice is totally arbitrary. If you use SQL Server, this problem doesn't exist and you can effectively store the bytes to a BLOB field. However, in my implementation I don't distinguish between Access and SQL Server and always Base64-encode the bytes generated by the binary serialization process.

Custom schema for serializing the personalization data to a text format is a good plan if you use Access (which is not recommended for Internet enterprise applications). ASP.NET 2.0 uses a new type of formatter object that generates its output to XML. So, what about using the XmlSerializer class in ASP.NET 1.1? You could do that, but remember that the XML serializer is a Web service tool and wasn't designed with object serialization in mind. As a result, it doesn't support dictionaries or the DataTable class, both of which are rather useful here. The SoapFormatter class is your only built-in option in ASP.NET 1.1 for serializing a .NET-based object to text. Unfortunately, it produces really verbose and clear text. That's why I've chosen the Base64-encoding and why ASP.NET 2.0 is needed to define a new text-based formatter.

The schema that I've chosen for the helper database is similar to the one used by ASP.NET 2.0. ASP.NET 2.0 splits names and values in two distinct columns—PropertyName and PropertyValue—and references users through a foreign key. The foreign table is named ASP_Users and lists all of the available users. This makes a lot of sense in ASP.NET 2.0 because the helper database is designed to contain information about personalization, membership, roles, and site counters. In this much simpler case, it would only represent a complication.

In Figure 4, the database query returns a Base64 string. This string is decoded to an array of bytes and deserialized using the binary formatter. The final result is a Hashtable object. This object is expected to contain one entry for each property declared in the <personalization> section. Notice that a new record is inserted in the database if no record is found for the specified user name.

Finally, the code in Figure 4 retrieves the list of declared personalization properties from the web.config file and adds a corresponding entry to the HttpContext.Items collection. The value of this entry is the value as read from the database.

Writing Personalization Data

Personalization data is saved back to the storage medium each and every time a request ends. Only one database operation is executed regardless of the number of properties defined in the personalization data. However, if the user hasn't changed anything in the HTTP context, there's no reason to save personalization data back to the storage medium. However, there's no immediate way to check whether the personalization attributes are changed. Without this valuable information, you can maintain two copies of the personalization data—original and current—in order to make a comparison and skip an unnecessary update when possible. But if you're dealing with a large amount of personalized data, this is not a good option.

A better choice is to add an extra Boolean value to the Items collection and call it something like "Modified." Then, whenever the application changes one of the personalization attributes, it can also set the Modified attribute to true and you only have to check the Boolean value in the EndRequest handler to prevent an unnecessary update. If you try this, you'll realize the importance of having a class wrapping all the attributes of the user profile. Such a class, in fact, could easily hide support for the Modified flag in the set accessors of all properties. If you plan to add personalization to your ASP.NET 1.1 application, I recommend that you make the effort to wrap all the properties in a class. I'll return to this idea later in this column.

When the EndRequest event is fired, the personalization module does the reverse of what it does when the request is authorized (this is evident in Figure 1 as well). The event handler first gets itself a new dictionary object and then populates it with as many entries as there are attributes in the web.config file. When the dictionary object is ready, it is serialized using the binary formatter and the resulting byte array is encoded to a Base64 string. Finally, the string is stored to the database using an UPDATE command. By design, the record exists because the module has created it upon authorization. Figure 5 shows the source code necessary for the personalization module to persist the changes to the data store.

Figure 5 Persist Changes to Data Store

// *************************************************************** // StorePersonalInfo: helper method that stores data private void StorePersonalInfo(HttpContext ctx, string currentUser) { // Instantiate a new dictionary Hashtable values = new Hashtable(); // Get the property list from web.config DataSet ds = (DataSet) ConfigurationSettings.GetConfig("personalization"); DataTable data = ds.Tables["property"]; foreach (DataRow row in data.Rows) { string prop = row["Name"].ToString(); if (ctx.Items[prop] != null) values[prop] = ctx.Items[prop]; } DataTable provider = ds.Tables["provider"]; bool useSqlServer = false; if (provider.Rows[0]["datastore"].ToString() == "sqlserver") useSqlServer = true; // Store user data to the Access db SetDataValues(values, currentUser, useSqlServer); } // *************************************************************** // SetDataValues: helper method that writes data out to the store private void SetDataValues(Hashtable values, string currentUser, bool useSqlServer) { if (currentUser == "") return; // Binary serialization and Base64 encoding BinaryFormatter bin = new BinaryFormatter(); MemoryStream mem = new MemoryStream(); bin.Serialize(mem, values); IDbConnection conn; IDbDataParameter parm1, parm2; string commandText = ""; if (useSqlServer) { string connString = SqlConnectionStringBase; conn = new SqlConnection(connString); parm1 = new SqlParameter("@PropertyValue", SqlDbType.NVarChar); parm2 = new SqlParameter("@LastUpdatedData", SqlDbType.DateTime); commandText = String.Format(CmdUpdateSql, currentUser); } else { string db = String.Format(AccessConnectionStringBase, HttpContext.Current.Server.MapPath(AccessPersonalization_DB)); conn = new OleDbConnection(db); parm1 = new OleDbParameter("PropertyValue", OleDbType.LongVarChar); parm2 = new OleDbParameter("LastUpdatedData", OleDbType.Date); commandText = String.Format(CmdUpdateAccess, currentUser); } IDbCommand cmd = conn.CreateCommand(); cmd.CommandText = String.Format(commandText, currentUser); parm1.Value = Convert.ToBase64String( mem.GetBuffer(), 0, (int) mem.Length); parm2.Value = DateTime.Now; cmd.Parameters.Add(parm1); cmd.Parameters.Add(parm2); cmd.Connection.Open(); cmd.ExecuteNonQuery(); cmd.Connection.Close(); mem.Close(); return; }

Using Personalization in an App

So far I have only discussed the underpinnings of the personalization module and the data storage layer. In this way, each ASP.NET page within a personalizable application finds values corresponding to attributes declared in the configuration in the HttpContext.Items collection. By default, these values are null, although you can support default values, too.

A personalizable application has two main characteristics. First, it supports authentication and authorization. Second, it includes some user interface elements to let users change the values of the personalization attributes.

In ASP.NET 1.1, enabling authentication and authorization is primarily a matter of tweaking the web.config file. There you declare the type of authentication you want and the users you intend to authorize. For most real-world scenarios, the most reasonable choice is forms authentication, and authorization is performed by comparing the credentials of the user with those stored in a database. The following code snippet illustrates how to modify the web.config for forms authentication:

<system.web> <authentication mode="Forms"> <forms loginUrl="login.aspx" /> </authentication> <authorization> <deny users="?" /> </authorization> ••• <system.web>

The Mode attribute of the <authentication> section selects the type of authentication desired out of a few predefined options. Forms authentication consists of a user-defined page (login.aspx in the example) that gathers the user's credentials and checks them against a database or any other persistent data store that you intend to use (such as Active Directory® or Passport). In practice, forms authentication gives you a built-in infrastructure for securing application access, but also makes you responsible for the actual implementation. Typically, the login form contains a button to log in. When clicked, the button will run code that looks something like the following:

string user = userName.Text; string pswd = passWord.Text; if (AuthenticateUser(user, pswd) FormsAuthentication.RedirectFromLoginPage(user, false);

AuthenticateUser is a custom function that verifies credentials against your app's repository of authorized users. The <deny> node in the configuration script serves to deny access to anonymous users. An application configured in this way might present a login box like the one in Figure 6. Once the user types her name and password and these credentials are successfully checked, the User object on the Page class is properly set, and the HttpContext.Items collection is populated with any user-specific personalization data stored in the database. For example, suppose that all the pages in the application let users choose the background color and the hyperlinks to display in the top bar. Figure 7 shows initialization code that could be used for such a page.

Figure 7 Initialization

private void Page_Load(object sender, System.EventArgs e) { ApplyPersonalization(); } private void ApplyPersonalization() { // The app has a BackColor property of type Color if(Context.Items["BackColor"] != null) { TheBody.Attributes["bgcolor"] = Context.Items["BackColor"].ToString(); } // The app has a Links property of type StringCollection StringCollection coll = (StringCollection) Context.Items["Links"]; if (coll != null) { foreach(object o in coll) { HyperLink h = new HyperLink(); h.Font.Name = "verdana"; h.Font.Size = FontUnit.Point(8); h.Text = o.ToString(); h.NavigateUrl = o.ToString(); Favorites.Controls.Add(h); Favorites.Controls.Add(new LiteralControl ("   ")); } } LinkButton link = new LinkButton(); link.Font.Name = "verdana"; link.Font.Size = FontUnit.Point(8); link.Text = "Log out"; link.Click += new EventHandler(OnLogOut); StdLinks.Controls.Add(link); } private void OnLogOut(object sender, System.EventArgs e) { FormsAuthentication.SignOut(); Server.Transfer("login.aspx?ReturnUrl=webform1.aspx"); }

Figure 6 LogIn Box for AuthenticateUser

Figure 6** LogIn Box for AuthenticateUser **

To change the page background programmatically, the application can add a runat attribute to the <body> tag and give it an ID. Thus, the body becomes a programmable element and exposes the programming interface of the HtmlGenericControl class. The interface is simple and provides an Attributes string collection. As the following code shows, though, this is enough to change the background color of a page:

TheBody.Attributes["bgcolor"] = Context.Items["BackColor"].ToString();

The Links property in the personalization data is a string collection and its elements are outlined in a strip of dynamically created hyperlinks. The hyperlinks are then bound to a placeholder control. For more details you can download the source code of this column at the link at the top of this article. In short, any type of .NET-based object can find its way into the personalization stream and the application can take advantage of its features accordingly. Figure 8 shows that the same application run by different users looks different. For a type to be used in personalizable attributes, the only requirement is that it's a serializable type.

Figure 8 Changing Personalizable Attributes

Figure 8** Changing Personalizable Attributes **

The sample application in Figure 8 also features a Personalize link button to let users explicitly change the value of personalizable attributes. The application is entirely responsible for building and managing the UI for personalization. The sample application does that using a Web user control that collects new values and stores them into the context Items collection (see Figure 9).

Figure 9 Collecting Attributes

Figure 9** Collecting Attributes **

Summing It Up

Personalization makes an application more flexible and enjoyable to use. In ASP.NET 1.1, the implementation of this layer of code is entirely up to the developer. In ASP.NET 2.0, you'll find an excellent ready-made infrastructure that reads declarations in the web.config file and creates a class. The class is then dynamically compiled to an assembly, thus providing for strongly typed access and better performance.

How would you code this yourself in ASP.NET 1.1? Create a base class and include as many features as you want to see supported. Next, extend the personalization module to read additional properties and their types, and create the source code of a C# or Visual Basic® .NET class using the CodeDOM managed API. Once you're finished, the same CodeDOM API allows you to compile the class to an assembly.

Sounds simple? Well, not completely. The toughest part is determining how to detect changes to the user's data model and invalidate the previously created assembly. The only approach I can think of is to monitor the web.config file for changes. If you have any ideas, send me your feedback and I might address them in a future column.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is an instructor and consultant based in Rome, Italy. Author of Programming ASP.NET (Microsoft Press, 2003), he spends most of his time teaching classes on ADO.NET and ASP.NET and speaking at conferences. Get in touch at cutting@microsoft.com or join the blog at https://weblogs.asp.net/despos.