Foundations

Workflow Communications

Matt Milner

Code download available at:  Foundations2007_09.exe(165 KB)

Contents

The Context for Communications
Communicating with the Host
Communicating with Workflow Instances
Two-Way Communications
A Note on Local Communications
Wrap-Up

Workflows are meant to coordinate systems and people, and that means communicating beyond the boundaries of the workflow instance itself. Whether sending e-mail or instant messages (IM) or interacting directly with the user interface in a host process such as ASP.NET or with Web services, communication is a key component of most workflows. This month I will introduce the Windows® Workflow Foundation (WF) communication architecture. Furthermore, I will cover sending data out of a workflow and submitting data to running workflow instances. As a bonus, I will also show you how to send data to a running workflow and get a response.

The Context for Communications

Some developers approaching Windows WF might wonder why a communication architecture even needs to exist. After all, aren't workflows just instances of Microsoft® .NET Framework classes that are being executed? Can't I just call methods on other .NET classes and invoke members on the workflow instance itself? Once you consider how Windows WF manages workflow instances, however, you begin to see that this traditional .NET approach will not work.

It is true that workflows are composed of .NET objects, but they are also managed by the WorkflowRuntime. Workflows model business logic or a business process and may therefore exist logically over the course of several days, being persisted and restored several times. Moreover, workflows are often executed on different threads from the host processing code, meaning that communication with the workflow must take threading into account. Because of these and other considerations, communications in workflows rely on a certain level of indirection that, in the end, provides a much richer framework for interaction.

Communications in Windows WF are often categorized as local (communication between the workflow and objects residing in the same host app) or remote (communication involving an application or service outside of the host process). The same architecture underpins both types of communication, and this article will focus on the core technologies. In a future edition of this column, I will look in more detail at remote communications using Windows WF with Windows Communication Foundation (WCF).

Communicating with the Host

When you have a workflow and you want it to interact with code running in the host application, you need to first get a reference to the object in the host. Windows WF uses the common .NET service provider model for managing communications from workflow instances to objects in the host. (See msdn2.microsoft.com/system.iserviceprovider for more information.) This model requires a service provider, a service, and a consumer, and is used in many different areas of the .NET Framework Base Class Library.

In Windows WF, the WorkflowRuntime is the main service provider and activities are the primary consumers of services. The services themselves are classes you write and load into the WorkflowRuntime. At run time, activities query the service provider for a service and invoke operations on that service as shown in Figure 1.

Figure 1 Communicating with Host Services

Figure 1** Communicating with Host Services **

To see how these pieces fit together, consider a simple notification activity. The purpose of this activity is to alert the interactive user with a message of some sort by presenting it in the user interface. The first step is to build a service interface and a service implementation that can be used by the activity. The interface and class in Figure 2 show a very simple service that can be used by activities to notify the user.

Figure 2 Service to Notify Users

public interface INotificationService
{
    void SendNotification(string notification);
}

public class ConsoleNotificationService : INotificationService
{
    public void SendNotification(string notification)
    {
        Console.WriteLine(notification);
    }
}

Once a service is defined, it needs to be added to the WorkflowRuntime, the service provider. This is usually done in the hosting application when the WorkflowRuntime is created and can be accomplished in code or configuration. Here's an example of adding the ConsoleNotificationService to the runtime in code:

ConsoleNotificationService notificationService = 
    new ConsoleNotificationService();
workflowRuntime.AddService(notificationService);

At this point, the service is hosted within the WorkflowRuntime and is available to consumers that can access the service and interact with it. Most often, you will access these services from your custom activities. In the code download for this article, there is a custom activity named NotifyActivity that acts as the consumer for this service. The activity has one property named Message that identifies the text to use to notify the user. In the Execute method, the activity needs to get a reference to the notification service so that it can call operations on it. The IServiceProvider interface is implemented by all service providers and defines a single method called GetService. The GetService method takes a single parameter of System.Type that identifies the type of service to retrieve. Note that the type you pass in can be the actual type of the service class, or it can be an interface that is implemented by that service.

