August 2009

Volume 24 Number 08

Foundations - Windows Workflow Design Patterns

By Matthew Milner | August 2009

Contents

Doing Work for N Items of Data
Listen with Timeout
Variation: State Machine
Scatter Gather
Starting Workflow Services with Multiple Operations

Design patterns provide a common, repeatable approach to solving software development tasks, and many different patterns can describe how to accomplish a certain goal in code. When developers begin working with Windows Workflow Foundation (WF), they often ask about how to accomplish common tasks with the technology. This month I discuss several design patterns used in WF.

Doing Work for N Items of Data

Often, workflows are not driven purely by logic but also by data, such as a list of people in an organization or a list of orders, where a set of steps in the workflow needs to execute once for each item. Although perhaps not a pattern in itself, this simple, reusable bit of logic is an important component of the other patterns I discuss in this article. The key to this scenario is using the Replicator activity to iterate over a collection of data and execute the same activities for each item in the collection.

The Replicator activity provides a property for the collection of data items that drives the iterations, events for initializing the child activities for each data item, and conditions to enable you to break out of the execution. Essentially, the Replicator activity provides you with ForEach semantics coupled with DoWhile-style conditional execution.

For example, given a workflow with a property of type List<string> containing employee e-mail addresses, you can iterate over the list and send a message to each employee, as shown in Figure 1.

fig1.gif

Figure 1 Replicator with SendMail Activity

In this scenario, the Replicator activity must have the InitialChildData property bound to a collection implementing the IEnumerable interface that contains the e-mail addresses to be used. These addresses are used to set the recipient's address for each iteration. By handling the ChildInitialized event, you gain access to the data item and the dynamic activity instance that is executed. Figure 2shows how the e-mail address from the collection is passed to the event and can be used to set the RecipientAddress property on the related e-mail activity instance.

Figure 2 Initializing the Child Activity

public List<string> emails = new List<string> {"matt@contoso.com","msdnmag@example.com"}; private void InitChildSendMail(object sender, ReplicatorChildEventArgs e) { SendMailActivity sendMail = e.Activity as SendMailActivity; sendMail.RecipientAddress = e.InstanceData.ToString(); }

The Replicator activity can execute either sequentially or in parallel. In sequential mode, Replicator waits for each iteration to complete before beginning a new iteration. In parallel mode, all activities are initialized and scheduled at the same time, and the execution is much like the Parallel activity, except with the same definition in each branch. Being able to iterate over data items, invoke some actions in parallel, and wait for responses for each item is critical in many design patterns, including several discussed in this article.

Listen with Timeout

In the Listen with Timeout scenario, you have a requirement to wait for some input, but only for a certain amount of time. For example, you might have notified a manager with an e-mail message and need to wait for a reply, but if the manager does not respond within a certain period of time, your workflow should take further action, such as sending a reminder.

The heart of any implementation of this pattern is the Listen activity. The Listen activity allows a workflow to pause and wait for many different events or inputs at the same time. This capability can also be accomplished with the Parallel activity, but the difference is that the Listen activity reacts when the first event occurs and stops listening for all other events, whereas the Parallel activity waits for all events. Combining this functionality with the ability to wait for a designated amount of time, provided by the Delay activity, lets a workflow wait for an event but timeout if the event does not occur. Figure 3shows a Listen activity waiting for messages to arrive via Windows Communication Foundation (WCF) or for the timeout to expire. Notice that the Listen activity can have multiple branches and can therefore be listening for many different events at the same time.

fig3.gif

Figure 3 Listen Activity with Multiple Branches

An implementation like this enables a workflow to wait for a certain amount of time for a response. Typically, if the timeout occurs, the workflow is designed to take appropriate actions. To expand on the manager approval example, once the timeout occurs, the manager should be reminded that she has an outstanding request she needs to approve. After the manager is reminded, the workflow needs to be restored to a state of waiting for the response and the timeout. Surrounding a Listen with a While activity enables the workflow to continue waiting until a certain condition is met. Inside the branches of the Listen activity, the condition is manipulated appropriately to continue waiting or to move on after the response that is wanted is received. In a simple case, a flag can be used to manage the condition, causing the While activity to loop until the flag is set. Thus, when the manager sends a response, the flag can be set and the While activity closes, allowing the workflow to move on to the next activity. In the branch with the Delay activity, after the delay occurs, activities are used to send a reminder to the manager and to ensure that the condition is still set to force the While activity to schedule the child activities again, as shown in Figure 4.

fig04.gif

Figure 4 Listen with While to Send Reminders

In this example, the trigger condition to stop waiting is simply a response from the manager, but, of course, any level of complex evaluation can be done on the input data received. One option is to use a rule set and the Policy activity to determine whether all conditions have been met to move to the next step in the workflow.

Variation: State Machine

One variation on the Listen with Timeout pattern occurs when you develop a State Machine workflow instead of a Sequential workflow. In this case, the State activity takes the place of the Listen activity and provides the ability to listen for multiple events at the same time, including using the Delay activity. In a given state, say, Waiting For Approval, you can model the same scenario as before, where you wait for a response or the timeout. Figure 5shows a sample workflow implementing the same logic as before but using a State Machine workflow.

