Making asynchronous calls to web services during ASP.NET page processing
Some ASP.NET web applications use web services to get some data that they display to users. Some pages may lay hardly on web services and these pages need some optimization to work better. In this posting I will show you how to use web services behind your ASP.NET page asynchronously and perform a lot of queries to web services. Sample code included!
Eneta.Examples.WebAppThreading.zip VS2008 solution | 60KB |
Asynchronous page processing
As an introduction let’s see how ASP.NET supports asynchronous calls behind pages. Although we can use our own threading mechanism and avoid what is offered by ASP.NET it is more safe and time saving to use ASP.NET own infrastructure.
The image on right that I stole from MSDN article Asynchronous Pages in ASP.NET 2.0 shows you how synchronous and asynchronous pages are processed by ASP.NET. I recommend to read this MSDN article because you can find also other methods for asynchronous calls. I will introduce you how to use PageAsyncTask based calls.
If you look at diagram on right you can see that asynchronous processing of registered tasks happens between PreRender and PreRenderComplete events in separate threads.
If we have data bound controls on our page we have to move data binding to PreRenderComplete event because this is the closest phase of page processing where asynchronous calls are finished. For PreRenderComplete event all asynchronous tasks are done.
Getting started
Now let’s go through simple page that performs asynchronous call to web service. We are using simple web service with one method that is able to wait for given amount of seconds before sending out response. Right now we don’t need waiting functionality. I will show you later how to make more than one call and then we will need delay parameter. Our web service method is here.
[WebMethod]
public string HelloWorld(int delay)
{
if(delay > 0)
Thread.Sleep(delay);
return "Hello World, delay: " + delay;
}
Our hello world method returns just hello world sting and time delay given to method.
NB! Before going to code you have to make one modification to your page. Open it in mark-up code view and add Async="true" to Page directive. Without this parameter you get the exception!
Now let’s add some code behind our asynchronous page. Some explanations too. We will define page level variable called _service that keeps the instance of web service client class. When page initializes we will create new PageAsyncyTask and provide it with references to BeginRequest and EndRequest operations. First one of them initializes new asynchronous call to web service method and the second one is called when asynchronous call is finished.
private readonly DelayedHelloSoapClient _service =
new DelayedHelloSoapClient();
private Random _random = new Random();
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
var task = new PageAsyncTask(BeginRequest, EndRequest,
null, null);
RegisterAsyncTask(task);
}
IAsyncResult BeginRequest(Object sender, EventArgs e,
AsyncCallback cb, object state)
{
var param = _random.Next(1, 5) * 1000;
return _service.BeginHelloWorld(param, cb, null);
}
void EndRequest(IAsyncResult asyncResult)
{
var answer = _service.EndHelloWorld(asyncResult);
Debug.WriteLine(answer);
}
Delay parameter for hello world method is generated by using page wide randomizer. Again, we don’t need this randomizer to be at page scope right now but we will need it in next section. If you need to call only one web service during page processing then you can use this code and make refactorings you need.
Calling multiple web service during page processing
Now let’s take more complex scenario and let’s suppose we need more than one web service call to be made. Lazy as I am I am using same web service for all calls. If you have different services then each one of them requires separate task with separate BeginRequest and EndRequest methods. Now let’s use this code behind our page.
private readonly Random _random = new Random();
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
for (var i = 0; i < 100; i++)
{
var task = new PageAsyncTask(BeginRequest, EndRequest,
null, null, true);
RegisterAsyncTask(task);
}
}
IAsyncResult BeginRequest(Object sender, EventArgs e,
AsyncCallback cb, object state)
{
var service = new DelayedHelloSoapClient();
var delay = _random.Next(1, 5)*1000;
var hash = service.GetHashCode();
Debug.WriteLine("Started " + hash + ", delay: " + delay);
return service.BeginHelloWorld(delay, cb, service);
}
void EndRequest(IAsyncResult asyncResult)
{
var service = (DelayedHelloSoapClient)asyncResult.AsyncState;
var hash = service.GetHashCode();
var answer = service.EndHelloWorld(asyncResult);
Debug.WriteLine("Finished " + hash + ", " + answer);
}
In OnInit we registered 100 calls to web services. Yes, 100 calls… I know it is not normal but we need a lot of instances to be test the results better. BeginRequest method is now tricky. We create new instance of service each time and provide it to BeginHelloWorld() as state object. When EndResult is fired we read the service for given call out from state parameter and call EndHelloWorld() method to get data back.
Run application and see how fast it runs. On my pretty old and heavily loaded laptop this code takes about 30 seconds to run. When I remove delay then all these 100 calls are made during 1.1 seconds.
Storing and displaying results
Now let’s see how we can store and display results returned by web services. Here is the full source of my page class. Notice that I addded Stopwatch to measure the time that web services take to run. I also added DataTable called _answers to page scope where web service instances write answers they got.
public partial class _Default : Page
{
private readonly DataTable _answers = new DataTable();
private readonly Stopwatch _watch = new Stopwatch();
private readonly Random _random = new Random();
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (IsPostBack)
return;
for (var i = 0; i < 100; i++)
{
// last parameter (true) means that we want
// parallel execution of task
var task = new PageAsyncTask(BeginRequest, EndRequest,
null, null, true);
RegisterAsyncTask(task);
}
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (IsPostBack)
return;
_answers.Columns.Add("InstanceId", typeof (int));
_answers.Columns.Add("Answer", typeof (string));
_watch.Start();
}
IAsyncResult BeginRequest(Object sender, EventArgs e,
AsyncCallback cb, object state)
{
var service = new DelayedHelloSoapClient();
var delay = _random.Next(1, 5) * 1000;
var hash = service.GetHashCode();
Debug.WriteLine("Started " + hash + ", delay: " + delay);
return service.BeginHelloWorld(delay, cb, service);
}
void EndRequest(IAsyncResult asyncResult)
{
var service =
(DelayedHelloSoapClient)asyncResult.AsyncState;
var hash = service.GetHashCode();
var answer = service.EndHelloWorld(asyncResult);
Debug.WriteLine("Finished " + hash + ", " + answer);
lock(_answers)
{
var row = _answers.NewRow();
row["InstanceId"] = hash;
row["Answer"] = answer;
_answers.Rows.Add(row);
}
}
protected override void OnPreRenderComplete(EventArgs e)
{
base.OnPreRenderComplete(e);
if (IsPostBack)
return;
_watch.Stop();
Debug.WriteLine("Time: " + _watch.Elapsed);
answersRepeater.DataSource = _answers;
answersRepeater.DataBind();
}
}
Take a careful look at EndRequest method. Because we are using threads we have to lock _answers table so it is not used by multiple threads at same time. I locked _answers for as short time as possible. Still I got 1 second additional time due to this lock. Also notice that we bind data to Repeater in OnPreRenderComplete method. If you do it in PreRender method then threads are not run yet and _answers table is not filled with results.
Conclusion
Using threads behind ASP.NET pages is not very complex task if you use built-in mechanism that takes care of running threaded operations. We used PageAsyncTask to register new asynchronous tasks and we gave service instance as state object to asynchronous calls so we were able to get correct service channel later when we finished asynchronous call and read data that web service method returned. We also saw that using ASP.NET built-in support for asynchronous pages it was very easy to store the results and display them in data bound controls. All we had to do was to move our data related functionality to PreRenderComplete event of page.