Click here to Skip to main content
15,860,859 members
Articles / Programming Languages / Visual Basic

Automatic Implementation of the Event-Based Asynchronous Pattern

Rate me:
Please Sign up or sign in to vote.
4.78/5 (32 votes)
26 Nov 2008CPOL20 min read 62.9K   912   101   14
Implement the event-based asynchronous pattern automatically with this code generator
Event-Based Async Client

Contents

Introduction

AsyncGen is a utility that generates client classes implementing the event-based asynchronous pattern from annotated classes and interfaces such as the following...

C#
using System;
using System.Collections.Generic;
using System.Text;
using AsyncGen;

namespace TestAssembly
{
    [GenerateAsyncClientClass("Client")]
    public interface IServer
    {
        [GenerateAsyncOperation(
            GenerateCancelMethod = true,
            CompletedEventName = "CalculationComplete",
            CallbackInterface = typeof(IServerCallbacks))]
        double Calculate(double argument, 
		[TaskID] object userState, IServerCallbacks callbacks);
    }

    public interface IServerCallbacks
    {
        [GenerateProgressEvent("CalculationProgressChanged")]
        [return:CancelFlag] bool ReportCalculationProgress
		(double approximateResult, [ProgressPercentage] int percentage, 
		[TaskID] object userState);
    }
}

... from which AsyncGen will generate the following client class:

Generated Client Class

A note on terminology: For the remainder of this article, I shall use the terms "client class" and "proxy" interchangeably.

Background

Since its inception, the .NET framework provided extensive support for asynchronous invocation of operations.  Many methods in the .NET BCL (Base Class Library) have asynchronous versions in the form of a pair of methods named BeginOperation and EndOperation.  For example, the FileStream class provides the methods BeginRead and EndRead which serve as an asynchronous version of the Read method.  BeginRead returns an object that implements the IAsyncResult interface, and this object is used to associate each invocation of EndRead with an earlier invocation of BeginRead.  This pattern is referred to in various places as the asynchronous programming model (APM), the IAsyncResult pattern, or simply as the asynchronous pattern.  (The last term is used especially in pre-.NET 2.0 literature since this was the only pattern for asynchronous operations in .NET 1.x.)

In addition to the asynchronous methods provided by the BCL, the asynchronous delegates mechanism allows you to invoke any method asynchronously.  The BeginInvoke and EndInvoke methods which the compiler automatically generates every time you use the delegate keyword conform to the same pattern as the BCL methods.  Hence it can be said that asynchronous delegates provide a universal implementation of the IAsyncResult pattern.

The IAsyncResult pattern is ideal for "under the hood" operations like FileStream.Read, but it doesn't facilitate user interaction, namely:

  1. It doesn't support cancellation.
  2. It doesn't support progress reports.
  3. It doesn't support incremental results.
  4. It doesn't support different threading models for the completion callback.

For these reasons, a new pattern was introduced in .NET 2.0, known as the Event-Based Asynchronous Pattern, which offers support for all of the above. (See also Asynchronous Programming Design Patterns on MSDN.) So far, the closest thing to a universal implementation of the event-based pattern was the BackgroundWorker class, which allows you to run an arbitrary operation in the background without blocking your main thread, and still be able to receive notifications on the main thread when the operation completes and when there is progress. However, even BackgroundWorker doesn't implement the event-based pattern in its most general form; for example, it doesn't support multiple concurrent invocations of the same operation. Moreover, its events can't be marshalled between different application domains or processes, so it's not suitable for distributed scenarios.

AsyncGen attempts to provide a universal implementation of the event-based pattern, including all the features described in the MSDN article. To make it suitable for distributed scenarios, AsyncGen employs a client/server approach, which means that the details of the asynchronous invocation are not handled by the same class that implements the logic of the operation, but by a proxy class that uses the original class as a server. The proxy can reside either in the same process as the server or in a separate process. In the latter case, the proxy and the server communicate with each other using .NET Remoting. Internally, the proxy uses asynchronous delegates to implement the operations.

Using AsyncGen

Overview

To use AsyncGen, you have to follow these steps:

  1. Add a reference from your project to AsyncGenLib.dll.
  2. Annotate your public classes or interfaces with some special attributes. This step is explained in detail later on this page.
  3. Build your assembly.
  4. Open a command prompt, cd to the target directory, and invoke AsyncGen.exe on the compiled assembly.
  5. Add the generated classes to the same project as the original classes or to another project that references the original one.

Command Line Syntax

Synopsis

AsyncGen.exe [ options ] <assembly containing annotated classes> 
	[ <additional assembly needed to compile the generated classes> ... ]

Options

/lang:<output language>

The valid values for <output language> are:

  • CS : C# (the default)
  • VB : Visual Basic
  • CPP : C++/CLI

The code is generated in the current directory, each class in a separate source file, named class.generated.cs.

Note: When you run AsyncGen.exe, make sure that AsyncGenLib.dll is in the current directory. This requirement is easily fulfilled by running AsyncGen.exe in the bin\Debug or bin\Release directory of your project.

Annotating Your Classes

Before you use AsyncGen.exe, you must annotate your source classes and interfaces in a style similar to WCF services, using the following attributes from AsyncGenLib.dll:

GenerateAsyncClientClassAttribute

Place this attribute on every class or interface for which you want to generate an asynchronous proxy. If you don't specify any constructor arguments or named properties, the name of the proxy class will be the same as the name of the source class or interface with the word 'Client' appended and the leading 'I' stripped from interface names.

GenerateAsyncOperationAttribute