fig05.gif

Figure 5 State Machine Listening

It is actually simpler to manage the conditions here because there is no need for a While activity. Instead, when the delay occurs, you can send the reminder or take other actions and then transition back to the current state by using the SetState activity, which causes the Delay activity to execute again, resetting the timeout. If the response is received and meets the conditions for continuing, you use the SetState activity to move to the next state. Both branches are shown in Figure 6.

fig06.gif

Figure 6 Listening in a State Activity

Scatter Gather

When you have the need to start many child workflows to do some work, use the Scatter Gather pattern. These workflows might all be doing the same work over different data, or each might be doing different work. The goal is to start all the work, optimize the use of multiple threads to accomplish the tasks faster if possible, and then notify the parent workflow when each task is complete to collect the results.

You can start multiple workflows simply by using the Replicator activity and the InvokeWorkflow activity. The workflows are started asynchronously, which is what you want, but it makes waiting in the parent workflow more challenging because you need a blocking activity that can receive the data back from the child workflows. Using the Receive activity, the parent workflow can wait for each child activity to finish and receive any results back from each workflow that was started. The high-level view of this pattern in the parent workflow is shown in Figure 7.

fig07.gif

Figure 7 Replicator with InvokeWorkfl ow and Receive Activities

The figure makes this pattern look simple to implement, but several key steps are needed to ensure that the child workflows can correctly call back to the parent workflow using WCF. Context information needs to be passed from the parent workflow to each child to enable the child to send data back to the parent workflow, including the workflow instance identifier and the conversation identifier to select the correct Receive activity. Additionally, the parent workflow must be hosted as a WCF service to enable the communication, but it needs to be started using WorkflowRuntime, as shown in Figure 8.

Figure 8 Starting a Workflow That Must Be Hosted as a WCF Service

WorkflowServiceHost host = new WorkflowServiceHost(typeof(MSDN.Workflows. ParentWorkflow)); try { host.Open(); WorkflowRuntime runtime = host.Description.Behaviors. Find<WorkflowRuntimeBehavior>().WorkflowRuntime; WorkflowInstance instance = runtime.CreateWorkflow( typeof(MSDN.Workflows.ParentWorkflow)); instance.Start(); Console.ReadLine(); } catch (Exception ex) { Console.WriteLine(ex); Console.ReadLine(); } finally { if (host != null && host.State == CommunicationState.Opened) host.Close(); else host.Abort(); }

Each child workflow needs to have input parameters for at least the parent workflow ID and the receive activity ID, in addition to any business data that needs to be processed in the child workflow. Parameters to the workflow are defined as public properties on the workflow definition.

The InvokeWorkflow activity allows you to pass parameters to the child workflow and surfaces those properties in the property dialog box. The parameters on the InvokeWorkflow activity can be bound to a property or to a field in the workflow. However, when using the Replicator activity to invoke many workflows, the parameters need to be set in code because each invocation requires unique values; for each iteration, the property or field can be set with the current inputs. Therefore, the parameters on the InvokeWorkflow activity should be bound to fields in the workflow, and those fields will be updated in your code before the child workflow is created.

Your initial inclination might be to set the property during the ChildInitialized event for the Replicator, as I showed with the SendMail example earlier, and this is a good place to start. However, when executing the Replicator activity in parallel mode, all the children are initialized before any instances begin to execute. Therefore, if you set the property in the ChildInitialized event, by the time the InvokeWorkflow activity executes, all instances of the activity would use a single set of data. However, the ChildInitialized event does provide access to the activity instance and the data item driving the iteration. One approach is to collect the data item and store it with a unique identifier so that it can be related to the correct activity instance during execution. Figure 9shows the ChildInitialized event handler for the Replicator activity where the instance data is stored in a dictionary keyed on the unique identifier for the ActivityExecutionContext.

Figure 9 Storing Data During Iterations

private void InitChild(object sender, ReplicatorChildEventArgs e) { InvokeWorkflowActivity startWF = (InvokeWorkflowActivity)e.Activity.GetActivityByName("StartChild Workflow"); InputValueCollection[(Guid)e.Activity.GetValue( Activity.ActivityContextGuidProperty)] = e.InstanceData.ToString(); }

Next, to initialize the InvokeWorkflow activity, you use the Invoking event to set up the parameters. At this point in the execution, all the values needed for input to the child workflow are available. The workflow identifier can be retrieved from the WorkflowEnvironment, and the conversation identifier can be retrieved from the context property on the Receive activity instance. Finally, the business data can be retrieved using the identifier for the current execution context. Figure 10shows the code to initialize the parameter to be passed to the workflow.

Figure 10 Initializing the InvokeWorkflow Activity

