When a client calls a service, usually the client is blocked while the service executes the call, and control returns to the client only when the operation completes its execution and returns. However, there are quite a few cases in which you want to call operations asynchronously; that is, you want control to return immediately to the client while the service executes the operation in the background, and then somehow let the client know that the method has completed execution and provides the client with the results of the invocation. Such an execution mode is called asynchronous operation invocation, and the action is known as an asynchronous call. Asynchronous calls allow you to improve client responsiveness and availability.
One-way operations are inadequate for asynchronous calls. First, one-way calls are not guaranteed to be asynchronous at all. If the service’s incoming calls queue is filled to capacity, WCF will block the caller of a one-way call until it can place the call in the queue. In addition, there is no easy way to notify the client regarding the results or errors of the call when a one-way operation completes. While you could hand-craft a custom mechanism that passes method IDs to every one-way call and then uses a callback to report completion, results, and errors back to the client, such a solution would be cumbersome and proprietary. It would require the service to always catch all exceptions, while communication errors may not reach the client at all. It also mandates the use of a duplex binding, and you will not be able to call the service operation both synchronously and asynchronously.
To make the most of the various options available with WCF asynchronous calls, it is best to first list generic requirements set for any service-oriented asynchronous calls support. These include the following:
The same service code should be used for both synchronous and asynchronous invocation. This allows service developers to focus on business logic and cater to both synchronous and asynchronous clients.
A corollary of the first requirement is that the client should be the one to decide whether to call a service synchronously or asynchronously. That in turn implies that the client will have different code for each case (whether to invoke the call synchronously or asynchronously).
The client should be able to issue multiple asynchronous calls and have multiple asynchronous calls in progress. The client should be able to distinguish between multiple methods completions.
When a service operation has output parameters or return values, these parameters are not available when control returns to the client. The client should have a way to harvest these results when the operation completes.
Similarly, communication errors or the service’s error should be communicated back to the client side. An exception thrown during the operation execution should be played back to the client later on.
The implementation of the mechanism should be independent of the binding and transfer technology used. Any binding should support asynchronous calls.
The mechanism should not use technology-specific constructs such as .NET exceptions or delegates.
The last item is less of a requirement and more of a design guideline: the asynchronous calls mechanism should be straightforward and simple to use. For example, the mechanism should as much as possible hide its implementation details, such as the worker threads used to dispatch the call.
The client has a variety of options for handling operation completion. The client issues an asynchronous call and then can choose to:
Perform some work while the call is in progress and then block until completion.
Perform some work while the call is in progress and then poll for completion.
Receive notification when the method has completed. The notification will be in the form of a callback on a client-provided method. The callback should contain information identifying which operation has just completed and its return values.
Perform some work while the call is in progress, then wait for only a predetermined amount of time, and stop waiting, even if the operation execution has not completed yet.
Wait simultaneously for completion of multiple operations. The client can also choose to wait for all or any of the pending calls to complete.
WCF offers all of these options to clients. The WCF support is strictly a client-side facility, and in fact the service is unaware it is being invoked asynchronously. This means that intrinsically any service supports asynchronous calls, and that you can call the same service both synchronously and asynchronously. In addition, because all of the asynchronous invocation support happens on the client side regardless of the service, you can use any binding for the asynchronous invocation.
Because the client decides if the call should be synchronous or asynchronous, you need to create a different proxy for the asynchronous case. Using the /async
switch of SvcUtil, you can generate a proxy that contains asynchronous methods in addition to the synchronous ones. For each operation in the original contract, the asynchronous proxy and contract will contain two additional methods of this form:
[OperationContract(AsyncPattern = true
,
Action = "<original action name>",
ReplyAction = "<original response name">)]
IAsyncResult Begin<Operation>(<in arguments>,
AsyncCallback callback,object asyncState);
<returned type> End<Operation>(<out arguments>,IAsyncResult result);
The OperationContract
attribute offers the AsyncPattern
Boolean property defined as:
[AttributeUsage(AttributeTargets.Method)] public sealed class OperationContractAttribute : Attribute { public bool AsyncPattern {get;set;} //More members }
The AsyncPattern
property defaults to false
. AsyncPattern
has meaning only on the client-side copy of the contract. You can only set AsyncPattern
to true
on a method with a Begin<Operation>( )
-compatible signature, and the defining contract must also have a matching method with an End<Operation>( )
-compatible signature. These requirements are verified at the proxy load time. What AsyncPattern
does is bind the underlying synchronous method with the Begin
/End
pair, and correlates the synchronous execution with the asynchronous one. Briefly, when the client invokes a method of the form Begin<Operation>( )
with AsyncPattern
set to true
, it tells WCF not to try to directly invoke a method by that name on the service. Instead, it will use a thread from the thread pool to synchronously call the underlying method (identified by the Action
name). The synchronous call will block the thread from the thread pool, not the calling client. The client will only be blocked for the slightest moment it takes to dispatch the call request to the thread pool. The reply method of the synchronous invocation is correlated with the End<Operation>( )
method.
Example 8-26 shows a calculator contract and implementing service, and the generated proxy class when the /async
switch is used.
Example 8-26. Asynchronous contract and proxy
////////////////////////// Service Side ////////////////////// [ServiceContract] interface ICalculator { [OperationContract] int Add(int number1,int number2); //More operations } class Calculator : ICalculator { public int Add(int number1,int number2) { return number1 + number2; } //Rest of the implementation } ////////////////////////// Client Side ////////////////////// [ServiceContract] public interface ICalculator { [OperationContract] int Add(int number1,int number2); [OperationContract(AsyncPattern = true
, Action = ".../ICalculator/Add
", ReplyAction = ".../ICalculator/AddResponse
")] IAsyncResult BeginAdd(int number1,int number2,AsyncCallback callback, object asyncState); int EndAdd(IAsyncResult result); //Rest of the methods } public partial class CalculatorClient : ClientBase<ICalculator>,ICalculator { public int Add(int number1,int number2) { return Channel.Add(number1,number2); } public IAsyncResult BeginAdd(int number1,int number2, AsyncCallback callback,object asyncState) { return Channel.BeginAdd(number1,number2,callback,asyncState); } public int EndAdd(IAsyncResult result) { return Channel.EndAdd(result); } //Rest of the methods and constructors }
Note that the BeginAdd( )
operation on the contract still has the original action and reply names, and in fact, you can just omit them:
[OperationContract(AsyncPattern = true
)]
IAsyncResult BeginAdd(int number1,int number2,AsyncCallback callback,
object asyncState);
Begin<Operation>( )
accepts the input parameters of the original synchronous operation. Input parameters include data contracts passed by value or by reference (using the ref
modifier). The original method’s return values and any explicit output parameters (using the out
and ref
modifiers) are part of the End<Operation>( )
method. For example, for this operation definition:
[ServiceOperation] string MyMethod(int number1,out int number2,ref int number3);
the corresponding Begin<Operation>( )
and End<Operation>( )
methods look like this:
[ServiceOperation(...)] IAsyncResult BeginMyMethod(int number1,ref int number3, AsyncCallback callback,object asyncState); string EndMyMethod(out int number2,ref int number3,IAsyncResult asyncResult);
Begin<Operation>( )
accepts two additional input parameters, not present in the original operation signature: callback
and asyncState
. The callback
parameter is a delegate targeting a client-side method completed notification event. asyncState
is an object that conveys whatever state information is needed by the party handling the method completion. These two parameters are optional: the caller can choose to pass in null
instead of either one of them. For example, to asynchronously invoke the Add( )
method of the Calculator
service from Example 8-26 using the asynchronous proxy, if you have no interest in the results or the errors:
CalculatorClient proxy = new CalculatorClient( ); proxy.BeginAdd(2,3,null,null);//Dispatched asynchronously proxy.Close( );
As long as the client has the definition of the asynchronous contract, you can also invoke the operation asynchronously using a channel factory:
ChannelFactory<ICalculator> factory = new ChannelFactory<ICalculator>( ); ICalculator proxy = factory.CreateChannel( ); proxy.BeginAdd(2,3,null,null); ICommunicationObject channel = proxy as ICommunicationObject; channel.Close( );
The problem with such invocation is that the client has no way of getting its results.
Every Begin<Operation>( )
method returns an object implementing the IAsyncResult
interface, defined in the System.Runtime.Remoting.Messaging
namespace as:
public interface IAsyncResult { object AsyncState {get;} WaitHandle AsyncWaitHandle {get;} bool CompletedSynchronously {get;} bool IsCompleted {get;} }
The returned IAsyncResult
object uniquely identifies the method that was invoked using Begin<Operation>( )
. You can pass the IAsyncResult
object to End<Operation>( )
to identify the specific asynchronous method execution from which you wish to retrieve the results. End<Operation>( )
will block its caller until the operation it waits for (identified by the IAsyncResult
object passed in) completes and it can rerun the results or the errors. If the method is already complete by the time End<Operation>( )
is called, End<Operation>( )
will not block the caller and will just return the results. Example 8-27 shows the entire sequence.
Example 8-27. Simple asynchronous execution sequence
CalculatorClient proxy = new CalculatorClient( ); IAsyncResult asyncResult1 = proxy.BeginAdd(2,3,null,null); IAsyncResult asyncResult2 = proxy.BeginAdd(4,5,null,null); proxy.Close( ); /* Do some work */ int sum; sum = proxy.EndAdd(asyncResult1);//This may block Debug.Assert(sum == 5); sum = proxy.EndAdd(asyncResult2);//This may block Debug.Assert(sum == 9);
As simple as Example 8-27 is, it does demonstrate a few key points. The first point is that the same proxy instance can invoke multiple asynchronous calls. The caller can distinguish among the different pending calls using each unique IAsyncResult
object returned from Begin<Operation>( )
. In fact, when the caller makes asynchronous calls, as in Example 8-27, the caller must save the IAsyncResult
objects. In addition, the caller should make no assumptions about the order in which the pending calls complete. It is quite possible the second call will complete before the first one. Finally, if you have no more use for the proxy, you can close it immediately after dispatching the asynchronous calls, and still be able to call End<Operation>( )
.
Although it isn’t evident in Example 8-27, there are two important programming points regarding asynchronous calls:
End<Operation>( )
can be called only once for each asynchronous operation. Trying to call it more than once results in an InvalidOperationException
.
You can pass the IAsyncResult
object to End<Operation>( )
only on the same proxy object used to dispatch the call. Passing the IAsyncResult
object to a different proxy instance results in an AsyncCallbackException
.
When a client calls End<Operation>( )
, the client is blocked until the asynchronous method returns. This may be fine if the client has a finite amount of work to do while the call is in progress, and if, once that work is done, the client cannot continue its execution without the returned value or the output parameters of the operation, or even just the knowledge that the operation has completed. However, what if the client only wants to check if the operation execution has completed? What if the client wants to wait for completion for a fixed timeout, do some additional finite processing, and then wait again? WCF supports these alternative programming models to calling End<Operation>( )
.
The IAsyncResult
interface object returned from Begin<Operation>( )
has the AsyncWaitHandle
property, of type WaitHandle
:
public abstract class WaitHandle : ... { public static bool WaitAll(WaitHandle[] waitHandles); public static int WaitAny(WaitHandle[] waitHandles); public virtual void Close( ); public virtual bool WaitOne( ); //More memebrs }
The WaitOne( )
method of WaitHandle
returns only when the handle is signaled. Example 8-28 demonstrates using WaitOne( )
.
Example 8-28. UsingIasyncResult.AsyncWaitHandle to block until completion
CalculatorClient proxy = new CalculatorClient( ); IAsyncResult asyncResult = proxy.BeginAdd(2,3,null,null); proxy.Close( ); /* Do some work */ asyncResult.AsyncWaitHandle.WaitOne( ); //This may block int sum = proxy.EndAdd(asyncResult); //This will not block Debug.Assert(sum == 5);
Logically, Example 8-28 is identical to Example 8-27, which called only End<Operation>( )
. If the operation is still executing, WaitOne( )
will block. If by the time WaitOne( )
is called, the method execution is complete, WaitOne( )
will not block, and the client proceeds to call End<Operation>( )
for the returned value. The important difference between Examples 8-28 and 8-27 is that the call to End<Operation>( )
in Example 8-28 is guaranteed not to block its caller.
Example 8-29 demonstrates a more practical way of using WaitOne( )
, by specifying the timeout (10 milliseconds in this example). When you specify a timeout, WaitOne( )
returns when the method execution is completed or when the timeout has elapsed, whichever condition is met first.
Example 8-29. Using WaitOne( ) to specify wait timeout
CalculatorClient proxy = new CalculatorClient( ); IAsyncResult asyncResult = proxy.BeginAdd(2,3,null,null); while(asyncResult.IsCompleted == false) { asyncResult.AsyncWaitHandle.WaitOne(10,false); //This may block /* Do some work */ } int sum = proxy.EndAdd(asyncResult); //This will not block
Example 8-29 uses another handy property of IAsyncResult
, called IsCompleted
. IsCompleted
lets you find the status of the call without waiting or blocking. You can even use IsCompleted
in a strict polling mode:
CalculatorClient proxy = new CalculatorClient( ); IAsyncResult asyncResult = proxy.BeginAdd(2,3,null,null); proxy.Close( ); //Sometime later: if(asyncResult.IsCompleted) { int sum = proxy.EndAdd(asyncResult); //This will not block Debug.Assert(sum == 5); } else { //Do something meanwhile }
The AsyncWaitHandle
property really shines when you use it to manage multiple concurrent asynchronous methods in progress. You can use WaitHandle
’s static WaitAll( )
method to wait for completion of multiple asynchronous methods, as shown in Example 8-30.
Example 8-30. Waiting for completion of multiple methods
CalculatorClient proxy = new CalculatorClient( );
IAsyncResult asyncResult1 = proxy.BeginAdd(2,3,null,null);
IAsyncResult asyncResult2 = proxy.BeginAdd(4,5,null,null);
proxy.Close( );
WaitHandle[] handleArray = {asyncResult1.AsyncWaitHandle,
asyncResult2.AsyncWaitHandle};
WaitHandle.WaitAll
(handleArray);
int sum;
//These calls to EndAdd( ) will not block
sum = proxy.EndAdd(asyncResult1);
Debug.Assert(sum == 5);
sum = proxy.EndAdd(asyncResult2);
Debug.Assert(sum == 9);
To use WaitAll( )
, you need to construct an array of handles. Note that you still need to call End<Operation>( )
to access returned values. Instead of waiting for all of the methods to return, you can choose to wait for any of them to return, using the WaitAny( )
static method of the WaitHandle
class. Much like WaitOne( )
, both WaitAll( )
and WaitAny( )
have a few overloaded versions, which let you specify a timeout to wait instead of waiting indefinitely.
Instead of blocking, waiting, or polling for an asynchronous call to complete, WCF offers another programming model altogether—completion callbacks. The client provides WCF with a method and requests that WCF will call that method back when the asynchronous method completes. The client can provide a callback instance method or static method and have the same callback method handle completion of multiple asynchronous calls. When the asynchronous method execution is complete, instead of quietly returning to the pool, the worker thread calls the completion callback. To designate a completion callback method, the client needs to provide Begin<Operation>( )
with a delegate of the type AsyncCallback
defined as:
public delegate void AsyncCallback(IAsyncResult asyncResult);
That delegate is provided as the penultimate parameter to Begin<Operation>( )
.
Example 8-31 demonstrates asynchronous call management by using a completion callback.
Example 8-31. Managing asynchronous call with a completion callback
class MyClient : IDisposable { CalculatorClient m_Proxy = new CalculatorClient( ); public void CallAsync( ) { m_Proxy.BeginAdd(2,3,OnCompletion,null); } void OnCompletion(IAsyncResult result) { int sum = m_Proxy.EndAdd(result); Debug.Assert(sum == 5); } void Dispose( ) { m_Proxy.Close( ); } }
Unlike the programming models described so far, when you use a completion callback method, there’s no need to save the IAsyncResult
object returned from Begin<Operation>( )
because when WCF calls the completion callback, WCF provides the IAsyncResult
object as a parameter. Because WCF provides a unique IAsyncResult
object for each asynchronous method, you can channel multiple asynchronous method completions to the same callback method:
m_Proxy.BeginAdd(2,3,OnCompletion
,null); m_Proxy.BeginAdd(4,5,OnCompletion
,null);
Instead of using a class method as a completion callback, you can just as easily use a local anonymous method:
CalculatorClient proxy = new CalculatorClient( ); int sum; AsyncCallback completion = delegate(IAsyncResult result) { sum = proxy.EndAdd(result); Debug.Assert(sum == 5); }; proxy.BeginAdd(2,3,completion,null); proxy.Close( );
Note that the anonymous method sets an outer variable (sum
) to provide the result of the Add( )
operation.
Callback completion methods are by far the preferred model in any event-driven application. An event-driven application has methods that trigger events (or requests) and methods that handle these events and fire their own events as a result. Writing an application as event-driven makes it easier to manage multiple threads, events, and callbacks, and allows for scalability, responsiveness, and performance. WCF asynchronous calls management using callback completion methods fits into such an architecture like a hand in a glove. The other options (waiting, blocking, and polling) are available for applications that are strict, predictable, and deterministic in their execution flow. I recommend that you use completion callback methods whenever possible.
Because the callback method is executed on a thread from the thread pool, you must provide for thread safety in the callback method and in the object that provides it. This means that you must use synchronization objects and locks to access the member variables of the client. You need to worry about synchronizing between client-side threads and the worker thread from the pool, and, potentially, synchronization between multiple worker threads all calling concurrently into the completion callback method to handle their respective asynchronous call completion. You need to make sure the completion callback method is reentrant and thread-safe.
The last parameter to Begin<Operation>( )
is asyncState
. The asyncState
object, known as a state object, is provided as an optional container for whatever need you deem fit. The party handling the method completion can access such a container object via the object AsyncState
property of IAsyncResult
. Although you can certainly use state objects with any of the other asynchronous call programming models (blocking, waiting, or polling), they are most useful in conjunction with completion callbacks. The reason is simple: in all the other programming models, it is up to you to manage the IAsyncResult
object, and managing an additional container is not that much of an added liability. When you are using a completion callback, the container object offers the only way to pass in additional parameters to the callback method, whose signature is predetermined.
Example 8-32 demonstrates how you might use a state object to pass an integer value as an additional parameter to the completion callback. Note that the callback must downcast the AsyncState
property to the actual type.
Example 8-32. Passing an additional parameter using a state object
class MyClient : IDisposable { CalculatorClient m_Proxy = new CalculatorClient( ); public void CallAsync( ) { int asyncState = 4; //int, for example m_Proxy.BeginAdd(2,3,OnCompletion,asyncState
); } void OnCompletion(IAsyncResult result) { int asyncState =(int)
asyncResult.AsyncState
; Debug.Assert(asyncState == 4); int sum = m_Proxy.EndAdd(result); } void Dispose( ) { m_Proxy.Close( ); } }
A common use for the state object is to pass the proxy used for Begin<Operation>( )
instead of saving it as a member variable:
class MyClient { public void CallAsync( ) { CalculatorClient proxy = new CalculatorClient( ); proxy.BeginAdd(2,3,OnCompletion,proxy
); proxy.Close( ); } void OnCompletion(IAsyncResult result) { CalculatorClient proxy = asyncResult.AsyncState
as CalculatorClient; Debug.Assert(proxy != null); int sum = proxy.EndAdd(result); Debug.Assert(sum == 5); } }
The completion callback may require some thread(s) affinity, to run in a particular synchronization context. This is especially the case if the completion callback needs to update some UI about the result of the asynchronous invocation. Unfortunately, you must manually marshal the call from the completion callback to the correct synchronization context, using any of the techniques described previously. Example 8-33 demonstrates such a completion callback that interacts directly with its containing form, assuring that the UI update will be on the UI synchronization context.
Example 8-33. Relying on completion callback synchronization context
partial class CalculatorForm :Form
{
CalculatorClient m_Proxy;
SynchronizationContext m_SynchronizationContext;
public MyClient()
{
InitializeComponent();
m_Proxy = new CalculatorClient();
m_SynchronizationContext = SynchronizationContext.Current;
}
public void CallAsync(object sender,EventArgs args)
{
m_Proxy.BeginAdd(2,3,OnCompletion,null);
}
void OnCompletion(IAsyncResult result)
{
SendOrPostCallback callback = delegate
{
Text = "Sum = " + m_Proxy.EndAdd(result);
};
m_SynchronizationContext.Send(callback,null);
}
public void OnClose(object sender,EventArgs args)
{
m_Proxy.Close( );
}
}
There is little sense in trying to invoke a one-way operation asynchronously, because one of the main features of asynchronous calls is retrieving and correlating a reply message, and yet no such message is available with a one-way call. If you do invoke a one-way operation asynchronously, End<Operation>( )
will never block, and no exceptions will ever be thrown. If a completion callback is provided for an asynchronous invocation of a one-way operation, the callback is called immediately after returning from Begin<Operation>( )
. The only justification for invoking a one way operation asynchronously is to avoid the potential blocking of the one-way call, in which case, you should pass a null
for the state object and the completion callback.
Output parameters and return values are not the only elements unavailable at the time an asynchronous call is dispatched: exceptions are missing as well. After calling Begin<Operation>( )
, control returns to the client, but it may be some time before the asynchronous method encounters an error and throws an exception, and it may be some time after that before the client actually calls End<Operation>( )
. WCF must therefore provide some way for the client to know that an exception was thrown and allow the client to handle it. When the asynchronous method throws an exception, the proxy catches it, and when the client calls End<Operation>( )
, the proxy rethrows that exception object, letting the client handle the exception. If a completion callback is provided, WCF calls the callback method immediately after the exception is received. The exact exception thrown is compliant with the fault contract and the exception type, as explained in Chapter 6.
Whenever calling Begin<Operation>( )
, the returned IAsyncResult
has a reference to a single WaitHandle
object, accessible via the AsyncWaitHandle
property. Calling End<Operation>( )
on that object does not close the handle. Instead, that handle will be closed when the implementing object is garbage-collected. As with any other case of using an unmanaged resource, you have to be mindful about your application-deterministic finalization needs. It is possible (in theory at least), for the application to dispatch asynchronous calls faster than .NET’s ability to collect those handles, resulting with a resource leak. To compensate, you can explicitly close that handle after calling End<Operation>( )
. For example, using the same definitions as those in Example 8-31:
void OnCompletion(IAsyncResult result)
{
int sum = m_Proxy.EndAdd(result);
Debug.Assert(sum == 5);result.AsyncWaitHandle.Close( );
}
Transactions do not mix well with asynchronous calls. First, well-designed transactions are of short duration, yet the main motivation for using asynchronous calls is the latency of the operations. Second, the client’s ambient transaction will not flow by default to the service, because the asynchronous operation is invoked on a worker thread, not the client’s thread. While it is possible to develop a proprietary mechanism that uses cloned transactions, this is esoteric at best and should be avoided. Finally, when a transaction completes, it should have no leftover activities done in the background that could commit or abort independently of the transaction, and yet this will be the result of spawning an asynchronous operation call from within a transaction. Do not mix transactions with asynchronous calls.
Although it is technically possible to call the same service synchronously and asynchronously, the likelihood that a service will be accessed both ways is low.
The reason is that using a service asynchronously necessitates drastic changes to the workflow of the client, and consequently the client cannot simply use the same execution sequence logic as with the synchronous access. Consider, for example, an online store application. Suppose the client (a server-side object executing a customer request) accesses a Store
service, where it places the customer’s order details. The Store
service uses three well-factored helper services to process the order: Order
, Shipment
, and Billing
. In a synchronous scenario, the Store
service calls the Order
service to place the order. Only if the Order
service succeeds in processing the order (i.e., if the item is available in the inventory) does the Store
service call the Shipment
service, and only if the Shipment
service succeeds does the Store
service access the Billing
service to bill the customer. This sequence is shown in Figure 8-4.
The downside to the workflow shown in Figure 8-4 is that the store must process orders synchronously and serially. On the surface, it might seem that if the Store
service invoked its helper objects asynchronously, it would increase throughput because it could process incoming orders as fast as the client submitted them. The problem in doing so is that it is possible for the calls to the Order
, Shipment
, and Billing
services to fail independently, and if they do then all hell will break loose. For example, the Order
service might discover there were no items in the inventory matching the customer request while Shipment
service tried to ship the nonexisting item and the Billing
service had already billed the customer for it.
Using asynchronous calls on a set of interacting services requires that you change your code and your workflow. To call the helper services asynchronously, the Store
service should call only the Order
service, which in turn should call the Shipment
service only if the order processing was successful (see Figure 8-5), to avoid the potential inconsistencies just mentioned. Similarly, only in the case of successful shipment should the Shipment
service asynchronously call the Billing
service.
In general, if you have more than one service in your asynchronous workflow, you should have each service invoke the next one in the logical execution sequence. Needless to say, such a programming model introduces tight coupling between services (they have to know about each other) and changes to their interfaces (you have to pass in additional parameters, which are required for the desired invocation of services downstream).
The conclusion is that using asynchronous instead of synchronous invocation introduces major changes to the service interfaces and the client workflow. Asynchronous invocation on a service that was built for synchronous execution works only in isolated cases. When dealing with a set of interacting services, it is better to simply spin off a worker thread to call them and use the worker thread to provide asynchronous execution. This will preserve the service interfaces and the original client execution sequence.