Place this attribute on every method for which you want to implement the pattern. It has several properties that can be used to customize the names of the generated members, such as the start method and the completion event. One property deserves special attention: the CallbackInterface property specifies an interface type that your server can then use to raise progress events on the proxy. This issue is explained in the next section.

GenerateProgressEventAttribute

This attribute is used on the methods of the callback interface to indicate the type of event that should be raised by each method.

TaskIDAttribute, ProgressPercentageAttribute, and CancelFlagAttribute

These attributes are used to identify method parameters that have special significance in the implementation of the event-based pattern. They don't have any constructor arguments or named properties.

Callback Interfaces

While the completion event is automatically raised by the proxy when the server method returns (either normally or abnormally), progress events must be raised by the server itself. This is done using a callback interface. Unlike WCF, which passes the callback interface implicitly to the server, AsyncGen requires the callback interface to be explicitly defined as one of the parameters of the original method, preferably the last one.

The callback interface's methods are tied to specific events on the proxy through the GenerateProgressEventAttribute. If the callback method has multiple arguments, AsyncGen packs all of them into a single value type which, in turn, is included in a generic EventArgs object. If the method has a single argument, it is included directly in the EventArgs.

You don't have to implement the callback interface. It is automatically implemented by the generated code, as you can see in the following example:

C#
// --- Source file: IServer.cs ---

[GenerateAsyncClientClass("Client")]
public interface IServer
{
    [GenerateAsyncOperation(CallbackInterface = typeof(IServerCallbacks))]
    double Calculate(double argument, [TaskID] object userState, 
	IServerCallbacks callbacks);
}

public interface IServerCallbacks
{
    [GenerateProgressEvent("CalculationProgressChanged")]
    [return:CancelFlag] bool ReportCalculationProgress
	(double approximateResult, [ProgressPercentage] int percentage, 
	[TaskID] object userState);
} 

// --- Source file: Client.generated.cs ---

public partial class Client : ClientBase<TestAssembly.IServer>
{
    // ...

    public void CalculateAsync(double argument, object userState)
    {
        this._calculateTracker.CreateOperation(userState);
        CalculateDelegate d = new CalculateDelegate(this.server.Calculate);
        d.BeginInvoke(argument, userState, this._calculateTracker, 
	new System.AsyncCallback(this._calculateTracker.PostOperationCompleted), 
	userState);
    }

    // ...

    public event AsyncGen.ProgressChangedEventHandler<double> CalculationProgressChanged;

    // ...

    private class CalculateTracker : OperationTracker<IServer, Client, 
		CalculateDelegate, double>, TestAssembly.IServerCallbacks
    {
        // ...

        bool IServerCallbacks.ReportCalculationProgress
		(double approximateResult, int percentage, object userState)
        {
            this.PostProgress<double>(new System.Threading.SendOrPostCallback
		(this.OnCalculationProgressChanged), percentage, 
		approximateResult, userState);
            return this.IsOperationCancelled(userState);
        } 
    }

    // ...
}

When the server calls callbacks.ReportCalculationProgress, the following takes place:

  1. If the client resides in a different application domain or in a different process, the call is marshalled to it over the Remoting channel.
  2. The callback method further marshals the event to the proper thread, using an AsyncOperation object associated with the current invocation (identified by the userState argument). In a WinForms application, this thread is usually the main thread, or, more generally, the thread that owns the control which was used to start the operation. In a console application, this thread is either the calling thread (if the message is sent synchronously, using SendProgress), or an arbitrary thread from the thread pool (if the message is sent asynchronously, using PostProgress).
  3. The DoSomethingProgressChanged event is raised on the proper thread. The arguments that were passed to ReportProgress are packed into the EventArgs argument.

Note the absence of the callback parameter from CalculateAsync's signature. CalculateAsync supplies this argument itself when it invokes the server's Calculate method. 

Cancellation

By default, AsyncGen doesn't generate a Cancel method. If your server supports cancellation, you should set the GenerateAsyncOperationAttribute.GenerateCancelMethod property to true. You can customize the name of this method using the GenerateAsyncOperationAttribute.CancelMethodName property.

The ability to cancel an operation requires the server to poll a boolean flag which can be raised by the client at any point during the lifetime of the operation. You have two options here:

  • Store the flag on the server side. AsyncGen does not provide any special support for this option because it needs to be implemented entirely on the server side. To comply with the event-based pattern, the server should define a public method named DoSomethingAsyncCancel that sets the cancel flag. This method should not be annotated.

  • Store the flag on the client side. The main advantage of this option is that the polling can be done on the return leg of the callback that reports the server's progress to the client. AsyncGen supports this by providing the CancelFlagAttribute. Applying this attribute to a callback method's return value or to one of its out parameters (either one has to be a Boolean) causes AsyncGen to generate an additional statement in the callback method, which returns the value of the cancel flag to the server. For example, if you annotate your method's return value as follows...

    C#
    public interface IServerCallbacks
    {
        [GenerateProgressEvent("CalculationProgerssChanged")]
        [return:CancelFlag] bool ReportCalculationProgress(double approximateResult, 
    	[ProgressPercentage] int percentage, [TaskID] object userState);
    }

    ...AsyncGen will generate the following code:

    C#
    bool IServerCallbacks.ReportCalculationProgress
    	(double approximateResult, int percentage, object userState)
    {
        this.PostProgress<double>(new System.Threading.SendOrPostCallback
    	(this.OnCalculationProgerssChanged), percentage, 
    	approximateResult, userState);
        return this.IsOperationCancelled(userState); 
    }

    If you prefer to use an out parameter instead, as follows...

    C#
    public interface IServerCallbacks
    {
        [GenerateProgressEvent("CalculationProgerssChanged")]
        void ReportCalculationProgress(double approximateResult, 
    	[ProgressPercentage] int percentage, [TaskID] object userState, 
            [CancelFlag] out bool cancel);
    }

    ...AsyncGen will generate the following code:

    C#
    void IServerCallbacks.ReportCalculationProgress
    	(double approximateResult, int percentage, 
    	object userState, out bool cancel)
    {
        this.PostProgress<double>(new System.Threading.SendOrPostCallback
    	(this.OnCalculationProgerssChanged), percentage, 
    	approximateResult, userState);
        cancel = this.IsOperationCancelled(userState); 
    }

    The server can then use the callback interface to combine progress reports with polling for cancellation, as seen in the following example:

    C#
    public int DoSomething(int n, object userState, IDoSomethingCallbacks callbacks)
    {
        bool cancelRequested;
        for (int i = 0; i < 20; i++)
        {
            if (callbacks != null)
            {
                callbacks.ReportProgress(5 * i, userState, out cancelRequested);
                if (cancelRequested) return -1;
            }
            Thread.Sleep(500);
        }
        if (callbacks != null)
        {
            callbacks.ReportProgress(100, userState, out cancelRequested);
            if (cancelRequested) return -1;
        }
        return n * n;
    }

