Wicked Code

Five Undiscovered Features on ASP.NET 2.0

Jeff Prosise

Code download available at:WickedCode0502.exe(121 KB)

Contents

Updating Browser Displays in (Almost) Real Time
Encrypted Configuration Sections
Auto-Culture Handling
Custom Expression Builders
Custom Web Events

By now, developers everywhere have had the opportunity to download the first beta of the Microsoft® .NET Framework 2.0. ASP.NET developers who have played with it are no doubt salivating at all the cool new features. From Master Pages to declarative data access to new controls to a new provider-based state management architecture, ASP.NET 2.0 offers myriad ways to do more with less code. And with Beta 2 just around the corner, now is the time to get serious about ASP.NET 2.0.

You may have read some of the many books and magazine articles previewing the upcoming features. You might even have seen a live demo at a conference or user group meeting. But how well do you really know ASP.NET 2.0? Did you know, for example, that those wonderful $ expressions used to declaratively load connection strings and other resources can be extended to create $ expressions of your own? Did you realize that the new ASP.NET 2.0 client callback manager provides an elegant solution to the problem of keeping browser displays in sync with constantly changing data on the server? Did you know that you can encrypt sections of Web.config to prevent connection strings and other potentially injurious data from being stored in plaintext?

Just underneath the surface of ASP.NET 2.0 lies a treasure trove of new features and capabilities that have received little coverage. This installment of Wicked Code presents five of them. All the code samples were tested against Beta 1; some may require modification for Beta 2. And as usual, remember things can change as these are beta versions.

Updating Browser Displays in (Almost) Real Time

In "An Overview of the New Services, Controls, and Features in ASP.NET 2.0" in the June 2004 issue of MSDN®Magazine, I wrote about the ASP.NET 2.0 new client callback manager and demonstrated how it can be used to transmit XML-HTTP callbacks to Web servers to convert ZIP codes into city names. Dino Esposito delved more deeply into XML-HTTP callbacks in his August 2004 Cutting Edge column ("Script Callbacks in ASP.NET").

XML-HTTP callbacks enable browsers to make calls to Web servers without performing full-blown postbacks. The benefits are numerous. XML-HTTP callbacks transmit less data over the wire, thereby using bandwidth more efficiently. XML-HTTP callbacks don't cause the page to flicker because they don't cause the browser to discard the page as postbacks do. Furthermore, XML-HTTP callbacks execute less code on the server because ASP.NET short-circuits the request so that it executes the minimum amount of code necessary. Inside an XML-HTTP callback, for example, a page's Render method isn't called, significantly reducing the time required to process the request on the server.

Once they learn about the ASP.NET 2.0 client callback manager, most developers can envision lots of different uses for it. But here's an application for XML-HTTP callbacks that you might not have thought of. I regularly receive e-mail from developers asking how to create two-way connections between browser clients and Web servers. The scenario generally involves an ASP.NET Web page displaying data that's continually updated on the server. The goal is to create a coupling between the browser and the Web server so that when the data changes on the server, it's automatically updated on the client, too.

It sounds reasonable enough, but transmitting asynchronous notifications from a Web server to a browser is a nontrivial problem. However, XML-HTTP callbacks provide a handy solution. Rather than try to contrive a mechanism for letting a Web server send notifications to a browser, you can have the browser poll the server in the background using efficient XML-HTTP callbacks. Unlike META REFRESH tags, an XML-HTTP solution causes no flashing in the browser, producing a superior user experience. And unlike more elaborate methods that rely on maintaining open ports, an XML-HTTP-based solution, properly implemented, has minimal impact on scalability.

The Web page shown in Figure 1 demonstrates how dynamic page updates using XML-HTTP callbacks work. To see for yourself, launch the page called DynamicUpdate.aspx in your browser. Then open the file named Stocks.xml in the Data directory. The fictitious stock prices displayed in DynamicUpdate.aspx come from Stocks.xml. Now change one of the stock prices in Stocks.xml and save your changes. After a brief pause (two or three seconds on average), the browser's display updates to reflect the change.

