Asynchronous Web Service Calls over HTTP with the .NET Framework

 

Matt Powell
Microsoft Corporation

September 9, 2002

Summary: Matt Powell walks through the various options provided by the Microsoft .NET Framework for making asynchronous Web service calls over HTTP, which allow for efficient calls to Web services without blocking applications while potentially lengthy network calls complete. (11 printed pages)

When writing applications that consume Web services, you inevitably must deal with the latency involved when making calls across a network. In certain situations, particularly on a private network with plenty of bandwidth, calls can complete in half a second or less and the wait isn't particularly significant. However, if you are sending requests to a remote location across the Internet, or are making calls that require a lot of processing time, then you need to start thinking about how lengthy delays can affect your application. Microsoft® Windows® Forms applications, for instance, may appear frozen while they wait for a call to a Web service to return. If you are calling a Web service from a Microsoft® ASP.NET page, you may find that multiple Web service calls make your page display many times slower. If you have an application that is making many Web service calls, then it's important to think about how to call them as efficiently as possible.

The solution to a lot of these problems is to make your Web service calls asynchronously. Asynchronous calls return immediately and then use some other mechanism for indicating when the call actually completes. This will allow your application to do other things, like any background processing that may be required, responding to user interactions with the UI, providing feedback about the current state of the request, or even initiating other Web service calls. We will be taking a look at how the Microsoft® .NET Framework provides support for making asynchronous Web service calls over HTTP, and how we might use them in a couple of common scenarios.

The Building Blocks

When you choose the Add Web Reference option in Microsoft Visual Studio® .NET, a class is created for you that inherits from System.Web.Services.Protocols.SoapHttpClientProtocol. SoapHttpClientProtocol has a protected function called Invoke that is actually used when you make the call to one of the methods exposed by a Web service. For each Web method defined in the WSDL of the Web service, the wizard creates a function with the appropriate name parameters and return values. Each of these functions then call the Invoke function on the SoapHttpClientProtocol class passing in the parameter information and so forth. For this article, I created a Web service with a method that purposefully could take a long time to return. The Web method is called DelayedResponse and takes an integer as its only parameter and returns a string. The Add Web Reference option generates the following proxy code for this method:

Public Function DelayedResponse(ByVal waitPeriod As Integer) _
        As String
    Dim results() As Object _
        = Me.Invoke("DelayedResponse", _
                    New Object() {waitPeriod})
    Return CType(results(0),String)
End Function
 

The Invoke method takes two parameters: the name of the function and an object array that holds the parameters to be passed to the function. The Invoke method returns an object array, which in our case just contains one element—the string returned from our function. This is the mechanism for making synchronous calls that wait until the response is received before returning.

The SoapHttpClientProtocol class also has a method called BeginInvoke, which is the mechanism used to start an asynchronous request. The class created by Add Web Reference also creates a public function called BeginDelayedResponse to go with the blocking DelayedResponse function we looked at before. The code for BeginDelayedResponse is shown below.

Public Function BeginDelayedResponse( _
        ByVal waitPeriod As Integer, _
        ByVal callback As System.AsyncCallback, _
        ByVal asyncState As Object) As System.IAsyncResult
   Return Me.BeginInvoke("DelayedResponse", _
                         New Object() {waitPeriod}, _
                         callback, _
                         asyncState)
End Function

BeginDelayedResponse uses the BeginInvoke method that in many ways is similar to the Invoke method we used before. The first two parameters are identical to the ones used for the Invoke method. However, there are two additional parameters for BeginInvoke, and it is no longer returning an object array. The key difference, however, is that BeginInvoke returns immediately and does not wait for the Web service call to complete.

If we look at the first of the two additional parameters to BeginInvoke, we see something called System.AsyncCallback. This is what is called a delegate, which basically is a mechanism for declaring a function pointer in managed code. In this case, the function will be called once the Web method call completes and we have received the response.

The last parameter to BeginInvoke is something called asyncState and is simply declared as an object type. This can be anything that you want to use to track this request. You can use the same callback function for many different asynchronous requests, so in order to distinguish the response to one call from the response to another, you place information about the call in the asyncState parameter and it will be available to you in your callback function.