When writing activities, the key class that allows access to the services is the ActivityExecutionContext, which is passed as a parameter to all methods that the workflow runtime invokes on the activity. ActivityExecutionContext implements the IServiceProvider interface and also provides a generic helper method to remove the need for casting the result from an object to your specific service type. This is the mechanism used by the NotifyActivity as shown here:

INotificationService service = 
    executionContext.GetService<INotificationService>();
service.SendNotification(Message);

The other point in an activity where a service provider is available is in the Initialize method of the activity. In this case, an IServiceProvider parameter is defined on the Initialize method and can be used to get access to services when the activity is first initialized. The reason for the difference is that activities are created and have their Initialize method called when you call CreateWorkflow, but there is no ActivityExecutionContext until the workflow begins executing. Passing in an IServiceProvider allows your activities to interact with services before the workflow begins executing.

Now you've seen the basics of using services to communicate with code in the host, but there are a few important considerations to bear in mind when using this type of communication. The first point to note is that since I've added only one instance of the service class to the runtime, this service acts as a singleton. But generally you'll be running multiple instances of workflows on different threads and they will all be accessing your service. In that case, thread safety quickly becomes an issue. If you are managing state in a service class, you will want to make sure to use appropriate synchronization semantics and potentially manage state for individual workflow instances. If you choose to do the latter, then you can use the System.Workflow.Runtime.WorkflowEnvironment.WorkflowInstanceId property to get the instance id of the current workflow instance that is calling into your code. You can then use that id as an index into a collection of state.