Figure 1 Dynamic Page Updates

Figure 1** Dynamic Page Updates **

What's the magic that allowed the update to occur? XML-HTTP callbacks, of course. Figure 2 lists the source code for the codebehind class that serves DynamicUpdate.aspx. Page_Load uses the new ASP.NET 2.0 Page.GetCallbackEventReference method to obtain the name of a function it can call to initiate a callback. Then it registers its own __onCallbackCompleted function, which uses client-side script to update the client's display, to be called when a callback returns. Finally, it registers a block of startup script that uses window.setInterval to call the function that initiates callbacks—the function whose name was returned by GetCallbackEventReference. Once the page loads, it polls the server every five seconds for updated data. Any changes made to the data on the server appear in the browser after a short delay.

Figure 2 DynamicUpdate.aspx.cs

using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class DynamicUpdate_aspx { static readonly string _script1 = "<script language=\"javascript\">\n" + "function __onCallbackCompleted (result, context)\n" + "{{\n" + "var args = result.split (';');\n" + "var gridView = document.getElementById('{0}');\n" + "gridView.rows[1].cells[1].childNodes[0].nodeValue = args[0];\n" + "gridView.rows[2].cells[1].childNodes[0].nodeValue = args[1];\n" + "gridView.rows[3].cells[1].childNodes[0].nodeValue = args[2];\n" + "}}\n" + "</script>"; static readonly string _script2 = "<script language=\"javascript\">\n" + "window.setInterval (\"{0}\", 5000);\n" + "</script>"; void Page_Load(object sender, EventArgs e) { // Get a callback event reference string cbref = GetCallbackEventReference (this, "null", "__onCallbackCompleted", "null", "null"); // Register a block of client-side script containing // __onCallbackCompleted RegisterClientScriptBlock ("MyScript", String.Format( _script1, GridView1.ClientID)); // Register a block of client-side script that launches // XML-HTTP callbacks at five-second intervals RegisterStartupScript ("StartupScript", String.Format( _script2, cbref)); } // Server-side callback event handler string ICallbackEventHandler.RaiseCallbackEvent(string arg) { // Read the XML file into a DataSet DataSet ds = new DataSet (); ds.ReadXml(Server.MapPath ("~/Data/Stocks.xml")); // Extract the stock prices from the DataSet string amzn = ds.Tables[0].Rows[0]["Price"].ToString (); string intc = ds.Tables[0].Rows[1]["Price"].ToString(); string msft = ds.Tables[0].Rows[2]["Price"].ToString(); // Return a string containing all three stock prices // (for example, "10.0;20.0;30.0") return (amzn + ";" + intc + ";" + msft); } }

Currently, DynamicUpdate.aspx.cs doesn't use bandwidth as efficiently as it could because every callback returns all three stock prices, even if the data hasn't changed. You could make DynamicUpdate.aspx.cs more efficient by modifying it to return only the data that has changed and to return nothing at all if no prices have changed. Then you'd have the best of both worlds: a scalable, lightweight mechanism for detecting updates on the server, and one that transmits only as much information as it must and not a single byte more. That's a win no matter how you look at it.

Encrypted Configuration Sections

ASP.NET 1.x texts frequently advise developers to put database connection strings in the <appSettings> section of Web.config. Doing so makes retrieving connection strings easy, and it centralizes the data so that changing a connection string in one place propagates the change throughout the application. Unfortunately, ASP.NET 1.x has no oblique support for encrypting connection strings (or any other data, for that matter) in Web.config. That leaves programmers with a Faustian choice: store connection strings in plaintext where they're vulnerable to hackers, or store them in encrypted form and write lots of code to decrypt them after you've already fetched them.

One of the tenets of writing secure ASP.NET code is to avoid storing as plaintext any secrets, passwords, connection strings, or other data that could be misused if divulged. To prevent such risky behavior, ASP.NET 2.0 lets you encrypt individual sections of Web.config. Encryption is transparent to the application. You don't have to do anything special to read an encrypted string from Web.config; you just read it as normal and if it's encrypted, it's automatically decrypted by ASP.NET. Not a single line of custom code is required. ASP.NET also offers you a choice of two encryption modes. One uses triple-DES encryption with a randomly generated key protected by RSA; the other encryption mode uses triple-DES encryption as implemented by the Windows® Data Protection API (DPAPI). You can add support for other encryption techniques by plugging in new data protection providers. Once encrypted, data stored in Web.config remains theoretically secure even if the Web server is compromised and the entire Web.config file falls into the wrong hands. Without the decryption key, the data can't be decoded.

Sometime before ASP.NET 2.0 ships, the ASP.NET page of the IIS Microsoft Management Console (MMC) snap-in will probably be upgraded with a GUI for encrypting and decrypting sections of Web.config. Beta 1 lacks such a tool. In fact, most ASP.NET developers I've talked to aren't even aware that Web.config supports encrypted configuration sections.

The good news is that you don't have to wait for tool support to take advantage of encrypted configuration sections. The new ASP.NET 2.0 configuration API has methods you can use to build a tool of your own. One call to ConfigurationSection.ProtectSection is sufficient to encrypt a configuration section; a subsequent call to ConfigurationSection.UnProtectSection decrypts it. Following a successful call to either method, you call Configuration.Update to write changes to disk. (Configuration.Update will probably be renamed to Configuration.Save in Beta 2.) Additionally, the aspnet_regiis.exe tool in Beta 1 provides some command-line support (see the -p* options).

The page in Figure 3 offers a simple UI for encrypting and decrypting the <connectionStrings> section of Web.config. If you'd like, you can modify it to support encryption of other configuration sections, too. To encrypt <connectionStrings>, simply click the Encrypt button. To decrypt, click Decrypt (tricky, huh?).

Figure 3 Encrypting and Decrypting <connectionStrings>

ProtectSection.aspx

<%@ Page Language="C#" CompileWith="ProtectSection.aspx.cs" ClassName="ProtectSection_aspx" %> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <div> <asp:Button ID="Button1" Runat="server" Text="Encrypt Connection Strings" Width="214px" Height="73px" OnClick="Button1_Click" /> <asp:Button ID="Button2" Runat="server" Text="Decrypt Connection Strings" Width="214px" Height="73px" OnClick="Button2_Click" /> </div> </form> </body> </html>

ProtectSection.aspx.cs

using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class ProtectSection_aspx { void Button1_Click(object sender, EventArgs e) { Configuration config = Configuration.GetWebConfiguration( Request.ApplicationPath); ConfigurationSection section = config.Sections["connectionStrings"]; section.ProtectSection ("DataProtectionConfigurationProvider"); config.Update (); } void Button2_Click(object sender, EventArgs e) { Configuration config = Configuration.GetWebConfiguration( Request.ApplicationPath); ConfigurationSection section = config.Sections["connectionStrings"]; section.UnProtectSection (); config.Update(); } }

Figure 4 shows the <connectionStrings> section of the accompanying Web.config file before and after encryption (note that the encrypted string is really more than a thousand characters long and has been excerpted here). You should also notice the <protectedData> section added to Web.config containing information needed to decrypt the connection strings. Significantly, <protectedData> doesn't contain the decryption key. When the Windows DPAPI is used to perform the encryption as it is here, the decryption key is autogenerated and locked away in the Windows Local Security Authority (LSA).

Figure 4 <connectionStrings> Before and After Encryption

connectionStrings Before Encryption

<connectionStrings> <add name="Pubs" connectionString=" Server=localhost;Integrated Security=True;Database=Pubs" providerName="System.Data.SqlClient" /> <add name="Northwind" connectionString="Server=localhost;Integrated Security=True;Database=Northwind" providerName="System.Data.SqlClient" /> </connectionStrings>

connectionStrings After Encryption

<connectionStrings> <EncryptedData> <CipherData> <CipherValue>AQAAANCMnd8BfdERjHoAw ...</CipherValue> </CipherData> </EncryptedData> </connectionStrings> <protectedData> <protectedDataSections> <add name="connectionStrings" provider="DataProtectionConfigurationProvider" /> </protectedDataSections> </protectedData>

ASP.NET 2.0 permits all but a handful of configuration sections to be encrypted. The <httpRuntime> section, for example, doesn't support encryption because it's accessed by the tiny fraction of ASP.NET that's built from unmanaged code. But everything that matters can be encrypted, and by using encrypted configuration sections judiciously, you can erect an additional barrier for hackers attempting to steal secrets from your site.

Auto-Culture Handling

Developers charged with the task of localizing Web sites in ASP.NET 1.x often found themselves writing code into Global.asax to inspect Accept-Language headers and attach CultureInfo objects representing language preferences to the threads that handle individual requests. The code they wrote to do that frequently took the form shown in Figure 5.

Figure 5 Attach Language Preference to Incoming Requests

void Application_BeginRequest (Object sender, EventArgs e) { try { if (Request.UserLanguages.Length > 0) { CultureInfo ci = CultureInfo.CreateSpecificCulture( Request.UserLanguages[0]); Thread.CurrentThread.CurrentCulture = ci; Thread.CurrentThread.CurrentUICulture = ci; } } catch (ArgumentException) { // Do nothing if CreateSpecificCulture fails } }

A new feature of ASP.NET called auto-culture handling obviates the need for such code. Auto-culture handling is enabled for individual pages by including Culture="auto" and UICulture="auto" attributes in the pages's @ Page directives. It's enabled site-wide by including a <globalization culture="auto" uiCulture="auto" /> element in Web.config. However you choose to enable it, auto-culture handling has an interesting effect: it maps Accept-Language headers to CultureInfo objects and attaches them to the current thread, just like the code in Figure 5.

To demonstrate, check out the page in Figure 6. Its output consists of a Calendar control and a text string showing today's date. The latter is generated by a call to DateTime.ToShortDateString. If a user who has configured her browser to transmit Accept-Language headers specifying French as her preferred language visits the page, she sees the page depicted in Figure 7. If auto-culture handling were not enabled, the page would appear no different to French users than it would to other users. The key is the @ Page directive turning auto-culture handling on. ASP.NET does the hard part; you do the rest.

Figure 6 Auto-Culture Handling

AutoCulture.aspx

<%@ Page Culture="auto" UICulture="auto" Language="C#" CompileWith="AutoCulture.aspx.cs" ClassName="AutoCulture_aspx" %> <html> <body> <form runat="server"> <asp:Calendar ID="Calendar1" Runat="server" ...> ... </asp:Calendar> <h2><asp:Label ID="Label1" Runat="server" /></h2> </form> </body> </html>

AutoCulture.aspx.cs

using System; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; public partial class AutoCulture_aspx { void Page_Load (object sender, EventArgs e) { Label1.Text = "Today's date is " + DateTime.Now.ToShortDateString(); } }

Figure 7 Auto-Culture Handling Enabled

Figure 7** Auto-Culture Handling Enabled **

Obviously, there's more to localizing an entire Web site than simply enabling auto-culture handling. Auto-culture handling does not, for example, localize static Web site content. But ASP.NET 2.0 offers other new localization features as well, including the new <localize> tag for localizing static content and $ Resources expressions for loading localization resources declaratively. Put simply, ASP.NET 2.0 makes it dramatically easier than ASP.NET 1.x to provide localized content to international users.

Custom Expression Builders

ASP.NET 2.0 developers are encouraged to store database connection strings in the <connectionStrings> section of the registry. Connection strings stored that way can be loaded declaratively, as demonstrated by the following tag declaring a SqlDataSource:

<asp:SqlDataSource ... Runat="server" ConnectionString="<%$ ConnectionStrings:Northwind %>" />

"<%$...%>" is a new expression type in ASP.NET. It can also be used to load resources with statements like this one:

<asp:Literal Runat="server" Text="<%$ Resources:MyResources, MyText %>" />

And it can be used to load strings from the <appSettings> configuration section, as shown here:

<asp:Literal Runat="server" Text="<%$ AppSettings:MyText %>" />

What is less widely known is that $ expressions are extensible. That is, you can add support for $ expressions of your own by writing custom expression builder classes. System.Web.Compilation.ExpressionBuilder in the Framework provides the basic plumbing needed and can be derived from in order to create custom expression builders.

Here is the source code for a simple page named Version.aspx:

<%@ Page Language="C#" CompileWith="Version.aspx.cs" ClassName="Version_aspx" %> <html> <body> <h1>Powered by ASP.NET <asp:Literal Text='<%$ Version:MajorMinor %>' Runat="server" /> </h1> </body> </html>

The page's output shows the version of ASP.NET that's running. The version number is generated by this expression:

<%$ Version:MajorMinor %>

On its own, ASP.NET has no idea what to do with this expression. It works because the application's Code directory contains the source code for a custom expression builder named VersionExpressionBuilder (shown in Figure 8). VersionExpressionBuilder derives from System.Web.Compilation.ExpressionBuilder and overrides one virtual method, GetCodeExpression, which is called at run time by ASP.NET to evaluate the $ Version expression.

Figure 8 VersionExpressionBuilder.cs

using System; using System.Web.UI; using System.Web.Compilation; using System.CodeDom; public class VersionExpressionBuilder : ExpressionBuilder { public override CodeExpression GetCodeExpression( BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context) { string param = entry.Expression; if (String.Compare(param, "All", true) == 0) return new CodePrimitiveExpression (String.Format("{0}.{1}.{2}.{3}", Environment.Version.Major, Environment.Version.Minor, Environment.Version.Build, Environment.Version.Revision)); else if (String.Compare(param, "MajorMinor", true) == 0) return new CodePrimitiveExpression (String.Format("{0}.{1}", Environment.Version.Major, Environment.Version.Minor)); else throw new InvalidOperationException ("Use $ Version:All or $ Version:MajorMinor"); } }

The Expression property of the BoundPropertyEntry parameter passed to GetCodeExpression contains the text to the right of the colon in the expression: in this particular case, "MajorMinor." GetCodeExpression responds by a CodePrimitiveExpression encapsulating the string "2.0". If you write the $ Version expression this way instead

<%$ Version:All %>

then the string returned contains build and revision numbers as well as major and minor version numbers.

Custom expression builders must be registered and mapped to expression prefixes so that ASP.NET knows what class to instantiate when it encounters a $ expression. Registration is accomplished by adding an <expressionBuilders> section to Web.config's <compilation> section, as shown here:

<!-- From Web.config --> <compilation> <expressionBuilders> <add expressionPrefix="Version" type="VersionExpressionBuilder"/> </expressionBuilders> </compilation>

Now that you know about custom expression builders, you can probably envision other uses for them. Imagine, for example, $ XPath expressions that extract data from XML files, or $ Password expressions that retrieve passwords or other secrets from ACLed registry keys. The possibilities are endless.

Custom Web Events

One of the major new services featured in ASP.NET 2.0 is the one provided by the health monitoring subsystem. With a few simple statements in Web.config, an ASP.NET 2.0 application can be configured to log failed logins, unhandled exceptions, expired forms authentication tickets, and more.

Logging is accomplished by mapping the "Web events" fired by the health monitoring subsystem to Web event providers. Each provider corresponds to a specific logging medium. For example, the built-in EventLogProvider logs Web events in the Windows event log, while SqlWebEventProvider logs them in a SQL Server™ database. Other providers supplied with ASP.NET 2.0 permit Web events to be transmitted in e-mail messages, redirected to the WMI subsystem, and even forwarded to registered trace listeners.

Out of the box, the health monitoring subsystem presents a world of possibilities for monitoring the health and well-being of running ASP.NET applications and for leaving paper trails for use in failure diagnostics. But what's really great about health monitoring is that it is entirely extensible. You can define custom Web events and fire them at appropriate junctures in an application's lifetime. Imagine that your application fired a Web event every time database contention resulted in a concurrency error. You could map these events to a provider and check the log at the end of each day to detect and correct excessive concurrency errors. Or what if a financial app fired a Web event every time it performed a monetary transaction? You could keep a running log of all such transactions simply by adding a few statements to Web.config.

The page shown in Figure 9 fires a custom Web event each time you click the "Fire Custom Web Event" button. The custom Web event is simple: it notifies any providers that are connected to it that the button was clicked.

Figure 9 Firing Custom Web Events

WebEvent.aspx

<%@ Page Language="C#" CompileWith="WebEvent.aspx.cs" ClassName="WebEvent_aspx" %> <html> <body> <form runat="server"> <asp:Button ID="Button1" Runat="server" Text="Fire Custom Web Event" Width="219px" Height="104px" OnClick="Button1_Click" /> </form> </body> </html>

WebEvent.aspx.cs

using System; using System.Web; using System.Web.Management; public partial class WebEvent_aspx { void Button1_Click (object sender, EventArgs e) { MyWebEvent mwe = new MyWebEvent ("Click!", null, 100001, DateTime.Now); WebBaseEvent.Raise (mwe); } }

Figure 10 shows how the event appears in the Windows Event Viewer if the event, which is named simply "MyWebEvent," is mapped to the Windows event log.

Figure 10 Windows Event Viewer

Figure 10** Windows Event Viewer **

How do custom Web events work? You begin by defining a custom Web event class by deriving from System.Web.Management.WebBaseEvent, as shown in Figure 11.

Figure 11 MyWebEvent.cs

using System; using System.Web.Management; public class MyWebEvent : WebBaseEvent { DateTime _time; public MyWebEvent (string message, object source, int eventCode, DateTime time) : base (message, source, eventCode) { _time = time; } public override void FormatCustomEventDetails( WebEventFormatter formatter) { formatter.AppendLine ("Button clicked at " +_time.ToString()); } }

Derived classes typically override FormatCustomEventDetails, which gives them the opportunity to append output of their own to the output generated by the base class. The base class's output contains key statistics about the event, such as its name and the time and date it was fired. MyWebEvent adds a line of its own—"Button clicked at [date and time]"—that appears at the end of the log entry. After you've defined a custom Web event in this manner, you fire it by instantiating it and passing it to the static WebBaseEvent.Raise method, as seen in Figure 11.

You must register custom Web events before firing them. The following code shows the registration entries in Web.config for MyWebEvent:

<!-- From Web.config --> <healthMonitoring enabled="true"> <eventMappings> <add name="My Web Events" type="MyWebEvent, __code" /> </eventMappings> <rules> <add name="My Web Events" eventName="My Web Events" provider="EventLogProvider" /> </rules> </healthMonitoring>

The enabled="true" attribute in the <healthMonitoring> tag enables the health monitoring subsystem. The <eventMappings> section defines an event named "My Web Event" and maps it to the MyWebEvent class. Finally, the <rules> section maps MyWebEvent to EventLogProvider, directing instances of MyWebEvent to the Windows event log. If you wanted to log MyWebEvents in other storage media, you could do so simply by changing the provider specified in <rules>.

One nuance to be aware of regarding custom Web events is that in Beta 1, you need to add the source code file containing the custom Web event class to the application's Code directory and run the application once before registering the event and mapping it to a provider in Web.config. Otherwise, ASP.NET complains that the Web event class is undefined, apparently because ASP.NET parses Web.config before its autocompilation engine gets a chance to compile the files in the Code directory. I presume this will be fixed in Beta 2, but it's only a minor annoyance if you stage deployment so that autocompilation happens first.

ASP.NET 2.0 is loaded with new features designed to make building cutting-edge Web apps easier and less time consuming. But beyond the features you read about, a host of "lesser" features makes ASP.NET 2.0 more powerful and more extensible than its predecessors. Exploiting these hidden gems is one of the keys to writing great ASP.NET 2.0 code.

Send your questions and comments for Jeff to  wicked@microsoft.com.

Jeff Prosise is a contributing editor to MSDN Magazine and the author of several books, including Programming Microsoft .NET (2002, Microsoft Press). He's also a cofounder of Wintellect, a software consulting and education firm that specializes in Microsoft .NET.