The last thing different about BeginInvoke is what it returns. Obviously, if the call has not completed, it cannot return the response data to the Web method. What it does return is a System.IAsyncResult interface pointer. You can use the IAsyncResult interface pointer to get information about the request. IAsyncResult exposes four public properties, which are listed below:

Property Description
AsyncState This is simply the data passed in the fourth parameter of the BeginInvoke method.
AsyncWaitHandle This is a WaitHandle object that can be used to block the thread's current execution until one or more Web service calls complete.
CompletedSynchronously This parameter does not apply to Web service calls. The IAsyncResult interface is used for a number of I/O operations, and this property would let you know if the asynchronous I/O operation request completed so fast that it finished even before the return of the Begin function.
IsCompleted This is simply a flag that you can use to determine whether the call has completed or not.

The IAsyncResult pointer is what ultimately will allow you and the system to tell one asynchronous completion from another. It also gives you different options for determining when a call completes. We will look at how to use these options shortly.

As noted earlier, the Add Web Reference option creates a function for each Web method provided by your service that uses BeginInvoke. In our case, the function generated is called BeginDelayedResponse, which is a very thin wrapper for the BeginInvoke method and exposes the parameters to the Web method call as well as the callback and asyncState parameters provided by BeginInvoke. Next we will look at the three options we have for making an asynchronous request and for determining when it is completed.

Three Options for Making Asynchronous Calls

Every application is different, and there are scenarios for making asynchronous calls that work for some applications that may not work for others. The .NET Framework is quite flexible in the ways that it provides for making asynchronous calls. You can either poll to see when a request completes, block on the WaitHandle, or wait for the callback function. Let's take a look at each of these approaches.

Polling for Completion

The IAsyncResult interface that is returned from our BeginDelayedResponse function has an IsCompleted property that can be checked to determine if the request has completed or not. One option you have is to poll this property until it returns a value of True. Abbreviated code that demonstrates this approach is shown below:

' Polling code that could tie up your processor
Dim proxy as New localhost.Service1()
Dim result as IAsyncResult
Result = proxy.BeginDelayedResponse(2000, _
                                    Nothing, _
                                    Nothing)
While (result.IsCompleted = False)
    ' Do some processing
        ...
Wend
Dim response as String
response = proxy.EndDelayedResponse(result)

Polling for completion is a pretty straightforward approach, but it does have some drawbacks. The code shows us making the initial call to BeginDelayedResponse, passing 2000 as the parameter to be passed to the Web method, and then has the callback and asyncState set to Nothing. We then have a while loop that polls the IsCompleted property until it is true. When the call completes and the IsCompleted property is set to True, we fall out of the while loop, and we get the response using another function in the Add Web Reference-generated class called EndDelayedResponse. EndDelayedResponse is the wrapper function for the EndInvoke method of the SoapHttpClientProtocol class and is the mechanism for getting the returned data from the Web method call. It is there to use after you know the Web service call has completed; it simply returns the same information that the Invoke method returns for blocking calls. We will be using the EndDelayedResponse method in all three asynchronous scenarios to get the results of our Web service call. It is worth noting that if EndDelayedResponse is called before the request has completed, it will simply block until the request does complete.

One of the problems you need to watch out for when using polling to determine if your call has completed is that you can end up using a lot of your machine's CPU cycles if you are not careful. For instance, if there is no code in our while loop, then the thread that is executing this code can gobble up most of the processing resources on your machine. In fact it can gobble up so much processing time that the underlying code to send our Web service request and receive the response may be delayed. Therefore you will want to be careful in how you use polling.

If you really want to just wait until the Web service request completes, you should probably use the WaitHandle approach that we will look at next. However, if you do have a lot of processing to do and you just want to check once in awhile to see if the Web service call is finished, then using polling is not a bad solution. Many times, applications will use a combination of polling with one of the other asynchronous approaches. For instance, you may make the Web service call asynchronously because you have some background processing to perform, but once you have finished performing your background processing, you may just want to block until the Web service is completed. In this case, you may poll occasionally while you are doing your processing, but then use a WaitHandle to wait for the result once the background processing is complete.