Overloaded Methods

The most trivial use of overloading in C# is to provide default values for one or more arguments. Typically in this scenario, the method has one main overload which takes all the possible parameters and implements the logic of the operation, and several simple overloads that call the main one and supply default values for some of the arguments. In the following code, for instance, the first overload of DoSomething provides a default value of 17 for the parameter n of the second overload.

C#
using System;
using System.Collections.Generic;
using System.Text;
using AsyncGen;

namespace TestAssembly
{
    [GenerateAsyncClientClass("OverloadedClient")]
    public class Server
    {
        [GenerateAsyncOperation]
        public int DoSomething(int m)
        {
            return DoSomething(m, 17);
        }

        [GenerateAsyncOperation]
        public int DoSomething(int m, int n)
        {
            return 42;
        }
    }
}

When you generate the asynchronous proxy for this class, you would probably want it to contain two methods named DoSomethingAsync—one with a single int argument, the other with two—and a single event named DoSomethingCompleted. However, the above annotation will not yield the expected result. Instead, the resulting code will contain two delegates named doSomethingDelegate, two classes named DoSomethingTracker, and two fields named _dosomethingTracker, and consequently it will fail to compile.

There are two ways to achieve the desired result:

  1. Annotate only the most general overload, and implement the other overloads manually in the proxy class. This is where the partial modifier comes in handy, because it allows you to implement the additional methods in a separate source file. This is the simpler approach, and therefore you should prefer it whenever possible. For example:

    C#
    // OverloadedServer.cs
    
    using System;
    using System.Collections.Generic;
    using System.Text;
    using AsyncGen;
    
    namespace TestAssembly
    {
        [GenerateAsyncClientClass("OverloadedClient")]
        public class OverloadedServer
        {
            public int DoSomething(int m)
            {
                return DoSomething(m, 17);
            }
    
            [GenerateAsyncOperation]
            public int DoSomething(int m, int n)
            {
                return 42;
            }
        }
    }
    
    // OverloadedClient.cs
    
    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace TestAssembly
    {
        public partial class OverloadedClient
        {
            public int DoSomething(int m)
            {
                return DoSomething(m, 17);
            }
    
            public void DoSomethingAsync(in-t m)
            {
                DoSomethingAsync(m, 17);
            }
        }
    }
    
    // OverloadedClient.generated.cs
    
    namespace TestAssembly
    {
        using System;
        using System.ComponentModel;
        using System.Diagnostics;
        using AsyncGen;
    
        public partial class OverloadedClient : 
    	ClientBase<TestAssembly.OverloadedServer>
        {
            // ...
    
            public event AsyncCompletedEventHandler<System.Int32> DoSomethingCompleted
    
            // ...
    
            public int DoSomething(int m, int n)
            {
                // ...
            }
    
            public void DoSomethingAsync(int m, int n)
            {
                // ...
            }
    
            // ...
    
            delegate int DoSomethingDelegate(int m, int n);
        }
    }
  2. Annotate all the overloads; give each overload a different base name, but use the other named properties of the GenerateAsyncOperationAttribute to assign identical names to the start method, the cancel method, and the completion event. For example:

    C#
    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace TestAssembly
    {
        [AsyncGen.GenerateAsyncClientClass("OverloadedClient")]
        public interface IOverloadedServer
        {
            [AsyncGen.GenerateAsyncOperation("op1",
                StartMethodName = "DoSomethingAsync",
                CancelMethodName = "DoSomethingAsyncCancel",
                CompletedEventName = "DoSomethingCompleted")]
            void DoSomething(int m);
    
            [AsyncGen.GenerateAsyncOperation("op2",
                StartMethodName = "DoSomethingAsync",
                CancelMethodName = "DoSomethingAsyncCancel",
                CompletedEventName = "DoSomethingCompleted")]
            void DoSomething(int m, int n);
        }
    }

    ... from which AsyncGen will generate the following client class:

    Generated Client Class

Note that DoSomethingAsync is overloaded, but DoSomethingAsyncCancel is not, and there is only one DoSomethingCompleted event.

When more than one overload of the same method implements complex logic, you must take the second approach.

Caveat: If the overloads have different ref or out arguments or a different return type (although this seems like bad practice), you must name their completion events differently to prevent type conflicts among the different output types.

Synchronous Invocation