private void PrepChildParams(object sender, EventArgs e) { InvokeWorkflowActivity startWf = sender as InvokeWorkflowActivity; ReceiveActivity receive = (ReceiveActivity)startWf.Parent.GetActivityByName( "ReceiveChildCompletion"); Contracts.ChildWFRequest request = new Contracts.ChildWFRequest(); request.WFInstanceID = WorkflowEnvironment.WorkflowInstanceId.ToString(); request.ConversationID = receive.Context["conversationId"]; request.RequestValue = InputValueCollection[(Guid)startWf.Parent.GetValue( Activity.ActivityContextGuidProperty)]; StartWF_Input = request; }

After the child workflow is started, it can begin to execute the work to be done and, on completion, use the Send activity to notify the parent workflow. Before sending the message, the context must be set on the Send activity to ensure that the message gets sent to the correct Receive activity in the parent workflow. Using the values passed from the parent, the context can be correctly set using the BeforeSend event, as shown here.

e.SendActivity.Context = new Dictionary<string, string>{ {"instanceId", InputValues.WFInstanceID}, {"conversationId", InputValues.ConversationID}};

With all these parts in place, the parent workflow starts, and the Replicator activity iterates over the collection of data, starting one child workflow for each item and waiting for a message back from each in parallel. Then, as the child workflows finish, they send a message back to the parent, which can continue processing after all the child workflows have reported back their results. Using this approach, the child workflows can be running at the same time, each with its own thread, providing truly asynchronous processing.

Starting Workflow Services with Multiple Operations

In many samples, workflow services start with a single Receive activity modeling a single operation. In the scenario I've discussed here, you have a need to start the workflow service with more than one method call. That is, the client application might not always invoke the same operation to begin interacting with your workflow, and you need to be able to design the workflow so that it can be started on the basis of multiple operations.

There are actually two different varieties of this pattern, depending on how you want to handle requests after the first request. The first option is to enable the workflow to start with one of several operations and, after that operation is complete, move on with the workflow processing until you define another point where operations can be called. To accomplish this goal, you need to return to the Listen activity and use it as the first activity in your workflow definition. Then, in each branch of the activity, add a Receive activity, configure it with the appropriate service operation information, and bind any necessary parameters for processing, as shown in Figure 11.

fig11.gif

Figure 11 Multiple Receive Activities in a Listen Activity

The crucial step is to ensure that all the Receive activities in the Listen activity have the CanCreateInstance property set to True. This instructs the WCF runtime that if no context information is available on the request indicating an existing workflow instance, it is okay to start a new instance based on the configured operation. Although it might seem slightly odd to create the workflow with an activity other than Receive, the runtime creates the instance, then starts, and only then attempts to send the contents of the WCF message to the Receive activity. In this case, once the workflow starts, both Receive activities are executing and waiting for input.

I mentioned that there are two variations of this pattern. When you use the Listen activity as I did in the previous example, one of the operations starts the workflow, but then the Listen activity completes after that single branch is done and the service is no longer able to receive requests for the other operations modeled in the Listen. This might be exactly what you want in some scenarios, but in others you want the workflow to handle an entire set of operations before it moves on. That is, you know the workflow will receive several requests on different operations in the service contract, but you are not sure which request will be first. In this case, instead of the Listen activity, you can use the Parallel activity with each branch containing a Receive activity with its CanCreateInstance property set to True. This still allows the workflow to start with any operation, but it also keeps the workflow in a state to receive all the other operation calls modeled in the various branches.

Finally, when using a State Machine workflow, you have more flexibility in how the workflow behaves when a particular message is received. Consider a state machine in which the initial state contains several event-driven activities, each with a Receive activity as the starting activity, and each Receive marked to enable creation. Normally, the State activity acts much like the Listen activity, but as the developer, you decide when control moves from the current state to another state. When the Listen activity completes, it closes, and control moves to the next activity in the sequence. With a State activity, after a branch executes, if the workflow does not move to a new state, the workflow remains in the current state and continues to wait for the defined inputs.

To use semantics such as the Listen activity, you must use the SetState activity to move the workflow to the next state when one of the operations is invoked. This usually puts the workflow into a state in which it is waiting for different WCF operations to be invoked. If, on the other hand, you want semantics closer to the Parallel model, where all the operations must be invoked but not in a particular order, then after each Receive activity you have the choice of not changing state or of using the SetState activity to transition back to the same state, a self-transition.

This last option is not entirely like the Parallel activity model in one potentially significant manner. With the Parallel activity, after an operation has been called, it cannot be called again unless modeled somewhere after the Parallel activity. In the state machine model, after the operation is invoked, if the workflow remains in the same state, it can receive messages for the other operations or the original operation. In effect, the state can provide you with semantics similar to a Listen activity in a While activity, waiting for all events, reacting to a single event, and then looping back and waiting for all those same events again.

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

Matt Milner is a member of the technical staff at Pluralsight, where he focuses on connected systems technologies (WCF, Windows Workflow, BizTalk, "Dublin," and the Azure Services Platform). Matt is also an independent consultant specializing in Microsoft .NET application design and development. Matt regularly shares his love of technology by speaking at local, regional, and international conferences such as Tech. Ed. Microsoft has recognized Matt as an MVP for his community contributions around connected systems technology. Contact Matt via his blog: mattmilner.com/blog/.