For the .NET Framework, the asynchronous API pattern was first introduced in v1.0.
It involves the Begin/End methods found in many .NET components such as the System.IO
and System.Net APIs, delegates (BeginInvoke/EndInvoke), WebService proxies, and
in .NET 2.0, SqlClient.
When the BeginXXX method is used, the Result Method(params) becomes:
IAsyncResult BeginMethod(params, callback, state)Result EndMethod(IAsyncResult)
The .NET 2.0 Framework and above offer us a new Event-based Asynchronous programming
model that makes asynchronous programming more friendly to the average developer
(no IAsyncResult / AsyncCallback). It is Implemented by some .NET components
including Web Service proxies.
In .NET 2.0 and higher, our Result Method(params) now becomes:
void MethodAsync(params)event MethodCompletedMethodCompletedEventHandlerMethodCompleted
EventArgs has our Result property It should be mentioned that ASP.NET v1.x does support
asynchronous APIs via IHttpAsyncHandler. This allows asynchronous calls, but
it can’t use page framework features or asynchronous processing of HTTP pipeline
events.
It is possible to call a web service asynchronously either from GLOBAL.ASAX or an
HTTP Module.
The ASP.NET 2.0 Asynchronous Page model
The previously-mentioned asynchronous API model is available in .NET 2.0, and the
advantages are that more components implement this Framework pattern than the
newer event-based pattern. It offers the lower level API for developers who prefer
to implement the IAsyncResult / IAsyncCallback pattern.
The disadvantages are that the Framework pattern is the least developer-friendly.
It doesn’t flow through impersonation, culture, or the HttpContext to the "end
handler", and it is difficult to combine multiple IAsyncResult objects.
The ASP.NET 2.0 Event-based Asynchronous model looks like this:
On Page_Load - subscribe to the completion event and call the asynchronous method:
ws.MethodCompleted += new MethodCompletedEventHandler(target method);
ws.MethodAsync(params);
On the completion event handler - retrieve the result:
void MyMethodCompletedHandler(object source, MethodCompletedEventArgs e)
{ resultVar = e.Result;}
The advantages of using this model are that it is easy to launch several parallel
async operations. You also get automatic flow of impersonation, culture, and
HttpContext.Current to the completion event handler. So for example, if you needed
to make 4 different WebRequests to various resources at the same time in order to combine them after completion in the UI of the page for display, this
is a situation where the pattern could be useful.
Be aware however, that this is not some magic bullet for "behind the scenes"
multithreaded processing, because Page class lifecycle completion is halted pending the return of all async method calls. What this does do, however, is give
you more flexibility to do processing of work in parallel.
In GLOBAL.ASAX or HTTP modules, each application pipeline event has the async equivalent
to support the Framework async pattern. An event-based async operation can be
launched from any application event. The next event will be executed when all
async operations complete.
In ASP.NET pages, pages must be marked as async="true". It is possible
to launch async work using different async patterns even on the same page. The
async patterns can be used in ASP.NET controls, including custom controls and
DataSources, when the controls are on async pages.
With just one attribute, by decorating the @Page directive with Async='true', developers
can spawn one or more long-running operations, returning the main thread to ASP.NET
to be used for other requests. This defers the completion of page processing
until the asynchronous operations have completed and the page has the information
it needs to complete the request.
The options to do this include calling the AddOnPreRenderCompleteAsync or RegisterAsyncTask methods of your page instance, calling an XXXAsync method of a component that supports
the event-based asynchronous pattern, or any number of other asynchronous execution
techniques. In this sample page app, I will focus on the RegisterAsyncTask method,
which is probably the most developer-friendly and intuitive. We will make two
separate WebRequests for RSS feeds, executing both requests in parallel, loading
each into a DataSet. We will also provide a Timeout EndEventHandler to handle
and report timeout conditions.
Let's take a look at some page code first, and then I will walk through the process
with a short explanation, and some additional comments at the end:
using System;
using System.Data;
using System.Configuration;
using System.Collections;
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;
using System.Text;
using System.Net;
using System.IO;
public partial class samples_MultiWebRequest : System.Web.UI.Page
{
HttpWebRequest req1;
HttpWebRequest req2;
IAsyncResult result1;
IAsyncResult result2;
DataSet ds1 = new DataSet();
DataSet ds2 = new DataSet();
protected void Page_Load(object sender, EventArgs e)
{
// Register the two separate WebRequests as AsyncTasks,
// specifying that they should be run in parallel:
Page.RegisterAsyncTask( new PageAsyncTask(new BeginEventHandler(this.BeginAsyncWork1),
new EndEventHandler(this.EndAsyncWork1), new EndEventHandler(this.TimeoutHandler), true) );
Page.RegisterAsyncTask(new PageAsyncTask(new BeginEventHandler(this.BeginAsyncWork2),
new EndEventHandler(this.EndAsyncWork2), new EndEventHandler(this.TimeoutHandler), true));
}
protected override void OnPreRenderComplete(EventArgs e)
{
base.OnPreRenderComplete(e);
// write the result messages to the Label.
MessageOut();
}
IAsyncResult BeginAsyncWork1(Object sender, EventArgs e, AsyncCallback cb, object
state)
{
AddTraceMessage("BeginAsyncWork");
AddTraceMessage("BeginGetRSSOne");
this.req1 = (HttpWebRequest)WebRequest.Create("http://www.eggheadcafe.com/forumrss.aspx?topicid=2");
req1.Method = "GET";
req1.Proxy = System.Net.GlobalProxySelection.GetEmptyWebProxy();
result1= req1.BeginGetResponse(cb,req1);
return result1;
}
IAsyncResult BeginAsyncWork2(Object sender, EventArgs e, AsyncCallback cb, object
state)
{
AddTraceMessage("BeginGetRSSTwo");
this.req2 = (HttpWebRequest)WebRequest.Create("http://www.eggheadcafe.com/forumrss.aspx?topicid=4");
req2.Method = "GET";
req2.Proxy = System.Net.GlobalProxySelection.GetEmptyWebProxy();
result2 = req2.BeginGetResponse(cb, req2);
return result2;
}
void EndAsyncWork1(IAsyncResult asyncResult)
{
AddTraceMessage("EndAsyncWork1");
if (result1!=null)
{
AddTraceMessage("EndGetRssOne");
WebResponse response = (WebResponse)req1.EndGetResponse(asyncResult);
Stream streamResponse = response.GetResponseStream();
ds1 = new DataSet();
ds1.ReadXml(streamResponse);
streamResponse.Close();
}
}
void EndAsyncWork2(IAsyncResult asyncResult)
{
AddTraceMessage("EndAsyncWork2");
if (result2 != null)
{
AddTraceMessage("EndGetRssTwo");
WebResponse response2 = (WebResponse)req2.EndGetResponse(asyncResult);
Stream streamResponse2 = response2.GetResponseStream();
ds2 = new DataSet();
ds2.ReadXml(streamResponse2);
streamResponse2.Close();
}
}
void TimeoutHandler(IAsyncResult asyncResult)
{
AddTraceMessage("Request Timed Out");
Response.Write("<strong>async Request timed out</strong><br />");
}
private void MessageOut()
{
Page.Trace.Write(_trace.ToString());
if (ds1.Tables.Count > 0)
this.Label1.Text += "Got " + ds1.Tables[2].Rows.Count.ToString() + " Rows from response1 <br>";
if (ds2.Tables.Count > 0)
this.Label1.Text += "Got " + ds2.Tables[2].Rows.Count.ToString() + " Rows from response2 <br>";
}
StringBuilder _trace = new StringBuilder();
DateTime _pageStartTime = DateTime.Now;
public void AddTraceMessage(string message)
{
double t = (DateTime.Now - _pageStartTime).TotalSeconds;
lock (_trace)
{
_trace.AppendFormat("Thread:[{0:000}] {1:00.000} -- {2}\r\n",
System.Threading.Thread.CurrentThread.GetHashCode(), t, message);
}
}
}
In the beginning, at class level, I've defined my two WebRequests, my two IAsyncResult
objects, and two DataSets, one each to hold an RSS feed that is returned.
In my Page_Load handler, I register my two Tasks - each with the last parameter set
to "true" to ensure parallel operations. So in Page_Load, both task
are automatically kicked off, with further page processing halted until they
both return or one or more are timed out.
As each EndAyncWork callback is executed, I grab my WebResponse out of the AsyncResult
object, create a stream, and have the respective DataSet perform its ReadXml
method from the contents of the stream, loading the RSS Xml into a DataSet.
If a request Times out , which timeout is controlled via the <@Page AsyncTimeout="2"
directive at the top of the ASPX portion of my page, the Timeout callback is
fired by the AsyncTask framework, and I would get a timeout message for that
request. At this point I could, for example, combine the two resultsets into
a new DataTable and use it to display the combined RSS feeds in a GridView if I liked. You can see the async action in the Trace output, which I have
turned on in the demo page:
aspx.pageBegin PreRenderComplete0.5651477062048660.554446Thread:[005] 00.277 -- BeginAsyncWork
Thread:[005] 00.277 -- BeginGetRSSOne
Thread:[012] 00.531 -- EndAsyncWork1
Thread:[012] 00.531 -- EndGetRssOne
Thread:[012] 00.786 -- BeginGetRSSTwo
Thread:[013] 00.807 -- EndAsyncWork2
Thread:[013] 00.807 -- EndGetRssTwo
0.5662519940975590.001104 aspx.pageEnd PreRenderComplete 0.5669990382576360.000747
There are several points to be aware of about this process:
The AsyncTimeout @Page directive specifies the total time budget for the Page, not
a "per-task" or "per-async operation" timeout. Provided
the timeout doesn't expire, and all parallel tasks complete, Page execution then
continues and the page is rendered to the browser. The BeginXXX Event handler
is always invoked. It is important to understand that Asynchronous Page processing
kicks off one or more long-running operations (here, "Tasks"), and
then returns the thread to ASP.NET to be used for other requests. Completion
of page processing is deferred until the long-running operations have completed
(or the Page times out, if specified) and the page has the information it needs
to complete the request.
Regarding the BeginXXX event handler always being invoked, essentially what this
means is that if you have Async Tasks to kick off in your page, and the Page
Timeout has already been invoked, they are basically doomed. This means that
the intent of the timeout property is really to allow the developer to know that
the Page has already exceeded the timeout and to know not to kick off any further
async tasks. So, it's important not only to provide a Timeout handler, but also
to consider having it set a flag that's visible to the Async Task methods so
they can decide whether they should or should not go into the Black Hole of Asynchronous
Processing (BHAP, if you are an acronym freak).
Download the Visual Studio 2010 Web Application Solution that accompanies this article.