The generated proxy also contains a synchronous version of the server's method, without the callback interface parameter. If you want to invoke the operation synchronously, use this method instead of calling the server's method directly. Note that you can still raise progress events, but if you attempt to do this in a typical WinForms application you will run into one of the following problems, depending on the value of the GenerateProgressEventAttribute.Async property:

  1. If you accept the default value (true), the events will be raised after the operation has already completed.
  2. If you set Async to false, you will run into a deadlock.

Still, there are several scenarios where a synchronously-invoked method can safely (and usefully) raise progress events:

  1. In a console application that uses the default, free-threaded, SynchronizationContext.
  2. In a WinForms application, if the method is invoked by a background thread.
  3. In a WinForms application, if the method is invoked by another method, which is, in turn, invoked asynchronously.

This last scenario occurs in practice when you need to invoke an operation that consists of a sequence of stages, each of which has to report its progress to the user, without blocking the main thread.

Lifetime Management

When the proxy and the server communicate with each other via .NET Remoting, both of them are subject to lifetime management. If one of them goes offline before the other one is done using it, you will get the following exception:

"Object ... has been disconnected or does not exist at the server."

To ensure that the server doesn't go offline prematurely, you should set its lifetime limits by doing one of the following:

  • Adding a <lifetime> element to the server's configuration file, for example:

    XML
    <configuration>
       <system.runtime.remoting>
          <application>
             <lifetime leaseTimeout="10M" renewOnCallTime="5M" />
          </application>
       </system.runtime.remoting>
    </configuration>
  • Overriding the method InitializeLifetimeService, inherited from MarshalByRefObject. (Your server must be derived from MarshalByRefObject to be reachable by .NET Remoting.) For example:

    C#
    class Server : MarshalByRefObject
    {
       public override object InitializeLifetimeService()
       {
         ILease tmp = (ILease) base.InitializeLifetimeService();
         if (tmp.CurrentState == LeaseState.Initial)
         {
             tmp.InitialLeaseTime = TimeSpan.FromSeconds(5);
             tmp.RenewOnCallTime = TimeSpan.FromSeconds(1);
          }
          return tmp;
       }
    }

To ensure that the proxy doesn't go offline before the server does, your server should sponsor its proxies. If your server implements the ISponsor interface, it will automatically be registered by the proxy as its sponsor. ISponsor consists of a single method, Renewal, which is called by the framework when the proxy's lease is about to expire. Renewal should return a reasonable amount of time by which to extend the proxy's lease, for example:

C#
class Server : MarshalByRefObject, ISponsor
{
    TimeSpan ISponsor.Renewal(ILease lease)
    {
        return TimeSpan.FromMinutes(2);
    }
}

To learn more about lifetime management, see [1].

The Demo Project

Event-Based Async Client

To see how changes to the annotated types affect the generated code, use TestAssembly.csproj in AsyncGen.sln. Since the Debug settings are not saved in the *.csproj file, you will have to configure the Start Options yourself:

  • In the Solution Explorer pane, right click AsyncGen and choose "Properties."
  • Switch to the Debug tab.
  • Make sure the Start Action is set to "Start project."
  • From the Configuration drop-down list, select "All Configurations."
  • Set the Command Line Arguments to:
    testassembly.dll /language:cs
  • For the Working Directory, click the ellipsis (...) and navigate to:
    ..\..\..\TestAssembly\bin\Debug

Once you complete this configuration, pressing F5 or Ctrl+F5 in Visual Studio will compile TestAssembly.csproj and run AsyncGen.exe on TestAssembly.dll.

To see how the proxy works in a distributed application, open the demo solution, EventBasedAsync.sln. This solution contains three projects:

  • Interface—Contains the definition of the IServer interface as well as the generated Proxy class.
  • Server—A console application that creates a single instance of the Server class, which implements IServer, and then waits for clients to connect to this object via Remoting.
  • Client—The Windows Forms application seen above.

Again, you have to configure some settings which are not saved in the *.sln and *.csproj files:

  • In Solution Explorer, right-click the solution and choose "Set Startup Projects..."
  • Choose "Multiple startup projects."
  • For Client and Server, change the Action to Start. For Interface, set the Action to None.

When you press F5 or Ctrl+F5 in Visual Studio, both the server and the client come up. When you press "Submit Request(s)," the client sends one or more simultaneous requests to the server, depending on the number in the "How many?" box. For each submitted request, a new row is added to the ListView. As the server processes each request, it sends progress reports to the client, and the client displays them using the progress bars embedded in each row. You can cancel any operation which is still in progress by selecting it and choosing "Cancel Selected" from the right-click menu. Finally, you can clear all the cancelled and completed operations from the ListView by choosing "Purge Completed" from the right-click menu.

In addition to demonstrating the functionality of the proxies generated by AsyncGen, this project also demonstrates the behavior of the .NET thread pool. The server provides a primitive command line interface to allow you to configure its thread pool's size limits. The following commands are available:

  • sminc n: Set the minimum number of completion port threads to n.
  • smaxc n: Set the maximum number of completion port threads to n.
  • sminw n: Set the minimum number of worker threads to n.
  • smaxw n: Set the maximum number of worker threads to n.
  • g: Query the current thread pool size limits.
  • q: Quit the server.

In a distributed application, the client's requests are handled by threads from the completion port portion of the server's thread pool, so the number of worker threads has no effect on the performance of the application. If, on the other hand, you move the server into the same AppDomain as the client, the requests will be handled by worker threads. In this case, you should see that the performance is only affected by the number of worker threads and not by the number of completion port threads.

Design and Implementation

AsyncGenLib.dll