Using WaitHandles

WaitHandles are convenient for scenarios where you need to make asynchronous calls, but you do not want to release the thread you are currently executing in. For instance, if you make an asynchronous Web service call from within an ASP.NET application, and then you return from the event you are handling for your ASP.NET application, you may not have the opportunity to include the data from the Web service call in the data returned to the user. With WaitHandles you can do some processing after your Web service call has been made, then block until the Web service call has completed. WaitHandles are particularly useful if you are making multiple Web service calls from within an ASP.NET page.

Access to a WaitHandle object is provided by the IAsyncResult returned from the BeginDelayedResponse function. The following code shows a simple scenario using the WaitHandle approach.

' Simple WaitHandle code 
Dim proxy As New localhost.Service1()
Dim result As IAsyncResult
result = proxy.BeginDelayedResponse(2000, Nothing, Nothing)
'  Do some processing.
'       ...
'  Done processing.  Wait for completion.
result.AsyncWaitHandle.WaitOne()
Dim response As String
response = proxy.EndDelayedResponse(result)

In the code above, we used the WaitOne method of the WaitHandle object to wait for this one handle. There are also static methods called WaitAll and WaitAny in the WaitHandle class. These two static methods take arrays of WaitHandles as parameters, and either return when all the calls have completed, or as soon as any of the calls have completed, depending on which function you call. Say you are calling three separate Web services. You can call each asynchronously, place the WaitHandle for each in an array, then call the WaitAll method until they are finished. This allows the Web service calls to execute at the same time. If you were doing this synchronously, you would be unable to run them in parallel, resulting in roughly triple the execution time.

It should be noted that the WaitOne, WaitAll, and WaitAny methods all have the option of taking a timeout as a parameter. This allows you to control just how long you are willing to wait for a Web service to complete. If the methods timeout, they will return a value of False. This allows you to do more processing before you wait again, or it gives you an opportunity to cancel the requests.

Using Callbacks

The third approach to doing asynchronous Web service calls is to use callbacks. Using callbacks can be very efficient if you are making a lot of simultaneous Web service calls. It lends itself well to doing background processing on the results to Web service calls. However, it is a more complex approach than the other two methods. Using callbacks in a Microsoft Windows® application is a particularly nice approach, as this prevents' blocking the message thread for your Window.

Callbacks simply result in your callback function being called when the Web service call has completed. However, the callback function may not be called in the context of the thread that made the original BeginInvoke call. This can cause issues with sending commands to the controls in a Windows Form application, because those commands must be called from the specific thread that handles message processing for that particular Window. Fortunately there is a way to get around this problem.

The following code shows a simple scenario where a callback is used and the results from the Web service call are displayed in a label control.

Dim proxy as localhost.Service1
Private Delegate Sub MyDelegate(ByVal response As String)

Private Sub Button1_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles Button1.Click
    proxy = New localhost.Service1()
    proxy.BeginDelayedResponse(2000, _
            New AsyncCallback(AddressOf Me.ServiceCallback), _
            Nothing)
End Sub

Private Sub ServiceCallback(ByVal result As IAsyncResult)
    Dim response As String
    response = proxy.EndDelayedResponse(result)
    Label1.Invoke( _
        New MyDelegate(AddressOf Me.DisplayResponse), _
        New Object() {response})
End Sub

Private Sub DisplayResponse(ByVal response As String)
    Label1.Text = response
End Sub

In this code, we call BeginDelayedResponse in the Button1_Click event and then return from the subroutine. Notice in this case that instead of passing Nothing as the second parameter, we pass an AsyncCallback object. Basically, this is just a way of wrapping up the address of our callback function, which is called ServiceCallback. The ServiceCallback function has to be a subroutine that takes a single parameter of type IAsyncResult. The SoapHttpClientProtocol class will call this function and will provide the IAsyncResult interface pointer that corresponds to the completed Web service call. Again we use the EndDelayedResponse method to get the results, but now we have a small problem.

