This section describes how CEMA and HEEA are used, and their internal workings. It describes, in detail, how CEMA and HEEA activities are configured in the WF designer, and how they are bound to custom local services. It also covers the details of CEMA/HEEA correlation and the runtime behavior of the CEMA, HEEA, and local-service interaction.
CEMA is used to invoke services that are not appropriate for use within the context of a running workflow instance. HEEA is used to receive events from an externally completed task. WF does not require these to be used in pairs; but, when they are used in pairs, they provide a powerful “hook” to call external programs. Specifically, they are allowed to call external services with well-defined interfaces. Any services that would block the calling thread for a significant period of time or that would spawn an operation on a separate thread are good candidates for external services.
External services are .NET Framework classes that implement a well-defined, published interface. We will illustrate with the integer-adding service, which implements the well-defined IAddIntegerService interface. At design time, a programmer binds external services to CEMA/HEEA activities by placing their well-defined interfaces in the CEMA/HEEA activity properties. At run time, a service is registered with the WorkflowRuntime during initialization, so that the previously bound WF activities (in our example, CEMA/HEEA) have a service instance from which to consume the services. Thus, the service works like a singleton class with respect to all workflow instances that run within a particular WorkflowRuntime. It will service the needs of all of the runtimes that run workflow instances that contain activities that are bound to the service interface. This is depicted in Figure 3:
Figure 3. Singleton service servicing multiple activities concurrently
Design-Time Binding Between CEMA, HEEA, and the External Service
The CEMA activity is used to invoke a method on a particular service interface. External services advertise themselves by decorating their interfaces with the [ExternalDataExchangeService] attribute (described in detail later). This allows a method of the advertised interface and its associated parameters to be bound to a CEMA instance using the WF designer. Additional attributes are also provided for correlating CEMA/HEEA instances, so that parameters are correctly passed and returned from the correct CEMA instance to the correct HEEA instance (see the following subsection, "Run-Time Binding Between CEMA, HEEA, and External Service Through Correlation").
The HEEA activity is used to receive events from a particular service. Those events could result from the invocation of the service by a particular CEMA instance, or they could be invoked by external applications, like responding to an e-mail or something else. This article focuses on events that result from a CEMA invocation only because we are using CEMA and HEEA as complementary pairs to accomplish multithreaded parallelism.
A concrete implementation of the service interface must be registered with the WorkflowRuntime prior to workflow execution, in order for the bound CEMA and HEEA instances to actually invoke the service at run time. This is depicted at a high level (see Figure 4). The following sections will explore those steps in greater detail. As soon as the CEMA and HEEA are bound to a local-service interface and the service is registered, the workflow can be executed, allowing the CEMA and HEEA to consume the service at run time.
Figure 4. CEMA and HEEA interaction with local service
Run-Time Binding Between CEMA, HEEA, and External Service Through Correlation
Recall that we use CEMA and HEEA as complementary pairs to accomplish the process of “shifting work” to an external thread. To accomplish this, we need a way to tell which CEMA event’s work is correlated to which HEEA event, so that the routing of the completion event to the right HEEA is accomplished correctly. This section describes how this is accomplished.
At any given moment, there could be hundreds of workflow instances being executed by a particular workflow runtime. Some or all of those instances could contain activities that consume the services of a local service through CEMA/HEEA calls. Correlation is the facility that allows a CEMA service call to associate itself with a unique identifier that differentiates it from the hundreds of other running instances. That same unique identifier is, in turn, used to locate the HEEA activity to which the event is subsequently returned. Local services are managed by the WorkflowRuntime, independent of any specific workflow instances. Without correlation, there would be no way for the local service to route calls correctly from a particular CEMA back to events for the corresponding HEEA. The WF correlation facility, described later, provides support for mapping CEMA calls to a local service back to the exact HEEA instance that should receive the resulting event invocation.
CEMA instances are correlated to specific HEEA instances by setting their CorrelationToken property to the same value. That token value is scoped to a specific parent activity and, therefore, must be unique for all activities within that parent. Furthermore, at run time, a correlation identifier must be passed to the external service call by the CEMA and returned by the external event arguments from the ExternalDataExchangeService to HEEA.
As noted earlier, the local service exists outside of the workflow instance. This dictates that workflow-instance correlation is required, because there could be multiple instances of a workflow running simultaneously. We need workflow-instance correlation to identify the specific instance. Therefore, the CEMA must pass the workflow-instance ID out to the local service, and it must be supplied as part of the ExternalDataEventArgs that are passed with the incoming event to the HEEA. The workflow runtime provides support for obtaining the workflow-instance ID from the WorkflowRuntime environment, so explicitly passing the workflow instance in the CEMA call is not required; however, it is required as a property value in the ExternalDataEventArgs that are associated with the returned HEEA event.
The specific external data-service parameters that are used for correlation are designated by the CorrelationInitializer, CorrelationParameter, and CorrelationAlias attributes that are used to decorate the custom external data-exchange service interface that is described in the following subsection, "Service Configuration and Binding." A sample decorated interface is shown in Listing 1. The attributes in the example function are as follows:
- ExternalDataExchange—Tells the workflow runtime that this is an interface that can be used as an ExternalDataExchangeService. It will be described, in detail, in the sections that follow.
- CorrelationParameter—Tells the workflow runtime that the id parameter is the outgoing correlation ID.
- CorrelationAlias—Tells the workflow runtime that the e.Id property of the AddIntegerCompleteEventArgs is an alias for the id correlation ID. This allows the workflow runtime to locate the incoming correlation-ID value on its way back from the ExternalDataExchangeService.
- CorrelationInitializer—Tells the workflow runtime that the Add() method initializes the correlation parameter id that is called out by the CorrelationParameter attribute.
[ExternalDataExchange]
[CorrelationParameter("id")]
public interface IAddIntegerService
{
[CorrelationAlias("id", "e.Id")]
event EventHandler<AddIntegerCompleteEventArgs> AddIntegerComplete;
[CorrelationInitializer]
void Add(string id, int a, int b);
}
Listing 1. Sample ExternalDataExchangeService interface
Therefore, the CorrelationInitializer and CorrelationParameter attributes instruct the workflow runtime where to obtain the outgoing correlation-ID value from the CEMA. In this case, it uses the id parameter of the IAddIntegerService.Add() method. The workflow runtime still needs to know where it will get the correlation-ID value to correlate to the returning HEEA. That information is provided by the CorrelationAlias attribute, which instructs the workflow runtime that the AddIntegerCompleteEventArgs.Id property is an alias for the correlation-ID value.
The workflow runtime will now associate the CorrelationToken of the CEMA with the id parameter to the IAddIntegerService.Add() method call. The implementation of the adder service must then make sure to return that same correlation-ID value in the AddIntegerCompleteEventArgs.Id property when it invokes the event. This will allow the workflow runtime to associate it with the same CorrelationToken value on the HEEA, so that the workflow runtime knows which HEEA should receive the incoming event notification.
Note Under the hood, the correlation allows the WorkflowRuntime to queue the serialized event to the correct WF HEEA Activity queue. Queuing the event allows the scheduler to schedule the HEEA for execution within the single-threaded workflow instance.
This correlation configuration is shown in Figure 5:
Figure 5. Correlation in external data exchange
The CallExternalMethodActivity (CEMA) and HandleExternalEventActivity (HEEA) have the same unique CorrelationToken, which is scoped to a specific activity—usually, the encompassing parent sequence activity. The CEMA invokes the Add() method with the id parameter set to 123. The workflow runtime records that the CorrelationToken “ABC” is associated with the correlation parameter "123". The ExternalDataExchangeService is then free to process the Add() method call as it sees fit, including delegating the work to a thread-pool thread.
As soon as the work is complete, the thread must invoke the AddIntegerComplete event of the IAddIntegerService ExternalDataExchangeService implementation. Furthermore, the AddIntegerCompleteEventArgs must contain the original workflow-instance ID of the workflow instance that invoked the Add() method, and the AddIntegerCompleteEventArgs.Id value must be set to 123. Those values allow the workflow runtime to look up the CorrelationToken value for the correct HEEA, so that it can pass the event back to that specific HEEA activity instance.
Note Once again, the underlying implementation actually queues the event to the unique queue that is associated with the unique HEEA activity instance.
Service Configuration and Binding
The preceding sections described how CEMA and HEEA activities interact with custom local-service implementations. This section describes the steps that are required to implement and initialize a custom local service. There are four steps to using a custom local service:
-
Defining the interface
-
Associating the interface with the CallExternalMethod and HandleExternalEvent activities (CEMA and HEEA)
-
Creating a concrete implementation of the interface
-
Registering an instance of the concrete implementation with the WF WorkflowRuntime
[ExternalDataExchange]
[CorrelationParameter("id")]
public interface IAddIntegerService
{
[CorrelationAlias("id", "e.Id")]
event EventHandler<AddIntegerCompleteEventArgs> AddIntegerComplete;
[CorrelationInitializer]
void Add(string id, int a, int b);
}
Listing 2. Local-service interface
The first step is to define the service interface. The service interface must define a method to be called by the CEMA activity and an event to be handled by the HEEA activity. The interface must then be decorated with the [ExternalDataExchange] attribute for the WF designer to identify it as a service. This is shown in Listing 3.
The code listing also shows the use of the optional correlation attributes. Correlation assures that a CEMA call to an external service will be returned to the correct HEEA activity, in the case where more than one HEEA activity is listening to the same service event in the same workflow. The correlation attributes are used to instruct the WF event listener activities about which fields can be used as lookup keys for unique correlation tokens, which are used to route events back to the unique queue that is associated with a specific HEEA activity. In the preceding example, the [CorrelationParameter] and [CorrelationInitializer] attributes tell WF that the id parameter of the Add() method will be used as the correlation key. The [CorrelationAlias] attribute then tells WF how the id value is mapped into the event arguments that are associated with the returning AddIntegerComplete event. In this case, it says that id is mapped to the AddIntegerEventArgs.Id property.
The attributed interface can be associated with a workflow, as long as it is part of the assembly or referenced assemblies of the workflow project. The interface is associated with the CEMA activity in Microsoft Visual Studio 2005 by using the CEMA activity’s property settings, as shown in Figure 6:
Figure 6. Association of CEMA with service interface
The InterfaceType property allows the user to browse all service interfaces that are attributed with the [ExternalDataExchange] attribute. As soon as the interface is selected, the CEMA property pane also provides support for selecting the service method to call and then binding parameters to the method call, as shown in Figure 6.
The interface is associated with a HEEA activity by using the HEEA activity’s property settings, as shown in Figure 7:
Figure 7. Association of HEEA with service interface
The InterfaceType property allows the user to browse all service interfaces that are attributed with the [ExternalDataExchange] attribute. As soon as the interface is selected, the HEEA property pane also provides support for selecting the service event on which to wait and then binding event parameters to local properties.
A concrete implementation of the IAddIntegerService service interface will be required in order to perform the actual service operation (in this case, Add) and to return the result to the caller. Listing 3 shows a trivial concrete implementation that performs the operation and invokes the returned event:
public class AddIntegerService : WorkflowRuntimeService, IAddIntegerService
{
public event EventHandler<AddIntegerCompleteEventArgs> AddIntegerComplete;
public void Add(string id, int a, int b)
{
int result = a + b;
AddIntegerComplete(null, new AddIntegerCompleteEventArgs(
WorkflowEnvironment.WorkflowInstanceId, id, a, b, result));
}
}
Listing 3. Concrete local-service implementation
The AddIntegerService in Listing 4 shows a more complete implementation that delegates the work to a separate thread-pool thread:
namespace MultithreadedParallelism
{
[ExternalDataExchange]
[CorrelationParameter("id")]
public interface IAddIntegerService
{
[CorrelationAlias("id", "e.Id")]
event EventHandler<AddIntegerCompleteEventArgs> AddIntegerComplete;
[CorrelationInitializer]
void Add(string id, int a, int b);
}
[Serializable]
public class AddIntegerCompleteEventArgs : ExternalDataEventArgs
{
private string id;
private int a;
private int b;
private int result;
public AddIntegerCompleteEventArgs(Guid instanceId, string id, int a, int b, int result) : base(instanceId)
{
this.Id = id;
this.A = a;
this.B = b;
this.Result = result;
}
// Get and set public properties for these member variable elided for brevity
}
public class AddIntegerService : WorkflowRuntimeService, IAddIntegerService
{
#region IAddIntegerService Members
public event EventHandler<AddIntegerCompleteEventArgs> AddIntegerComplete;
public void Add(string id, int a, int b)
{
ThreadPool.QueueUserWorkItem(Add, new AddArgs(WorkflowEnvironment.WorkflowInstanceId, id, a, b));
}
#endregion
private void Add(object state)
{
AddArgs args = state as AddArgs;
Console.WriteLine("\tAdding {0} + {1} on thread {2} at {3}", args.A, args.B,
Thread.CurrentThread.ManagedThreadId,
DateTime.Now.ToString("HH:mm:ss.fff"));
AddIntegerComplete(null, new AddIntegerCompleteEventArgs(args.InstanceId, args.Id, args.A, args.B, args.A +
args.B));
WorkflowManager.WorkflowRuntime.GetService<ManualWorkflowSchedulerService>().RunWorkflow
(args.InstanceId);
}
}
internal class AddArgs
{
private Guid instanceId;
private string id;
private int a;
private int b;
public AddArgs(Guid instanceId, string id, int a, int b)
{
InstanceId = instanceId;
Id = id;
A = a;
B = b;
}
// Get and set public properties for these member variable elided for brevity
}
}
Listing 4. A simple AddInteger service
The final step in service registration is to register the concrete implementation with the WF WorkflowRuntime in your workflow program. This is done during the initialization of the WorkflowRuntime instance, which you will use to run workflows that contain CEMA/HEEA activities. An example initialization, which uses our example AddIntegerService, is shown in Listing 5:
class Program
{
static void Main(string[] args)
{
WorkflowRuntime workflowRuntime = WorkflowManager.WorkflowRuntime;
ExternalDataExchangeService edes = new ExternalDataExchangeService();
workflowRuntime.AddService(edes);
edes.AddService(new AddIntegerService());
AutoResetEvent waitHandle = new AutoResetEvent(false);
workflowRuntime.WorkflowCompleted +=
delegate(object sender, WorkflowCompletedEventArgs e) {waitHandle.Set();};
workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e)
{
Console.WriteLine(e.Exception.Message);
waitHandle.Set();
};
waitHandle.Reset();
WorkflowInstance instance =
workflowRuntime.CreateWorkflow(typeof (MultithreadedParallelism.SimpleSleepParallel));
instance.Start();
waitHandle.WaitOne();
}
}
Listing 5. Registering local service with WorkflowRuntime
The first step is to create a new instance of the ExternalDataExchangeService service and an instance of your concrete service, AddIntegerService. You then associate your concrete service implementation with the ExternalDataExchangeService by passing your concrete service to the AddService() method of the ExternalDataExchangeService. This registration allows the ExternalDataExchangeService to add the concrete implementation to a data structure that allows the concrete service to be retrieved by its interface type (in this case, IAddIntegerService) at run time. The ExternalDataExchangeService also must be associated with the WorkflowRuntime instance by passing the ExternalDataExchangeService to the AddService() method of the WorkflowRuntime. The listing shows an alternate order of initialization; either order is acceptable. You are ready to execute workflows that contain your CEMA/HEEA activities, as soon as the concrete implementation is registered with the ExternalDataExchangeService, which is, in turn, registered with the WorkflowRuntime.
CEMA and HEEA Under the Hood
Figure 8 depicts the actions that happen behind the scenes of a CEMA/HEEA invocation. The CEMA uses reflection to invoke the configured method of the local service. In our case, the local service then delegates its work to a thread from the pool. The thread then signals the event that is associated with the local service. This results in the invocation of the HEEA event handler, which had similarly used reflection to associate its invocation with a particular event that is exposed by the local service.
Figure 8. CEMA and HEEA under the hood
The pseudocode in Listing 6 and Listing 7 CNDJ6nn5us4RjIIAqgBLqQsCAAAACAAAAA4AAABfAFIAZQBmADEANgAyADcANgA5ADMANgA3AAAA
AA==
REF _Ref162769367 \h provides more details of the plumbing that the workflow library and runtime provide under the hood to execute the CEMA and the HEEA.
When the CEMA executes, it uses its InterfaceType property, which is set to IAddIntegerService, to obtain a reference to the associated concrete service. The concrete service is registered with the workflow runtime, so that the CEMA obtains a reference to it by using the WorkflowRuntime.GetService() method.
The CEMA then uses its MethodName property to obtain the MethodInfo information, via reflection, from the interface reference. As soon as the CEMA has the MethodInfo information, it can then invoke the method on the interface by using reflection. It passes the parameters that were configured in its property settings (in this case, id, a, and b).
Workflow runtime executes CEMA
IAddIntegerService adder = WorkflowRuntime.GetService<IAddIntegerService>() ;
MethodInfo mi = (adder.GetType()).GetMethod(MethodName);
mi.Invoke(adder, new object[] { method_parameters }) ;
Now within the scope of the AddIntegerService
ThreadPool.QueueUserWorkItem(AddTwoIntegers, new AddIntegerState(instance id, correlation id, first_int, second_int)) ;
Now returned back to the calling CEMA
return ActivityExecutionStatus.Closed ;
WorkflowRuntime resumes execution: next activity in queue is HEEA
CreateWorkflowQueueForExpectedEvent() ;
ContinueAt(ReceivedExternalDataEvent) ;
return ActivityExecutionStatus.Executing ;
Listing 6. Pseudocode for main thread CEMA invocation and HEEA setup
The AddIntegerService simply delegates all work to a separate thread-pool thread by passing the method parameters and a delegate to the ThreadPool.QueueUserWorkItem() method, which queues the work for a separate thread and returns immediately to the AddIntegerService, which returns immediately back to the CEMA.
The CEMA has completed its work, having invoked the external service, so now it returns “Closed” to the workflow runtime to report that it has completed.
In Listing 7, we follow the path of execution of the worker thread for which we previously queued work:
ThreadPool assigns the next free thread to our queued task
result_int = first_int + second_int ;
FireExternalDataEvent(new AddCompleteEventArgs(instance id, correlation id, first_int, second_int, result_int)) ;
WorkflowRuntime resumes the HEEA after queuing the event for its consumption: next activity in queue is HEEA
int result = (e as AddIntegerCompleteEventArgs).Result ;
Listing 7. Pseudocode for HEEA invocation from separate worker thread
The thread-pool thread invokes the queued delegate, which performs the actual work—in this case, adding two integers together.
As soon as the work is completed, we have to inform the HEEA, which is done by firing our “complete event,” which will get picked up by the HEEA correlation code, which uses the correlation-ID information in the event to place the event on the correct HEEA queue. The HEEA is also scheduled for execution with the workflow runtime.
The HEEA resumes execution, at which time it removes the event from its queue and stores it in its event property. The HEEA then calls its configured “Invoked” delegate, which is free to process the event that is stored in the properties. This entire process is depicted in Figure 8.