Other than the attribute classes described above, AsyncGenLib.dll contains several classes that facilitate the implementation of the generated proxies:

  1. Generic versions of AsyncCompletedEventArgs and AsyncCompletedEventHandler, derived from the non-generic versions defined by the .NET framework. The type parameter TOutput represents a type that holds the output of the operation. If the operation has a single output value (e.g. a return value but no out or ref arguments), the type of this value is substituted for TOutput. If the operation has one or more out or ref arguments, the values of these arguments, along with the method's return value, are packaged into a single value type (typically named DoSomethingOutput, but this can be customized using the GenerateAsyncOperationAttribute.OutputTypeName property), which is substituted for TOutput. If the operation's return type is void and it has no out or ref arguments, the non-generic version of AsyncCompletedEventArgs is used.
  2. Similarly, there are generic versions of ProgressChangedEventArgs and ProgressChangedEventHandler.
  3. ClientBase, a generic base class for the generated proxies. The type parameter TServer represents the type of the original class or interface.
  4. OperationTracker, a base class used internally by the proxy to do most of the heavy lifting (see below). Actually there are two versions of OperationTracker, both of them generic. The more general version takes two type parameters: TDelegate, which represents the signature of the annotated method, and TOutput, which represents its output. The more specialized version takes only the TDelegate parameter, and is used for methods that don't have any output. Having two base classes to choose from helps to simplify the generated code.

How the Client Class is Implemented

For each annotated method, AsyncGen.exe generates two private members in the client class:

  • A nested class, derived from OperationTracker.
  • A field of the type of the nested class, and a statement to initialize it in the constructor.

Internally, each OperationTracker maintains a dictionary of OperationState objects, indexed by the unique Task ID provided by the application when the operation is started. (In order for this to work, one of the parameters of the original method must be annotated by the TaskIDAttribute.) Each OperationState keeps track of one specific invocation. If the start method doesn't have a Task ID parameter, a single dummy object is used instead. In this case, the operation cannot support multiple concurrent invocations.

If a method is overloaded, each overload gets its own tracker class and a corresponding field.  The names of these members are constructed using the base name specified for each overload, so the base names must be different for each method.  (See "Handling Overloaded Methods" above). The program ensures that if multiple overloads specify the same name for any of the following members, only one member will actually be added to the main class:

  • Completion event
  • Cancel method
  • Every kind of progress event (in case multiple overloads specify the same callback interface)

The tracker class, which is private, declares an event for every method in the callback interface which was annotated with the GenerateProgressEventAttribute, and the main class, which is public, declares an identical event and registers a handler on the tracker's event which raises the public event.  Similarly, the main class declares a completion event and registers a handler on the OperationCompleted event of each tracker (inherited from OperationTracker) to raise the public event.

As an elegant alternative to relaying the event through a handler in the main class, it is possible for the main class to define for every public event an add accessor, which immediately registers the handler passed to it on the corresponding event of the proper tracker (or, in the case of an overloaded method, trackers), and a remove accessor, which unregisters the given handler from all the trackers.  The only problem with this implementation is that the System.CodeDom namespace doesn't support events with custom accessors, so code snippets have to be used instead.  Consequently, this implementation is currently available only in C# and in Visual Basic, while the more naïve implementation of the previous paragraph is available in every language that has a CodeDOM provider.  You can switch between the two alternative implementations by selecting either "Debug" or "Debug With Snippets" as the solution configuration.

Points of Interest

.NET Remoting

.NET Remoting supports asynchronous invocation via IMessageSink.AsyncProcessMessage and several similar methods, which are implemented by every stage ("message sink," in Remoting jargon) of the Remoting pipeline, or "sink chain." The same client code can handle both local servers (objects hosted by the local AppDomain) and remote servers (objects hosted by a different AppDomain, process, or machine).

For example, consider the following code:

C#
DoSomethingDelegate d = new DoSomethingDelegate(server.DoSomething);
d.BeginInvoke(new AsyncCallbac(server_DoSomethingCompleted), this);

Normally, BeginInvoke calls ThreadPool.QueueUserWorkItem and passes it a delegate to server.DoSomething. However, if server is a transparent proxy to a remote object, BeginInvoke creates a MethodCall message and passes it to the IMessageSink.AsyncProcessMessage method of the first sink in the proxy's sink chain, which eventually sends it over the Remoting channel to the server. It also registers a reply sink to process the ReturnMessage which will be sent by the server when the operation completes.  The reply sink invokes the AsyncCallback that was passed to BeginInvoke.

It is important to note that the underlying connection will still be synchronous. This means that separate connections will be made for each concurrently running asynchronous call.[1]

