Extreme ASP.NET

Asynchronous Web Parts

Fritz Onion

Code download available at:Onion2006_07.exe(176 KB)

Contents

Web Part Congestion
Asynchronous Web Access
How It Works
Asynchronous Data Access

Building a customizable Web site complete with a collection of pluggable Web Parts is fairly easy with the portal infrastructure of ASP.NET 2.0. This model is very flexible, allowing users to easily place your Web Parts anywhere on the Web page so they are free to customize your site. However, these advantages can also lead to inefficiencies that may degrade the user experience, since you may not know beforehand which components will be used together, and therefore can’t make specific data retrieval optimizations for each individual component.

The most common inefficiency in a typical portal site occurs when multiple Web Parts simultaneously make network requests for data. Each request, whether to a Web service or a remote database, ends up adding to the overall time it takes to process the page even though the requests are typically independent of each other and could conceivably be issued in parallel.

Fortunately, ASP.NET 2.0 also introduces an easy-to-use asynchronous page model that, when used in combination with asynchronous Web service calls and asynchronous database access, can significantly improve the response time for a portal page as several independent Web Parts collect data in parallel. Here I’ll look at techniques for building Web Parts that perform their data retrieval asynchronously to make the portal pages that contain them more responsive and scalable.

Web Part Congestion

Let’s begin by considering the portal page shown in Figure 1. In this sample there are four Web Parts on a portal page, each retrieving data from a different source. The full source for this sample application is available for download on the MSDN®Magazine Web site and I encourage you to review the application as you read this column. In the sample, three of the Web Parts retrieve their data from a Web service, which intentionally waits for three seconds before returning. The fourth Web Part issues an ADO.NET query to a SQL Server database, which also waits three seconds before returning. This is an exaggerated example of the problem, but it’s not all that improbable.

Figure 1 Sample Portal Page

Figure 1** Sample Portal Page **

Each of the Web Parts in the sample application is built with a user control and binds the results of the data retrieval to the controls that display it. The code and markup for each control is kept to a minimum so that the example is simple and lets you focus on making the Web Parts asynchronous.

Here’s the NewsWebPart.ascx user control file:

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="NewsWebPart.ascx.cs" Inherits="webparts_ NewsWebPart" %> <asp:BulletedList ID="_newsHeadlines" runat="server"> </asp:BulletedList>

And here’s the corresponding codebehind file for the news headlines sample Web Part:

public partial class webparts_NewsWebPart : UserControl { protected void Page_Load(object sender, EventArgs e) { PortalServices ps = new PortalServices(); _newsHeadlines.DataSource = ps.GetNewsHeadlines(); _newsHeadlines.DataBind(); } }

Note how it interacts with a Web service to retrieve the sample news headlines. The stock quotes Web Part and the weather forecaster Web Part are implemented in much the same way and use different methods of the same Web service to retrieve their data. Similarly, Figure 2 shows the SalesReportWebPart.ascx user control file and the corresponding codebehind file for the sales report sample Web Part. Note how the control uses ADO.NET to retrieve the sales data from a database and then populates a GridView control with that data.

Figure 2 Sales Report Web Part

SalesReportWebPart.ascx

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="SalesReportWebPart.ascx.cs" Inherits="webparts_SalesReportWebPart" %> Sales data for <asp:TextBox ID="_yearTextBox" runat="server" Width="120px">2005</asp:TextBox> <asp:Button ID="_setYearButton" runat="server" Text="Set year" /><br /> <asp:GridView ID="_salesGrid" runat="server" AutoGenerateColumns="False" DataKeyNames="id"> <Columns> <asp:BoundField DataField="id" HeaderText="id" Visible="False" ReadOnly="True" /> <asp:BoundField DataField="quarter" HeaderText="Quarter" /> <asp:BoundField DataField="amount" HeaderText="Amount" DataFormatString="{0:c}" HtmlEncode="False" /> <asp:BoundField DataField="projected" HeaderText="Projected" DataFormatString="{0:c}" HtmlEncode="False" /> </Columns> </asp:GridView>

SalesReportWebPart.ascx.cs