Because our callback will probably not be in the main thread for our Window, we need to use a different Invoke method to allow us to set the text for a label in our Window. All controls have an Invoke method that you can call in order to call a function that will run in their main message thread. Using Invoke on the label control is similar to some of the other things we have already looked at. We must give Invoke the address of the function we want it to call, and we include any parameters we want to pass to that function in an object array as the second parameter. In this case, we have to declare a delegate type that we call MyDelegate in order to inform the Invoke method of the syntax of the function we want it to call. In this case, the function is called DisplayResponse and only has one parameter—the string that is returned from the Web service call. DisplayResponse will be called within the proper thread for the label1 control, and it will have no problem with setting the text of the control for our application.

Advanced Issues

So far, the examples that we have looked at have been pretty straightforward. They make a lot of assumptions about things like the success of our Web service calls and are simplified by the fact that only a single call is made at any one time. Now we are going to look at a scenario where there are multiple Web service calls. We will see how to handle any faults that might be generated by these calls, and we will look at how we can cancel calls if we need to.

Using asyncState

We have yet to look at an example where we pass something besides Nothing as the last parameter to our BeginDelayedResponse call. If you recall, this is the asyncState parameter, which is simply declared as an object. We will now use this parameter to allow us to make multiple Web service calls, so that we can correlate the responses with the appropriate request.

The scenario I am going to look at is a scenario where I want to make three different calls to the DelayedResponse Web method. I will then display the results from these calls in three different label controls in my Windows application. The results of my first call should be displayed in the first label, the results of my second call should be displayed in my second label, and the results of my third call should be displayed in my third label. I am going to use the same callback function for each Web service call, and in order to determine which label goes along with which call, I will pass the label object in the asyncState parameter. Just to create even more confusion, I have randomized the length of time that each call will take to complete. The code for doing this is shown below.

Dim proxy As localhost.Service1
Private Delegate Sub LabelDelegate( _
    ByVal responseLabel As Label, _
    ByVal response As String)

Private Sub Button1_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles Button7.Click
    proxy = New localhost.Service1()
    ClearForm()
    Dim r As New Random()

    proxy.BeginDelayedResponse(r.Next(1, 10000), _
            New AsyncCallback(AddressOf Me.ServiceCallback), _
            Label1)
    proxy.BeginDelayedResponse(r.Next(1, 10000), _
            New AsyncCallback(AddressOf Me.ServiceCallback), _
            Label2)
    proxy.BeginDelayedResponse(r.Next(1, 10000), _
            New AsyncCallback(AddressOf Me.ServiceCallback), _
            Label3)
End Sub

Private Sub ServiceCallback(ByVal result As IAsyncResult)
    Dim response As String
    response = proxy.EndDelayedResponse(result)
    Dim responseLabel As Label = result.AsyncState
    responseLabel.Invoke( _
        New LabelDelegate(AddressOf Me.DisplayResponses), _
        New Object() {responseLabel, response})
End Sub

Private Sub DisplayResponses(ByVal responseLabel As Label, _
                             ByVal response As String)
    responseLabel.Text = response
    Form1.ActiveForm.Refresh()
End Sub

The label object that was passed in the asyncState parameter is available in our callback function through the IAsyncResult parameter that is passed to the callback. We have also modified the delegate from the earlier example to take another parameter. The additional parameter is again the label, so that the delegate knows which control to set the text for.

Quite often you may want to pass more information than just a single object through asyncState. Because asyncState is only declared as an object, it can also be used to pass complex information, such as an array of objects or some complex structure that you would like to use. In our case, we only used one proxy object, but in some scenarios, you may have a different proxy object for each Web service call. If this is the case, then you might want to also include the proxy object in the asyncState data. Any other data specific to your Web service call is also a likely candidate for inclusion.

Catching Faults

If you are used to making synchronous Web service calls, then you are probably also used to catching faults through a try…catch block that envelopes the remote call you are making. Doing the same thing with asynchronous calls may seem puzzling. Would I have to wrap the BeginDelayedResponse or the EndDelayedResponse in a try…catch block? Maybe I even have to wrap both!