In effect, when the server resides in a different process, the operation is performed using the server's thread pool.  On the client side, only one thread per channel is blocked rather than one thread for every concurrent invocation of the operation. The client's thread pool is only used for executing the callbacks. Ideally, the callbacks should return quickly enough to avoid using more than one thread simultaneously, which in .NET 2.0 means they have to return within 500 milliseconds (see .NET's ThreadPool Class - Behind The Scenes by Marc Clifton).

WCF

WCF has an analogous mechanism, but instead of delegates (which are hard-wired to .NET Remoting) you have to use SvcUtil with the /async switch to generates two additional methods in the proxy class:[2]

C#
[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);

Currently AsyncGen doesn't support WCF. Adding such support will require some extensions to AsyncGenLib.dll, and AsyncGen.exe will either have to invoke SvcUtil /async itself or rely on the pre-existence of WCF asynchronous proxies for all the processed types.

If you implement the event-based pattern using WCF, you might also want to take advantage of WCF's built-in support for callback interfaces.

Complete Example

Here is the code generated from the IServer interface declared above:

C#

C#
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.1433
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace TestAssembly
{
    using System;
    using System.ComponentModel;
    using System.Diagnostics;
    using AsyncGen;


    public partial class Client : ClientBase<TestAssembly.IServer>
    {

        private CalculateTracker _calculateTracker;

        public Client(TestAssembly.IServer server) :
                base(server)
        {
            this._calculateTracker = new CalculateTracker(this.server, this);
            this._calculateTracker.OperationCompleted += 
		new AsyncGen.AsyncCompletedEventHandler<double>
		(this._calculateTracker_OperationCompleted);
            this._calculateTracker.CalculationProgressChanged += 
		new AsyncGen.ProgressChangedEventHandler<double>
		(this._calculateTracker_CalculationProgressChanged);
        }

        public event AsyncGen.AsyncCompletedEventHandler<double> CalculationComplete;

        public event AsyncGen.ProgressChangedEventHandler<double> 
		CalculationProgressChanged;

        public double Calculate(double argument, object userState)
        {
            double value;
            this._calculateTracker.CreateOperation(userState);
            try
            {
                value = this.server.Calculate(argument, userState, 
			this._calculateTracker);
            }
            finally
            {
                this._calculateTracker.CompleteOperation(userState);
            }
            return value;
        }

        public void CalculateAsync(double argument, object userState)
        {
            this._calculateTracker.CreateOperation(userState);
            CalculateDelegate d = new CalculateDelegate(this.server.Calculate);
            d.BeginInvoke(argument, userState, this._calculateTracker, 
	       new System.AsyncCallback(this._calculateTracker.PostOperationCompleted), 
		userState);
        }

        public void CalculateAsyncCancel(object userState)
        {
            if (this._calculateTracker.TryCancelOperation(userState))
            {
                return;
            }
            throw new System.ArgumentException();
        }

        private void _calculateTracker_CalculationProgressChanged
		(object sender, AsyncGen.ProgressChangedEventArgs<double> args)
        {
            if ((this.CalculationProgressChanged != null))
            {
                this.CalculationProgressChanged(this, args);
            }
        }

        private void _calculateTracker_OperationCompleted
		(object sender, AsyncGen.AsyncCompletedEventArgs<double> args)
        {
            if ((this.CalculationComplete != null))
            {
                this.CalculationComplete(this, args);
            }
        }

        private class CalculateTracker : OperationTracker<IServer, Client, 
		CalculateDelegate, double>, TestAssembly.IServerCallbacks
        {

            public CalculateTracker(IServer server, Client client) :
                    base(server, client)
            {
            }

            public event AsyncGen.ProgressChangedEventHandler<double> 
		CalculationProgressChanged;

            protected override void CallEndInvoke(CalculateDelegate d, 
		System.IAsyncResult iar, out double output)
            {
                output = d.EndInvoke(iar);
            }

            protected virtual void OnCalculationProgressChanged(object args)
            {
                if ((this.CalculationProgressChanged != null))
                {
                    this.CalculationProgressChanged(this.client, 
			((AsyncGen.ProgressChangedEventArgs<double>)(args)));
                }
            }

            bool IServerCallbacks.ReportCalculationProgress
		(double approximateResult, int percentage, object userState)
            {
                this.PostProgress<double>(new System.Threading.SendOrPostCallback
		(this.OnCalculationProgressChanged), percentage, 
		approximateResult, userState);
                return this.IsOperationCancelled(userState);
            }
        }

        delegate double CalculateDelegate(double argument, object userState, 
		TestAssembly.IServerCallbacks callbacks);
    }
}

Visual Basic

VB.NET
'------------------------------------------------------------------------------
' <auto-generated>
'     This code was generated by a tool.
'     Runtime Version:2.0.50727.1433
'
'     Changes to this file may cause incorrect behavior and will be lost if
'     the code is regenerated.
' </auto-generated>
'------------------------------------------------------------------------------

Option Strict Off
Option Explicit On

Imports TestAssembly
Imports AsyncGen
Imports System
Imports System.ComponentModel
Imports System.Diagnostics

Namespace TestAssembly

    Partial Public Class Client
        Inherits ClientBase(Of IServer)

        Private _calculateTracker As CalculateTracker

        Public Sub New(ByVal server As IServer)
            MyBase.New(server)
            Me._calculateTracker = New CalculateTracker(Me.server, Me)
            AddHandler Me._calculateTracker.OperationCompleted, _
		AddressOf Me._calculateTracker_OperationCompleted
            AddHandler Me._calculateTracker.CalculationProgressChanged, _
		AddressOf Me._calculateTracker_CalculationProgressChanged
        End Sub

        Public Event CalculationComplete As _
		AsyncGen.AsyncCompletedEventHandler(Of Double)

        Public Event CalculationProgressChanged As _
		AsyncGen.ProgressChangedEventHandler(Of Double)

        Public Function Calculate(ByVal argument As Double, _
		ByVal userState As Object) As Double
            Dim value As Double
            Me._calculateTracker.CreateOperation(userState)
            Try
                value = Me.server.Calculate(argument, userState, Me._calculateTracker)
            Finally
                Me._calculateTracker.CompleteOperation(userState)
            End Try
            Return value
        End Function

        Public Sub CalculateAsync(ByVal argument As Double, ByVal userState As Object)
            Me._calculateTracker.CreateOperation(userState)
            Dim d As CalculateDelegate = AddressOf Me.server.Calculate
            d.BeginInvoke(argument, userState, Me._calculateTracker, _
		AddressOf Me._calculateTracker.PostOperationCompleted, userState)
        End Sub

        Public Sub CalculateAsyncCancel(ByVal userState As Object)
            If Me._calculateTracker.TryCancelOperation(userState) Then
                Return
            End If
            Throw New System.ArgumentException
        End Sub

        Private Sub _calculateTracker_CalculationProgressChanged_
		(ByVal sender As Object, ByVal args As _
		AsyncGen.ProgressChangedEventArgs(Of Double))
            RaiseEvent CalculationProgressChanged(Me, args)
        End Sub

        Private Sub _calculateTracker_OperationCompleted(ByVal sender As Object, _
		ByVal args As AsyncGen.AsyncCompletedEventArgs(Of Double))
            RaiseEvent CalculationComplete(Me, args)
        End Sub

        Private Class CalculateTracker
            Inherits OperationTracker(Of IServer, Client, CalculateDelegate, Double)
            Implements IServerCallbacks

            Public Sub New(ByVal server As IServer, ByVal client As Client)
                MyBase.New(server, client)
            End Sub

            Public Event CalculationProgressChanged As _
		AsyncGen.ProgressChangedEventHandler(Of Double)

            Protected Overrides Sub CallEndInvoke(ByVal d As CalculateDelegate, _
		ByVal iar As System.IAsyncResult, ByRef output As Double)
                output = d.EndInvoke(iar)
            End Sub

            Protected Overridable Sub OnCalculationProgressChanged(ByVal args As Object)
                RaiseEvent CalculationProgressChanged(Me.client, CType_
			(args,AsyncGen.ProgressChangedEventArgs(Of Double)))
            End Sub

            Public Function ReportCalculationProgress_
		(ByVal approximateResult As Double, ByVal percentage As Integer, _
		ByVal userState As Object) As Boolean Implements _
		IServerCallbacks.ReportCalculationProgress
                Me.PostProgress(Of Double)(AddressOf Me.OnCalculationProgressChanged, _
			percentage, approximateResult, userState)
                Return Me.IsOperationCancelled(userState)
            End Function
        End Class

        Delegate Function CalculateDelegate(ByVal argument As Double, _
	ByVal userState As Object, ByVal callbacks As IServerCallbacks) As Double
    End Class
End Namespace

C++/CLI

C++
// --- Source file: Client.h ---

#pragma once

#using <mscorlib.dll>

using namespace System::Security::Permissions;
[assembly:SecurityPermissionAttribute(SecurityAction::RequestMinimum, 
	SkipVerification=false)];
namespace TestAssembly {
    using namespace System;
    using namespace System::ComponentModel;
    using namespace System::Diagnostics;
    using namespace AsyncGen;
    using namespace System;
    ref class Client;


    public ref class Client : public ClientBase<TestAssembly::IServer^ >
    {
        private : ref class CalculateTracker;

        private: TestAssembly::Client::CalculateTracker^  _calculateTracker;

        private : delegate System::Double CalculateDelegate
		(System::Double argument, System::Object^  userState, 
		TestAssembly::IServerCallbacks^  callbacks);

        public: event AsyncGen::AsyncCompletedEventHandler
		<System::Double >^  CalculationComplete;

        public: event AsyncGen::ProgressChangedEventHandler
		<System::Double >^  CalculationProgressChanged;

        public: Client(TestAssembly::IServer^  server);

        public: System::Double Calculate
		(System::Double argument, System::Object^  userState);

        public: System::Void CalculateAsync
		(System::Double argument, System::Object^  userState);

        public: System::Void CalculateAsyncCancel(System::Object^  userState);

        private: System::Void _calculateTracker_CalculationProgressChanged
		(System::Object^  sender, 
		AsyncGen::ProgressChangedEventArgs<System::Double >^  args);

        private: System::Void _calculateTracker_OperationCompleted
		(System::Object^  sender, 
		AsyncGen::AsyncCompletedEventArgs<System::Double >^  args);

        private : ref class CalculateTracker : 
		public OperationTracker<IServer^, TestAssembly::Client^, 
		TestAssembly::Client::CalculateDelegate^, System::Double >,
            public TestAssembly::IServerCallbacks
        {
            public: event AsyncGen::ProgressChangedEventHandler
		<System::Double >^  CalculationProgressChanged;

            public: CalculateTracker(IServer^  server, TestAssembly::Client^  client);

            protected: virtual System::Void CallEndInvoke
		(TestAssembly::Client::CalculateDelegate^  d, 
		System::IAsyncResult^  iar, System::Double %output) override;

            protected: virtual System::Void OnCalculationProgressChanged
		(System::Object^  args);

            public: virtual System::Boolean ReportCalculationProgress
		(System::Double approximateResult, System::Int32 percentage, 
		System::Object^  userState) sealed;
        };
    };
}
C++
// --- Source file: Client.cpp ---

#include "StdAfx.h"
#include "Client.h"

namespace TestAssembly
{

    inline Client::Client(TestAssembly::IServer^  server) :
            ClientBase<TestAssembly::IServer^ >(server)
    {
        this->_calculateTracker = (gcnew TestAssembly::Client::CalculateTracker
		(this->server, this));
        this->_calculateTracker->OperationCompleted += 
	  gcnew AsyncGen::AsyncCompletedEventHandler<System::Double >
	  (this, &TestAssembly::Client::_calculateTracker_OperationCompleted);
        this->_calculateTracker->CalculationProgressChanged += 
	  gcnew AsyncGen::ProgressChangedEventHandler<System::Double >(this, 
	  &TestAssembly::Client::_calculateTracker_CalculationProgressChanged);
    }

    inline System::Double Client::Calculate(System::Double argument, 
		System::Object^  userState)
    {
        System::Double __identifier(value);
        this->_calculateTracker->CreateOperation(userState);
        try
        {
            __identifier(value) = this->server->Calculate
		(argument, userState, this->_calculateTracker);
        }
        finally
        {
            this->_calculateTracker->CompleteOperation(userState);
        }
        return __identifier(value);
    }

    inline System::Void Client::CalculateAsync(System::Double argument, 
		System::Object^  userState)
    {
        this->_calculateTracker->CreateOperation(userState);
        TestAssembly::Client::CalculateDelegate^  
		d = gcnew TestAssembly::Client::CalculateDelegate
		((cli::safe_cast<TestAssembly::IServer^  >(this->server)), 
		&TestAssembly::IServer::Calculate);
        d->BeginInvoke(argument, userState, this->_calculateTracker, 
		gcnew System::AsyncCallback(this->_calculateTracker, 
		&TestAssembly::Client::CalculateTracker::PostOperationCompleted), 
		userState);
    }

    inline System::Void Client::CalculateAsyncCancel(System::Object^  userState)
    {
        if (this->_calculateTracker->TryCancelOperation(userState))
        {
            return;
        }
        throw (gcnew System::ArgumentException());
    }

    inline System::Void Client::_calculateTracker_CalculationProgressChanged
	(System::Object^  sender, 
	AsyncGen::ProgressChangedEventArgs<System::Double >^  args)
    {
        this->CalculationProgressChanged(this, args);
    }

    inline System::Void Client::_calculateTracker_OperationCompleted
	(System::Object^  sender, 
	AsyncGen::AsyncCompletedEventArgs<System::Double >^  args)
    {
        this->CalculationComplete(this, args);
    }

    inline Client::CalculateTracker::CalculateTracker
	(IServer^  server, TestAssembly::Client^  client) :
            OperationTracker<IServer^, TestAssembly::Client^, 
		TestAssembly::Client::CalculateDelegate^, 
		System::Double >(server, client)
    {
    }

    inline System::Void Client::CalculateTracker::CallEndInvoke
	(TestAssembly::Client::CalculateDelegate^  d, 
	System::IAsyncResult^  iar, System::Double %output)
    {
        output = d->EndInvoke(iar);
    }

    inline System::Void Client::CalculateTracker::OnCalculationProgressChanged
	(System::Object^  args)
    {
        this->CalculationProgressChanged(this->client, 
	(cli::safe_cast<AsyncGen::ProgressChangedEventArgs
		<System::Double >^  >(args)));
    }

    inline System::Boolean Client::CalculateTracker::ReportCalculationProgress
	(System::Double approximateResult, System::Int32 percentage, 
	System::Object^  userState)
    {
        this->PostProgress<System::Double >
	(gcnew System::Threading::SendOrPostCallback(this, 
	&TestAssembly::Client::CalculateTracker::OnCalculationProgressChanged), 
	percentage, approximateResult, userState);
        return this->IsOperationCancelled(userState);
    }
}

History

  • November 2008: Initial version

References

[1] Ingo Rammer, Advanced .NET Remoting (C# Edition), Apress © 2002

[2] Juval Löwy, Programming WCF Services, O'Reilly © 2007

License

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


Written By
Software Developer (Senior) Philips Healthcare
Israel Israel
I got my B.Sc. in Mathematics and Computer Science from Tel Aviv University in 1997. Since then I have developed software in UNIX, Win32 and .NET. I currently live in Haifa.

Comments and Discussions

 
Questionupdate Pin
kiquenet.com22-Mar-18 4:59
professionalkiquenet.com22-Mar-18 4:59 
AnswerRe: update Pin
Ron A Inbar3-Sep-18 10:04
Ron A Inbar3-Sep-18 10:04 
GeneralMy vote of 5 Pin
Filip D'haene25-May-11 6:29
Filip D'haene25-May-11 6:29 
QuestionHow client Implementation with Async methods and timeout if server logoff Pin
camunoz19-May-09 13:26
camunoz19-May-09 13:26 
AnswerRe: How client Implementation with Async methods and timeout if server logoff Pin
Ron A Inbar31-Oct-09 9:43
Ron A Inbar31-Oct-09 9:43 
GeneralImplementation of the client Pin
Bugnar Tudor13-Jan-09 3:30
Bugnar Tudor13-Jan-09 3:30 
GeneralRe: Implementation of the client Pin
Bugnar Tudor13-Jan-09 5:16
Bugnar Tudor13-Jan-09 5:16 
GeneralRe: Implementation of the client Pin
Ron A Inbar13-Jan-09 5:33
Ron A Inbar13-Jan-09 5:33 
NewsBUG: NullReferenceException is thrown when an operation without a Task ID completes. Pin
Ron A Inbar2-Dec-08 0:02
Ron A Inbar2-Dec-08 0:02 
GeneralRe: BUG: NullReferenceException is thrown when an operation without a Task ID completes. Pin
degree451200230-Oct-09 12:30
degree451200230-Oct-09 12:30 
GeneralRe: BUG: NullReferenceException is thrown when an operation without a Task ID completes. Pin
Ron A Inbar27-Jan-10 3:40
Ron A Inbar27-Jan-10 3:40 
GeneralWell written Pin
rht34122-Nov-08 2:13
rht34122-Nov-08 2:13 
GeneralCouple of things Pin
Dmitri Nеstеruk19-Nov-08 21:55
Dmitri Nеstеruk19-Nov-08 21:55 
GeneralRe: Couple of things [modified] Pin
Ron A Inbar25-Dec-08 7:33
Ron A Inbar25-Dec-08 7:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.