Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Orchestration of asynchronous communication

5.00/5 (6 votes)
31 May 2012CPOL4 min read 26.5K   251  
How to centralize and unify asynchronous execution of actions and functions.

Orchestrating the asynchronous communication

Introduction

Asynchronous communication is becoming more and more popular and widespread nowadays. It is mainly used to improve performance in multi-processor environments and to increase responsiveness of applications. At the latest with the introduction of Silverlight, asynchronous communication has become a must, because all web requests in that runtime happen asynchronously. This trend continues with Windows 8 on an even more fundamental level, as many system calls happen asynchronously there.

Implementing asynchronous communication asks for additional skills in developers, because they lose control over the sequence of operations to a certain degree, which in turn leads to distributed but still related pieces of source code, which contradicts some OO paradigms.

This article describes some simple means by which dealing with asynchronous communication can be greatly simplified. The associated library provides several tools with the following benefits:

  • Unification of the sequence of operations in 4 areas: Main, OnSuccess, OnError, OnFinally
  • Combination of the code of a sequence of operation
  • Type safety of return values and parameters through usage of generics
  • Automatic reuse of functions' return values
  • Simplification of error and exception handling
  • Centralizing the core of asynchronous communication in one place for easier analysis, logging etc.

Asynchronous Action

Image 2

To asynchronously execute an action, the library provides the class AsyncAction, which in turn offers various utility methods for starting it. The most elaborate signature is defined as follows:

C#
public static void Start(
  Invocable main,
  Invocable onSuccess,
  InvocableAction1Base<Exception> onError,
  Invocable onFinally,
  AsyncContext context )

The parameters main, onSuccess, onError, onFinally represent the code blocks according to the above operation sequence diagram. In case Main is executed without error, the OnSuccess block is started. Otherwise the resulting Exception is given as a parameter to OnError. For the use of AsyncAction, a given Main action is mandatory. The AsyncContext encapsulates the execution context and is described further down.

In the following example, an OnSuccess action is used aside from the Main action:

C#
// ----------------------------------------------------------------------
public void DeleteAllCustomersMinimal()
{
  AsyncAction.Start(
    new InvocableAction0( Services.DeleteAllCustomers ),
    new InvocableAction0( () => Customers.Clear() ) );
} // DeleteAllCustomersMinimal

Actions are available in various variants with up to 4 parameters: InvocableAction0InvocableAction4:

Asynchronous Action Classes

Given this inheritance hierarchy, calls to the respective actions can be customized individually as required. The following example demonstrates how to furnish the OnError action with an additional Customer parameter by using the InvocableAction2:

C#
// ----------------------------------------------------------------------
public void DeleteCustomer( Customer customer )
{
  AsyncAction.Start(
    // main
    new InvocableAction1<int>( 
      Services.DeleteCustomer, customer.Id ),
    // on success
    new InvocableAction1<Customer>( 
      c => Customers.Remove( c ), customer ),
    // on error
    new InvocableAction2<Exception, Customer>( 
      ( e, c ) => ShowError( e + " - " + c ), customer ),
    // on finally
    new InvocableAction1<Customer>( 
      c => Debug.WriteLine( "DeleteCustomer: " + c ) ) );
} // DeleteCustomer

Asynchronous Function

Image 4

Asynchronous functions can be started through the different variants of the start method in the class AsyncFunction. The most elaborate signature with all execution parameters is defined as follows:

C#
public static void Start(
  InvocableFunction<TResult> main,
  InvocableAction1Base<TResult> onSuccess,
  InvocableAction1Base<Exception> onError,
  Invocable onFinally,
  AsyncContext context )

The parameters main, onSuccess, onError, onFinally represent the code blocks according to the above operation sequence diagram. Contrary to the AsyncAction, the parameter Main is of type InvocableFunction, hence returning a result. In case Main is executed without error, the OnSuccess block is started. The return value of the Main function is given to the OnSuccess action as its first parameter. In case execution of the Main function results in an exception, it is passed as a parameter to the OnError action. Otherwise the resulting Exception is given as a parameter to OnError. For the use of AsyncFunction, a given Main function and a corresponding OnSuccess action are mandatory. The AsyncContext encapsulates the execution context and is described further down.

The following example demonstrates how the result of the Main Function is passed to the OnSuccess action:

C#
// ----------------------------------------------------------------------
public void GetCustomerSalesVolumeMinimal( Customer customer )
{
  AsyncFunction<double>.Start(
    // main
    new InvocableFunction1<int, double>(
      Services.GetCustomerSalesVolume, customer.Id ),
    // on success
    new InvocableAction2<double, Customer>(
      ( sv, c ) => c.SalesVolume = sv, customer ) );
} // GetCustomerSalesVolumeMinimal

Suitable arguments for the Main parameter of AsyncFunction can be functions with up to 4 parameters InvocableFunction0InvocableFunction4:

Asynchronous Function Classes

Execution context

At the center of asynchronous communication is the class AsyncContext, which is used by AsyncAction and AsyncFunction. It governs the following aspects:


Dispatcher  

The Dispatcher which is used to actually run the asynchronous Action or Function. By default it will use the Dispatcher of the current thread.

Thread  

Given the property ShouldInvokeOnDispatcherThread, it is possible to decide whether the Dispatcher should be run in the current Thread or via a ThreadPool in separate Thread.

Invoke  

Implements the central operation sequence which actually executes the asynchronous action or function.

Error handling

For proper handling of server side errors in case of asynchronous web communication (Silverlight), further steps are required. It is necessary to encapsulate errors which occur on the server and transfer them (via WCF) to the client.

This article abstains from a description of fault handling because it is not strictly on its main topic and beyond its scope. There are however many examples of how to accomplish this, for example in Catch and Handle WCF Service Exceptions in Silverlight.

Examples of application

Asynchronous communication demo application

The bundled sample application for WPF, Silverlight and Windows Phone demonstrates how to load a list of customer information items asynchronously:

C#
// ----------------------------------------------------------------------
private void LoadCustomers()
{
  AsyncFunction<CustomerCollection>.Start(
    // main
    new InvocableFunction0<CustomerCollection>( services.LoadCustomers ),
    // on success
    new InvocableAction1<CustomerCollection>( OnCustomersLoaded ),
    // on error
    new InvocableAction1<Exception>( OnCustomerLoadError ),
    // on finally
    new InvocableAction1<string>( OnFinished, "LoadCustomers" ) );
} // LoadCustomers

// ----------------------------------------------------------------------
private void OnCustomersLoaded( CustomerCollection customers )
{
  CustomerList.ItemsSource = customers;
  if ( customers != null && customers.Count > 0 )
  {
    CustomerList.SelectedIndex = 0;
  }
  AddToHistory( string.Format( "{0} customers loaded", customers != null ? customers.Count : 0 ) );
} // OnCustomersLoaded

// ----------------------------------------------------------------------
private void OnCustomerLoadError( Exception exception )
{
  AddToHistory( string.Format( "error: {0}", exception.Message ) );
} // OnCustomerLoadError

// ----------------------------------------------------------------------
private void OnFinished( string action )
{
  AddToHistory( string.Format( "'{0}' finished", action ) );
} // OnFinished

After the customers have been loaded, they are displayed in a list. Selecting a customer in that list will show its data in a detail section. Additionally, the sales volume of that customer is fetched asynchronously from an external service:

C#
private void UpdateCustomerInfo( Customer customer )
{
  // static customer data
  CustomerId.Text = customer.Id.ToString( CultureInfo.InvariantCulture );
  CustomerFirstName.Text = customer.FirstName;
  CustomerLastName.Text = customer.LastName;
 
  // sales volume
  AsyncFunction<double>.Start(
    // main
    new InvocableFunction1<int, double>( services.GetCustomerSalesVolume, customer.Id ),
    // on success
    new InvocableAction2<double, Customer>(
      ( salesVolume, cust ) =>
      {
        cust.SalesVolume = salesVolume;
        CustomerSalesVoume.Text = salesVolume.ToString( "C" );
        AddToHistory( string.Format( "customer {0} sales volume: {1:C}", cust, salesVolume ) );
      },
      customer ),
    // on error
    new InvocableAction2<Exception, Customer>(
      ( e, cust ) =>
      {
        CustomerSalesVoume.Text = string.Empty;
        AddToHistory( string.Format( "error get sales volume: {0}", cust ) );
      },
      customer ),
    // on finally
    new InvocableAction1<string>(
      action => AddToHistory( string.Format( "'{0}' finished", action ) ),
      "GetCustomerSalesVolume" ) );
} // UpdateCustomerInfo

Acknowledgement

Special thanks to Leon Poyyayil for the design and initial implementation and his support and contribution in the development of this component.

History

  • 1st June, 2012 - v1.0.0.0
    • Initial public release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)