Actually, for normal SOAP faults, and for other transport-related faults, we only need to wrap the EndDelayedResponse call in a try…catch block. It makes sense that the BeginDelayedResponse call is not going to generate a fault because it returns immediately and doesn't wait to see if there are any problems. The faults are not going to just happen randomly while you are waiting for callbacks to be called or for Wait calls to unblock. What will happen in the case of a fault is that completion will be triggered through whatever mechanism you are using, whether it be setting the IsCompleted property to True, triggering a WaitHandle, or invoking a callback. It is only when you make the call to EndDelayedResponse that a fault will be triggered that gives you the information on what might have failed.

To account for the possibility of failure in my application, I added a try...catch block to my callback function surrounding my EndDelayedResponse call. I modified the response text accordingly on failure.

Private Sub ServiceCallback(ByVal result As IAsyncResult)
    Dim response As String
    Try
        response = proxy.EndDelayedResponse(result)
    Catch e As Exception
        Response = "Failed"
    End Try
    Dim responseLabel As Label = result.AsyncState
    responseLabel.Invoke( _
        New LabelDelegate(AddressOf Me.DisplayResponses), _
        New Object() {responseLabel, response})
End Sub

Aborting Requests

One of the benefits of using asynchronous requests in an application is that you don't have to lock up your user interface while the call completes. Of course if you are going to allow your application's users to continue to interact with your application, then one very common feature you may want to include is a button for canceling the outstanding requests. If for some reason it is going to take a long time for the Web service call to complete, it often makes sense to give the user the option of determining just how long they are willing to wait.

Aborting a request is simple. Use the Abort method available on the proxy class that is created when you used the Add Web Reference option. You should remember a couple things about aborting requests. When you call the Abort method, any outstanding requests will still complete, but they will complete with a fault. This means that if you are using callbacks, your callback function will still be called for each outstanding request . When the EndInvoke method is called, or in our case, the wrapper function EndDelayedResponse, then a fault will be generated indicating that the underlying connection has been closed.

Spawning Threads to Make Synchronous Calls

There is another option available for solving many of the problems that asynchronous calls address. That is to simply spawn a thread and have that thread make easy-to-code synchronous calls. This may make sense in some scenarios, but you should realize a couple of issues with this approach.

First of all, this may make your Web service call code a little simpler, but there is a good chance that you will have to implement logic that is at least as complicated as some of the code we have used in order to manage and communicate between your threads. If you are making a lot of Web service calls, you may be overburdening your system with threads to manage. For a normal client application, this may not be a big deal, but if you are making calls from an ASP.NET page, you need to take into account how many users may simultaneously use your system. Adding two threads that do Web service calls from an ASP.NET page may not seem like a lot of overhead, but what happens if 200 other people are simultaneously using the same page? Now you are talking about 400 new threads, which is definitely significant. Although the callback mechanism for doing asynchronous calls uses extra threads for making the callbacks, they reuse a pool of threads in order to make the callbacks as efficient as possible—as well as to avoid problems with having hundreds of threads simultaneously running.

If spawning background threads to make your Web service calls still makes sense to you, then you should consider spawning these threads using delegates and the process thread pool. You can then call these methods using asynchronous paradigms similar to the ones we used for Web service callbacks and invoking function calls to interact with controls across threads. For more information on using delegates to make general asynchronous method calls, see Richard Grimes' article, .Net Delegates: Making Asynchronous Method Calls in the .Net Environment.

Conclusion

Making Web service calls asynchronously is probably one of the more useful capabilities provided for consuming Web services over HTTP from .NET Framework applications. Most real-world applications will want to use this capability to efficiently call Web services without blocking applications while potentially lengthy network calls complete. The .NET Framework is flexible in how it supports asynchronous Web service calls over HTTP, and gives developers a lot of control in determining how they want to handle completions.

 

At Your Service

Matt Powell is a member of the MSDN Architectural Samples team, where he helped develop the groundbreaking SOAP Toolkit 1.0. Matt's other achievements include co-authoring Running Microsoft Internet Information Server from Microsoft Press, writing numerous magazine articles, and having a beautiful family to come home to every day.