public partial class webparts_SalesReportWebPart : UserControl { protected void Page_Load(object sender, EventArgs e) { string dsn = ConfigurationManager. ConnectionStrings["salesDsn"].ConnectionString; string sql = "WAITFOR DELAY ‘00:00:03’ " + "SELECT [id], [quarter], [year], [amount], "[projected] FROM [sales] WHERE year=@year"; using (SqlConnection conn = new SqlConnection(dsn)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue("@year", int.Parse(_yearTextBox.Text)); conn.Open(); _salesGrid.DataSource = cmd.ExecuteReader(); _salesGrid.DataBind(); } } } }

Figure 3 Sequential Web Part Processing

Figure 3** Sequential Web Part Processing **

As soon as the sample portal page is run, a problem becomes apparent. It takes more than 12 seconds for the request to process—a delay that will make most users avoid using the application. The reason for this lengthy delay is shown in Figure 3, which traces the path of execution a request takes when this page is executed. Like any other control in a page’s control hierarchy, each Web Part is loaded in turn, in the order defined by the control hierarchy of the page. Because this process is sequential, each Web Part must wait for the part preceding it in the hierarchy to complete before it can begin requesting its data and preparing its response. Because of the artificial 3-second delay introduced in each data retrieval, it is easy to see why it takes 12 seconds for the response to complete. Each Web Part is performing a completely independent data retrieval, one after the other. The important thing to realize is that all of these retrievals could be performed in parallel, cutting the response time by 75 percent. That is my goal here.

Asynchronous Web Access

In the example, three Web Parts use Web services to retrieve their data, and one uses ADO.NET to access a database. Let’s start by making the Web service invocations asynchronous, since there is some nice support in the Web service proxy classes generated by the Web Services Description Language tool WSDL.exe (or the Visual Studio 2005 Add Web Service Reference tool) for performing Web method invocation asynchronously.

When a Web service proxy class is created in ASP.NET 2.0, it actually generates three different ways of invoking any particular method, one synchronous and two asynchronous. For example, the Web service proxy that the Web Parts are using has the following methods available for invoking the GetNewsHeadlines Web method:

public string[] GetNewsHeadlines() public IAsyncResult BeginGetNewsHeadlines( AsyncCallback callback, object asyncState) public string[] EndGetNewsHeadlines( IAsyncResult asyncResult) public void GetNewsHeadlinesAsync() public void GetNewsHeadlinesAsync( object userState) public event GetNewsHeadlinesCompletedEventHandler GetNewsHeadlinesCompleted;

The first method, GetNewsHeadlines, is the standard synchronous method. The next two, BeginGetNewsHeadlines and EndGetNewsHeadlines, can be used to invoke the method asynchronously and can be tied into any number of asynchronous mechanisms in .NET through the standard IAsyncResult interface.

But the most interesting method to use in this scenario is the last one: GetNewsHeadlinesAsync. In order to use this particular method, I must register a delegate with the proxy class’s event that was specifically generated to capture the results of async invocations (the GetNewsHeadlinesCompleted event in the example). The delegate signature is strongly typed to contain the return values of the method so that I can easily extract the results in the method implementation.

Using this event-based asynchronous method, rewriting the Web method invocation in the headline news Web Part to be asynchronous is easy, as shown in Figure 4. I first subscribe a delegate to the GetNewsHeadlinesCompleted event of the proxy class, and then call the GetNewsHeadlinesAsync method. In the implementation of the method subscribed to the completed event, I bind the results of the Web method call to the BulletedList to display to the client. One additional consideration is that these asynchronous methods only work if the Web Part is placed on a page with the Async="true" attribute set, which can be checked programmatically by looking at the IsAsync property of the containing page. If the page the Web Part is placed on is not async, then I need to resort to standard synchronous binding, as shown in Figure 4.

Figure 4 Asynchronous Data Binding

public partial class webparts_NewsWebPart : UserControl { protected void Page_Load(object sender, EventArgs e) { // Instantiate Web service proxy for retrieving data PortalServices ps = new PortalServices(); // If page has async enabled, invoke Web service // asynchronously and harvest results in callback. if (Page.IsAsync) { ps.GetNewsHeadlinesCompleted += new GetNewsHeadlinesCompletedEventHandler( ps_GetNewsHeadlinesCompleted); ps.GetNewsHeadlinesAsync(); } else { // If not async, perform direct databinding _newsHeadlines.DataSource = ps.GetNewsHeadlines(); _newsHeadlines.DataBind(); } } // This callback is only invoked if the async web // service model was used. void ps_GetNewsHeadlinesCompleted(object sender, GetNewsHeadlinesCompletedEventArgs e) { _newsHeadlines.DataSource = e.Result; _newsHeadlines.DataBind(); } }

Now, for the asynchronous Web Part to perform its data retrieval asynchronously, it must be placed on a page with the Async attribute set to true, so I modify the Page directive of the portal page to look like the following:

<%@ Page Language="C#" AutoEventWireup="true" Async="true" %>

Once I update the other two Web Parts that use Web services to retrieve their data asynchronously, the portal page is much more responsive. In fact, depending on the order in which the parts are loaded, it may render to the client in just over three seconds (if the sales Web Part is loaded first, it will take just over six seconds)! Even though the sales report Web Part is still sequentially accessing the database, the other three Web Parts are now performing their Web service invocations asynchronously so the primary request thread is no longer waiting for their completion. Of course, I ultimately want to have all of the I/O-bound work be asynchronous so that clients could use both Web service and database-driven Web Parts without unnecessary sequential blocking.

The other reason to push the I/O-bound work onto asynchronous I/O requests is to relinquish the primary thread back to the thread pool to service other requests. Currently I’m relinquishing the thread only after my sales report database query has completed, which means I’m sitting idly for three full seconds occupying a threadpool thread that could be used to service other requests. If I can make this last I/O-bound request for data asynchronous as well, my page will use the request thread only long enough to spool off all the asynchronous I/O requests and then return immediately back to the pool.

How It Works

If you’ve ever done any asynchronous programming, you probably have the feeling that the minor changes made to the Web service invocation can’t possibly be sufficient. I didn’t even have to touch an IAsyncResult interface, nor did I have to let the containing page know that I was performing asynchronous operations (by registering a task or some other technique), and yet it all seemed to work as I had hoped.

The secret lies in the Web service proxy class’s implementation of the asynchronous method, along with a helper class introduced in the Microsoft® .NET Framework 2.0 called the AsyncOperationManager. When I called the GetNewsHeadlinesAsync method of the proxy class, it mapped the call onto an internal helper method of the SoapHttpClientProtocol base class, called InvokeAsync, from which the proxy class derives. InvokeAsync does two important things—it registers the asynchronous operation by calling the AsyncOperationManager’s static CreateOperation method, and it then launches the request asynchronously using the WebRequest class’s BeginGetRequestStream method. At this point the call returns and the page goes on processing its lifecycle, but because the page has been marked with the Async="true" attribute, it will only continue processing the request up through the PreRender event and will then return the request thread to the thread pool. Once the asynchronous Web request completes, it will invoke the method I subscribed to the completed event of the proxy on a separate thread drawn from the I/O thread pool. If this is the last of the asynchronous operations to complete (kept track of by the synchronization context of the AsyncOperationManager), the page will be called back and the request will complete its processing from where it left off, starting at the PreRenderComplete event. Figure 5 shows this entire lifecycle when you use asynchronous Web requests in the context of an asynchronous page.

Figure 5 Asynchronous Web Requests in Asynchronous Pages

Figure 5** Asynchronous Web Requests in Asynchronous Pages **

The AsyncOperationManager is a class that is designed to be used in different environments to help in the management of asynchronous method invocations. For example, if I called a Web service asynchronously from within a Windows® Forms application, it would also tie into the AsyncOperationManager class. The difference between each environment is the SynchronizationContext associated with the AsyncOperationManager. When running in the context of an ASP.NET-based application, the SynchronizationContext will be set to an instance of the AspNetSynchronizationContext class. The primary purpose here is to keep track of how many outstanding asynchronous requests are pending so that when they are all complete, the page request processing can resume. In contrast, when in a Windows Forms-based application, the SynchronizationContext will be set to an instance of the WindowsFormsSynchronizationContext class. Its primary purpose is to allow for easier marshaling of invocations from a background thread to the UI thread.

Asynchronous Data Access

Now, back to the problem of making the last Web Part asynchronous and the general issue of performing asynchronous data retrieval with ADO.NET. Unfortunately, there is no equivalent to the simple asynchronous mechanism exposed by Web service proxies for performing asynchronous data retrieval, so I’m going to have to do a little more work to get the final Web Part to participate in the asynchronous shuffle. I can work with the new asynchronous methods on the SqlCommand class and the asynchronous task feature of ASP.NET. Using SqlCommand, I can now invoke commands asynchronously using one of the following methods:

  • IAsyncResult BeginExecuteReader(AsyncCallback ac, object state)
  • IAsyncResult BeginExecuteNonQuery(AsyncCallback ac, object state)
  • IAsyncResult BeginExecuteXmlReader(AsyncCallback ac, object state)

And I can invoke the corresponding completion methods once the data stream is ready to begin reading:

  • SqlDataReader EndExecuteReader(IAsyncResult ar)
  • int EndExecuteNonQuery(IAsyncResult ar)
  • XmlReader EndExecuteXmlReader(IAsyncResult ar)

To use any of these asynchronous retrieval methods, "async=true" must be added to the connection string. For this scenario, I am interested in populating a GridView by binding it to a SqlDataReader, so I will use the BeginExecuteReader method to initiate the asynchronous call.

To tie this into the asynchronous page, ASP.NET 2.0 also allows you to register asynchronous tasks that need to be executed before the page completes rendering. This is a more explicit model than the one I used with the Web service proxies, but it also provides more flexibility. To register an asynchronous task, I create an instance of the PageAsyncTask class and initialize it with three delegates: begin handler, end handler, and timeout handler. The begin handler must return an IAsyncResult interface, so this is where I will launch my asynchronous data request using BeginExecuteReader. The end handler is called once the task is complete (when there is data ready to read in this example), at which point I can use the results. ASP.NET will take care of invoking the begin handler just before it relinquishes the request thread (immediately after the PreRender event completes). Figure 6 shows the updated implementation of the sales report Web Part performing asynchronous data access using the asynchronous tasks and the asynchronous BeginExecuteReader method of the SqlCommand class.

Figure 6 Asynchonous Data Access

public partial class webparts_SalesReportWebPart : UserControl { // Local variables to store connection and command // for async data retrieval SqlConnection _conn; SqlCommand _cmd; protected void Page_Load(object sender, EventArgs e) { string dsn = ConfigurationManager. ConnectionStrings["salesDsn"].ConnectionString; dsn += ";async=true"; // Allow for asynchronous operations string sql = "WAITFOR DELAY ‘00:00:03’ SELECT [id], " + "[quarter], [year], [amount], [projected] FROM " + "[sales] WHERE year=@year"; _conn = new SqlConnection(dsn); _cmd = new SqlCommand(sql, _conn); _conn.Open(); _cmd.Parameters.AddWithValue("@year", int.Parse(_yearTextBox.Text)); // Launch data request asynchronously using page async task Page.RegisterAsyncTask(new PageAsyncTask( new BeginEventHandler(BeginGetSalesData), new EndEventHandler(EndGetSalesData), new EndEventHandler(GetSalesDataTimeout), null, true)); } IAsyncResult BeginGetSalesData(object src, EventArgs e, AsyncCallback cb, object state) { return _cmd.BeginExecuteReader(cb, state); } void EndGetSalesData(IAsyncResult ar) { _salesGrid.DataSource = _cmd.EndExecuteReader(ar); _salesGrid.DataBind(); _conn.Close(); } void GetSalesDataTimeout(IAsyncResult ar) { // operation timed out, so just clean up by closing the connection if (_conn.State == ConnectionState.Open) _conn.Close(); _messageLabel.Text = "Query timed out..."; } }

Note that I could use this same technique with my Web service requests by using the alternate asynchronous methods provided on the proxy class (BeginGetNewsHeadlines, for example). One potential advantage to this technique is that I can also specify a timeout handler. If the remote invocations fail to return in time, the associated timeout handler will be invoked. This timeout is specified in the Page directive using the AsyncTimeout attribute and defaults to 20 seconds. Also note that unlike when using the event-based asynchronous pattern, when using Page.RegisterAsyncTask I don’t have to branch to a synchronous invocation based on the result of Page.IsAsync. Asynchronous page tasks in Web Parts work just fine on synchronous pages, and even allow for parallel execution of Web Parts. The core difference is that on synchronous pages (ones without the Async="true" attribute), the main page thread won’t be released back to the thread pool during the execution of the asynchronous operations.

With all of the Web Parts now performing their data retrieval asynchronously, I can now use these parts in any page marked as asynchronous and know that the response time will no longer be the sum of the time it takes for all the Web Parts to retrieve their data, but the maximum amount of time taken by any one Web Part. By marking the page as asynchronous and using Web Parts that perform asynchronous I/O, I also increase the potential scalability of the site since the page will free up the primary request thread to service other clients while waiting for the data. The key takeaway here is that if you’re building portal sites with ASP.NET 2.0, you should keep in mind all of the new asynchronous features introduced in this release, and take advantage of them to improve both the responsiveness and scalability of your applications. For more information on asynchronous support in ASP.NET 2.0, see Jeff Prosise’s Wicked Code column in the October 2005 issue of MSDN Magazine.

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 2.0 (Addison Wesley, 2006). Reach him at pluralsight.com/fritz.