Another consideration related to threading is that code in the service class will run on the same thread the workflow instance is running on. If you are using the DefaultWorkflowScheduler (which you are if you haven't configured something different) then the code is running on a thread that is different from the host thread. The most obvious case where this causes problems is when hosting your workflows inside a Windows Forms application. When the workflow will interact with controls or your form, you must use a Control's Invoke method and delegates where appropriate to make changes to these elements. (For more information on thread-safe programming with Windows Forms, see msdn2.microsoft.com/system.windows.forms.control.invokerequired.)

Finally, notice that the activity code asks the service provider for the service based on an interface instead of the concrete class type. The primary benefit of using interface-based programming in your activities is that it allows the activity to be neutral in regard to the hosting application. This example uses a console-based notification, but suppose you want to reuse this activity in workflows that are hosted in Windows Forms or ASP.NET applications. Because the activity uses the interface, the host application can insert an appropriate implementation of the service for use in the host. In the Windows Forms example, a new class can be written that also implements the INotificationService interface, but instead of doing a Console.WriteLine, the class can show a message box. An ASP.NET implementation might redirect the user to a Web page that shows the message. The key is that the activity is not aware of these issues because they relate to the hosting code and the host is responsible for adding the service to the runtime.

But what if you just want to send a message from your activity, do you really need to use this service model? The short answer is no, you can write code directly in your activity to, for example, send an e-mail message. But flexibility is one key reason you would not put the code directly in your activity. Instead, allow the host to define the implementation details of sending that e-mail message. Perhaps in many environments SMTP would do the trick, but in others the consumer of your activity might want to handle that message differently. By using a service and coding to an interface, you make your activity more flexible. The other main reason you wouldn't use code directly in your activity is if you need to manage state or keep an object alive in memory. Consider a connection to a server or a Web service proxy that you use to communicate with a service. If you have multiple activities in a workflow, or multiple instances of your workflow, that are going to use that connection, you will want to manage it outside of the activity and outside of the workflow. Using a service hosted in the runtime means that the service instance will be around as long as the runtime; you cannot say the same for your workflow instance.

Communicating with Workflow Instances

Besides initiating communication, workflows must also be able to receive communication. The mechanism for receiving communication in a workflow instance is based on an internal queuing system: activities create queues, then register to get notified when data arrives. The host application or a service loaded into the WorkflowRuntime initiates communication with the workflow instance by placing data on the queue as shown in Figure 3.

Figure 3 Queuing Data to the Workflow Instance

Figure 3** Queuing Data to the Workflow Instance **

In a previous article (see msdn.microsoft.com/msdnmag/issues/06/12/WindowsWorkflow), I discussed how activities can create queues and register to get notified when data arrives, so I won't dwell on the activity portion of the process here. Suffice it to say that it is the responsibility of the activity to create the queue and register for notifications. The queue can be created either when the activity is initialized or when it is subscribed/executed, depending on the needs of the activity.

From the host perspective, several issues emerge when submitting information to the workflow instance: knowing the instance id and queue name to submit information to, making sure the workflow instance is loaded, and optionally getting notified when the information has successfully been submitted to the workflow instance. In the case of the workflow instance id, it is the responsibility of the host application or the service loaded into the runtime to be able to determine this information. The id could be part of the data structure that is being exchanged, it could be managed in a context property such as an HTTP cookie or SOAP header, or it could be managed in a separate data store and looked up based on the data being submitted to the workflow. Regardless of the implementation, the host must be able to resolve the id of the workflow in order to get a handle to the instance.

Using the instance id, the host can get a reference to the workflow instance by using the GetWorkflow method on the WorkflowRuntime. This method returns the WorkflowInstance class which is the object used to represent the workflow and send data to it. When you call GetWorkflow, the runtime ensures that the workflow is loaded from the persistence store if necessary, saving you the responsibility of managing this important step.

The second piece of data that is required to communicate with a workflow is the name of the queue to which data is to be delivered. Since a workflow could have several activities waiting for data at one time, it is possible to have several different queues available on a single workflow instance. Rather than use simple string values for queue names, the IComparable interface is used, which allows activities to create complex and unique queue names based on their configuration and type.

The host application code needs to know the name of the queue it is submitting data to, but unlike with the instance id, the workflow itself can provide some help. The WorkflowInstance class provides a GetWorkflowQueueData method that returns a collection of WorkflowQueueInfo objects to provide information about the currently available queues on the workflow instance:

public class WorkflowQueueInfo
{
    public ICollection Items { get; }
    public IComparable QueueName { get; }
    public ReadOnlyCollection<string> 
        SubscribedActivityNames { get; }
 }

As you can see, the information returned from this operation includes not only the name of the queue, but also the list of items already in the queue and the activities which are registered to get notified when data is submitted to the queue. Using this information, it is often possible to resolve the queue name for the queue you desire.

User interface interaction is a key scenario where GetWorkflowQueueData is used. Many applications present users with options as to how they can interact with a process, and need the ability to filter the options based on the current state of the business process. For example, if you are managing an order for a customer, you might only want to show the button used for returns after the item has shipped. Your workflow will likely model this type of behavior with an activity that waits for a request signaling a return. By querying the queue data on a workflow instance, your host application can tailor the user interface to the current state of the business process.

One final note about managing the instance id and the queue names—you can use a runtime service in conjunction with your activity to manage this information. When your activity creates a queue and registers for queue events, it can also register with your service in the runtime and provide the instance id, queue name, and other data that helps the service resolve the keys it needs, as shown in Figure 4. For example, if your activity is interested in data coming from a Web page, it might use the page name and user ID as the key. In your host (ASP.NET), you would call an operation on the runtime service passing in the page name, user ID, and the data to be submitted to the queue. The runtime service could then look up the instance id and queue name based on the page name and user ID, and pass the data to the queue.

Figure 4 Registering with a Runtime Service

Figure 4** Registering with a Runtime Service **

I have mentioned several times now that the host or a service enqueues data to the workflow instance, but haven't yet discussed how this is accomplished. The WorkflowInstance class contains two methods for enqueuing data, as shown here:

public void EnqueueItem(IComparable queueName, object item,
    IPendingWork pendingWork, object workItem);

public void EnqueueItemOnIdle(IComparable queueName, object item, 
    IPendingWork pendingWork, object workItem);

Both methods take the same parameters, including the queue name and the object to be enqueued. (I'll talk about the other two parameters shortly.) At this point, it becomes apparent why the instance id and the queue name are so important. In order to get the WorkflowInstance object, you need the instance id, and in order to enqueue data to that instance, you need the queue name. The difference between the two methods is in how they handle delivering the data to the queue.

The EnqueueItem method attempts to deliver the data to the queue immediately and assumes that the workflow has reached a point where the queue has been created and is awaiting data. In some scenarios, this is exactly what you would like to have happen. In other cases, this type of behavior can create a race condition where your workflow has not yet reached the point where the activity creates the queue you are interested in. You could check this by calling the GetWorkflowQueueData and continuing to check until the queue is there, but this is cumbersome and messy.

The EnqueueItemOnIdle method is intended to address those race conditions. When you use this method to enqueue data, the runtime does not attempt to deliver the data to the queue until the workflow has become idle (when all activities that are currently executing are waiting for input of some sort). This can provide a higher level of certainty to the host application that the workflow is ready for the data being submitted as it is more likely that the activity creating the queue has begun to execute and has in fact created that queue.

Even using EnqueueItemOnIdle, it is still possible that the workflow is waiting for another activity before executing the one you are interested in. For example, consider Figure 5, which shows a delay activity that occurs before a ReceiveData activity (available in the code download for this article). If data is enqueued before the delay, the idle event will occur on the delay, but the queue is not available yet. In this scenario, you might need to use an alternate method for enqueuing data. One possibility is to attempt to enqueue, but if there is a failure, wait for a short time and then attempt to enqueue the data again.

Figure 5 Multiple Idle Points

Figure 5** Multiple Idle Points **

The next big question for a host is how to get notified as to whether an item sent to the queue actually made it to the workflow. You can use two different approaches here that are not mutually exclusive: exceptions and transactions. With the exception approach, you can catch a System.InvalidOperationException when calling either of the methods. If a queue by the name you are using is not found, for example, the exception will tell you just that. The Message property of the exception should provide some meaningful reason why the operation failed (meaningful to a developer, of course, not to your users). Exceptions should always be caught so that the host knows the data did not reach the workflow.

Using transactions allows you to get notified when the message has been successfully delivered to the queue and the workflow has committed the current transaction. This is where those two extra parameters to the EnqueueItem and EnqueueItemOnIdle come into play. The IPendingWork interface identifies a class that can be added to the workflow's work batch and participate in a transaction. The workItem parameter is state or data that the class implementing IPendingWork can use when committing or completing transactions. Figure 6 shows a simple class that implements the IPendingWork interface and can be used to get notified when the data has been delivered to the queue.

Figure 6 Getting Notified of Data Delivery

public class EnqueuePendingWorkItem : IPendingWork
{
    private ManualResetEvent resetEvent;
    private bool result;

    public bool Success
    {
        get { return result; } set { result = value; }
    }

    public void WaitForCommit()
    {
        if (resetEvent != null) resetEvent.WaitOne();
        else throw new InvalidOperationException("Already completed");
    }

    public EnqueuePendingWorkItem()
    {
        resetEvent = new ManualResetEvent(false);
    }

    public void Commit(System.Transactions.Transaction transaction, 
        System.Collections.ICollection items)
    {
       //option to do work here using the workflow's tx
       Console.WriteLine("Commit called on pending work item");
    }

    public void Complete(bool succeeded,  
        System.Collections.ICollection items)
    {
        Success = succeeded;
        resetEvent.Set();
        resetEvent = null;
    }

    public bool MustCommit(System.Collections.ICollection items)
    {
        return true;
    }
}

The IPendingWork interface has three methods that must be implemented by inheritors: Commit, Completed, and MustCommit. The Commit method is where work can be done in the workflow's transaction. The Completed method is the callback to the class to indicate the outcome of the transaction. Finally, the MustCommit method allows the IPendingWork item to indicate if it must currently be committed. I will talk more about the impact of this last method shortly.

To use this class, I add it to my call to the EnqueueItem method and then use the WaitForCommit method to know that the transaction has completed so I can check the success or failure, like so:

EnqueuePendingWorkItem workItem = new EnqueuePendingWorkItem();
instance.EnqueueItemOnIdle(
    "ReceiveData:FirstInput", inputs, workItem, "");
workItem.WaitForCommit();

Because you can take actions within the same transaction as the workflow, you can take steps in your host that will be transactionally consistent with your workflow—especially when you use a persistence service. However, when you submit data to a queue using a transaction, it is important to understand when the transaction will commit and thus when you will know that your data was processed. The table in Figure 7 shows the interaction between using a persistence service, transaction scopes, and the MustCommit method on the IPendingWork item and when the transaction actually commits.

Figure 7 Transaction Committing with Enqueued Data

TransactionScope MustCommit PersistenceService Result
False True False OnIdle
False False False Hang
True True/False False Error
True True/False True EndOfTxScope

The key take away from this grid is that your work item should always return True from the MustCommit method in order to ensure that you don't get into a hang scenario. When you use a persistence service and a transactionscope, then you can force the transaction to commit sooner than when the workflow goes idle. For more information on transactions in workflows, see msdn.microsoft.com/msdnmag/issues/07/06/CuttingEdge.

Two-Way Communications

What you might have noticed in the discussion so far is that while you can call a service operation from your workflow and get a response value, there is no direct way to send data into a workflow and get a response back: enqueuing is a one-way operation. However, you often want to submit data to the workflow, let it process that data, and receive a response that the host application can use. For example, in an ASP.NET application, as part of processing a page, you could submit data to the workflow, wait for a response, and then use the data coming out of the workflow in your page or display logic.

In order to achieve this, you need a way to send an object to a custom activity in the workflow and a means to wait until the activity signals that it has updated the object. The simplest way to manage this is by submitting a message or data structure that provides input to the workflow and has properties for the information you need to get back out. This message should also have a way for your custom activity to signal to the host that it has updated the object with the results.

In the code download for this article, available from the MSDN® Magazine Web site, there is a RequestResponseData activity that shows how to use this type of pattern to submit data to the workflow and wait for a response. Figure 8 shows the TwoWayMessage class that is enqueued by the host, updated by the activity, and then used to signal to the host that the output is ready.

Figure 8 Request- Response Message Class

[Serializable]
public class TwoWayMessage
{
    ManualResetEvent resetEvent;
    bool isResponse;

    public TwoWayMessage()
    {
        resetEvent = new ManualResetEvent(false);
        isResponse = false;
    }

    private Dictionary<string,string> input;
    public Dictionary<string,string> InputValues
    {
        get { return input; } set { input = value; }
    }

    private Dictionary<string,string> output;
    public Dictionary<string,string> OutputValues
    {
        get { return output; } set { output = value; }
    }

    public void SendResponse(Dictionary<string, string> returnValues)
    {
        OutputValues = returnValues;
        this.isResponse = true;
        resetEvent.Set();
    }

    public void WaitForResponse()
    {
        resetEvent.WaitOne();
        resetEvent = null;
    }
}

As you can see, this class allows for input and output values and uses a ManualResetEvent to allow the activity to signal when it has set the response. The caller/host uses the WaitForResponse method to block its thread until the output values are set, while the activity uses the SendResponse method to set the output values and signal the completion. The RequestResponseData activity in the sample code provides the activity implementation for this sample. It is a composite activity that receives data, executes all of its children in sequence, and then sends the response. Figure 9 shows this activity used in a workflow. The code activity contained within the activity represents the logic that sets the output values.

Figure 9 Request-Response

Figure 9** Request-Response **

A Note on Local Communications

The observant reader may notice that I have not discussed what is frequently referred to as local communications in Windows WF. Local communications refers to a set of activities and a runtime service that abstract away much of what I have explained so far. The goal of these activities, along with the ExternalDataExchangeService, the runtime service they interact with, is to provide a simpler .NET development model on top of the communication architecture. As a developer, you use the CallExternalMethod and HandleExternalEvent activities to model the communication and .NET methods and events for communication to and from the host. In some cases, this may be the simplest option for you, but the goal of this article is to help you understand the core communication architecture, not a single abstraction that may prove limiting. There are several examples on the Web of using these activities and of the ExternalDataExchange service layered on the communication architecture discussed here.

Wrap-Up

The communication capabilities of Windows WF are critical to building real-world workflows that interact with systems and people. Understanding the communication architecture can help save you troubleshooting time and allow you to solve difficult problems that do not easily fit the common examples.

Send your questions and comments to mmnet30@microsoft.com.

Matt Milner is an independent software consultant specializing in Microsoft technologies including .NET, Web services, Windows WF, WCF, and BizTalk Server. As an instructor for Pluralsight, Matt teaches courses on Workflow, BizTalk Server, and WCF. Matt lives in Minnesota with his wife Kristen and his two sons.