Workflow Communication Spelunking

I just an interesting week taking a crash course in Windows Workflow Foundation (formerly WWF, not just WF) programming. My big hang-up was, how do I communicate into, out of and between activities?

I started with Dennis's explanation of communications into and out of a workflow. The fundamental idea is that, once a workflow has started, you can't talk to it directly. Instead, you set up a communications interface that the workflow can use to talk to the host and that the workflow can use to watch for events from the host, e.g.

[DataExchangeService]
public interface IOrderService {
  void CreateOrder(string customer, string orderDescription);
  event EventHandler<OrderEventArgs> OrderApproved;
}

The interface is marked with the DataExchangeServer attribute to mark it as an interface suitable for host<->workflow communication, but other than that, it's a normal .NET interface. The host, i.e. the chunk of code that creates the WF runtime, implements data exchange service interfaces as singletons, e.g.

class OrderServiceImpl : IOrderService {
  Dictionary<Guid, Order> _workflowOrderMap = new Dictionary<Guid, Order>();

  public void CreateOrder(string customer, string desc) {
    _workflowOrderMap.Add(BatchEnvironment.CurrentInstanceId, new Order(customer, desc));
  }

  public void ApproveOrder(WorkflowInstance wf, string comment) {
    if (OrderApproved != null) {
      Guid wfId = wf.InstanceId;
      OrderApproved(null, new OrderEventArgs(wfId, _workflowOrderMap[wfId], comment));
    }
  }

  public event EventHandler<OrderEventArgs> OrderApproved;
}

With this implementation, the host is allowing the workflow to call the CreateOrder method (which we'll see it do later) and to subscribe to the OrderApproved event. The CreateOrder method uses its arguments to create an Order object and associate it with the ID of the currently executing workflow (available via BatchEnvironment.CurrentInstanceId). Remember, the service implementation is a singleton, but any number of workflows can call it, so when they do, we track information on a per workflow basis.

WF Question #1: How do I associate objects directly w/ a running workflow instead of tracking things in dictionaries?

The OrderApproved event is used to get information into the workflow.

In our scenario, imagine we're creating an order, approving it (w/ a comment) and logging the result. In my sample, I have a workflow imaginatively named "Workflow2" which captures this sequence:

The invoke method activity is bound to the CreateOrder method of the IOrderMethod method:

<InvokeMethodActivity
ID="invokeMethodActivity1"
  MethodName="CreateOrder"
  InterfaceType="EventSinkAndMethodInvoke2.IOrderService">
  <InvokeMethodActivity.ParameterBindings>
    <wcm:ParameterBinding ParameterName="customer" xmlns:wcm="ComponentModel">
      <wcm:ParameterBinding.Value>
        <?Mapping XmlNamespace="System" ClrNamespace="System" Assembly="mscorlib" ?>
        <ns0:String xmlns:ns0="System">Fabrikam</ns0:String>
      </wcm:ParameterBinding.Value>
    </wcm:ParameterBinding>
    <wcm:ParameterBinding ParameterName="orderDescription" xmlns:wcm="ComponentModel">
      <wcm:ParameterBinding.Value>
        <?Mapping XmlNamespace="System" ClrNamespace="System" Assembly="mscorlib" ?>
        <ns0:String xmlns:ns0="System">42" Plasma TV</ns0:String>
      </wcm:ParameterBinding.Value>
    </wcm:ParameterBinding>
  </InvokeMethodActivity.ParameterBindings>
</InvokeMethodActivity>

In this case, we're hard-coding the custom and order description fields, but in a real workflow, you'd take those as input parameters.

WF Question #2: How do you pass input parameters into a workflow?

WF Question #3: Is it legal XML to have a processing instruction in the middle of a file, e.g. <?Mapping...?>?

After the workflow creates the order, it waits for a human to approve it via an event sink activity:

<EventSinkActivity
ID="eventSinkActivity1"
  EventName="OrderApproved"
  InterfaceType="EventSinkAndMethodInvoke2.IOrderService">
  <EventSinkActivity.ParameterBindings>
    <wcm:ParameterBinding ParameterName="Comment" xmlns:wcm="ComponentModel">
      ...
    </wcm:ParameterBinding>
    <wcm:ParameterBinding ParameterName="Order" xmlns:wcm="ComponentModel">
      ...
    </wcm:ParameterBinding>
  </EventSinkActivity.ParameterBindings>
</EventSinkActivity>

The event sink waits for the host to fire an event, which has several interesting bits. The first interesting bit is the parameter names, which are bound to the public properties of the OrderEventArgs class passed to the OrderEvent event:

[Serializable]
public class OrderEventArgs : WorkflowMessageEventArgs {
  Order _order;
  public Order Order { get { return _order; } }

  string _comment;
  public string Comment { get { return _comment; } }

  public OrderEventArgs(Guid workflowInstanceId, Order order, string comment)
: base(workflowInstanceId) {
    _order = order;
    _comment = comment;
  }
}

Notice that the custom OrderEventArgs class derives from the WorkflowMessageEventArgs class and passes in the workflow instance ID. This is required so that the event can be routed to the appropriate workflow. Without it, you'll get the following illuminating error in beta 1:

"An unhandled exception of type 'System.Workflow.Runtime.EventDeliveryFailedException' occurred System.Workflow.Runtime.dll"

WF Question #4: Can we get more descriptive exception messages?

Luckily, this error only happens when you're running under the debugger; it's swallowed completely when your program runs normally.

WF Question #5: Can we get exceptions at runtime, too?

Notice also that the OrderEventArgs class is marked with the Serializable attribute. This is required to cross the boundary into the workflow. Without it, you'll get the ever helpful EventDeliveryFailedException exception.

WF Question #6: What boundary are we crossing when fire an event into a workflow?

Further, all objects sent into a workflow need to be serializable as well, like the Order class (also yielding EventDeliveryFailedException if you forget):

[Serializable]
public class Order {
  Guid _orderId = Guid.NewGuid();
  public Guid OrderId { get { return _orderId; } }

  string _customer;
  public string Customer {
    get { return _customer; }
    set { _customer = value; }
  }

  string _desc;
  public string Description {
    get { return _desc; }
    set { _desc = value; }
  }

  public Order(string customer, string desc) {
    _customer = customer;
    _desc = desc;
  }
}

Firing the event is as easy as calling our helper function from outside of our workflow to cross the boundary into the workflow:

class OrderServiceImpl : IOrderService {
  ...
  public void ApproveOrder(WorkflowInstance wf, string comment) {
    if (OrderApproved != null) {
      Guid wfId = wf.InstanceId;
      OrderApproved(null, new OrderEventArgs(wfId, _workflowOrderMap[wfId], comment));
    }
  }

  public event EventHandler<OrderEventArgs> OrderApproved;
}

Notice that we need the workflow instance ID, which we pass in via the WorkflowInstance we get when starting the workflow:

static void Main() {
  WorkflowRuntime workflowRuntime = new WorkflowRuntime();

  // Add IOrderService implementation
  OrderServiceImpl orderService = new OrderServiceImpl();
  workflowRuntime.AddService(orderService);

  workflowRuntime.StartRuntime();
  workflowRuntime.WorkflowCompleted += OnWorkflowCompleted;

  Type type = typeof(EventSinkAndMethodInvoke2.Workflow2);
  WorkflowInstance wf = workflowRuntime.StartWorkflow(type);

  // Simulate human decision time and approve the order
  System.Threading.Thread.Sleep(1000);
  orderService.ApproveOrder(wf, "this is a *fine* order!");

waitHandle.WaitOne();
  workflowRuntime.StopRuntime();
}

Once the event data is fired into the event, the event sink's parameter binding provide enough infrastructure to be able to access the data in subsequent activities, e.g. the code activity that comes right after the event activity:

<Code ExecuteCode="code1_ExecuteCode" ID="code1" />

In the code1_ExecuteCode method, my code can reach over to the event sink activity's parameter bindings and access the data that was fired into it:

public partial class Workflow2 : SequentialWorkflow {
  void code1_ExecuteCode(object sender, EventArgs e) {
    Console.WriteLine("Order: approved w/ comment= {0}",
eventSinkActivity1.ParameterBindings["Comment"].Value);
}

There are three reasons I really don't like this code. The first is that I have to cast to get something typed out of the Value property and I don't like casting. The second reason is that this technique only works through code; I can't bind to a parameter from an event sink to a property on another activity, e.g. the Error property on a Termination activity. The third, and most damning reason, is because this induces coupling from my code activity to my event sink activity. I don't want this coupling to be captured in code, which is another reason to really like the declarative data binding solution.

WF Question #7: Why can't I bind event sink parameters as input into other activities?

Unfortunately, while I can't solve reasons #2 or #3 very well, I can solve them partially and I can solve #1 nicely by adding some fields to my workflow class:

public partial class Workflow2 : SequentialWorkflow {
  Order _approvedOrder;
  string _approvalComment;

void code1_ExecuteCode(object sender, EventArgs e) {
    Console.WriteLine("Order: approved w/ comment= {0}", _approvalComment);
  }
}

The _approvedOrder and _approvalComment fields can be bound to the event sink parameters like so:

<EventSinkActivity
ID="eventSinkActivity1"
  EventName="OrderApproved"
  InterfaceType="EventSinkAndMethodInvoke2.IOrderService">
  <EventSinkActivity.ParameterBindings>
    <wcm:ParameterBinding ParameterName="Comment" xmlns:wcm="ComponentModel">
      <wcm:ParameterBinding.Value>
        <wcm:ActivityBind Path="_approvalComment" ID="{/Workflow}" />
      </wcm:ParameterBinding.Value>
    </wcm:ParameterBinding>
    <wcm:ParameterBinding ParameterName="Order" xmlns:wcm="ComponentModel">
      <wcm:ParameterBinding.Value>
        <wcm:ActivityBind Path="_approvedOrder" ID="{/Workflow}" />
      </wcm:ParameterBinding.Value>
    </wcm:ParameterBinding>
  </EventSinkActivity.ParameterBindings>
</EventSinkActivity>

Now, when the event sink activity fires, these two workflow fields are populated so that by the time the code activity is executed, they're ready for use, all typed up and ready to go. However, while this reduces coupling to some degree, i.e. the code activity just hopes that somebody provides the data and not that is has to be a specific activity, this is not the same as failing to execute an activity until you have valid values to pass to a function. Instead, it's like setting global variables, hoping they're set properly when the code that accesses them needs them and having no idea what's affected if you have to remove them.

Still, with the parameter binding in place between the event sink parameters and the workplace class fields, I can bind the input to an activity:

<Terminate
Error="*d2p1:ActivityBind(ID={/Workflow};Path=_approvalComment)" ID="terminate1"
  xmlns:d2p1="ComponentModel" />

Clearly, this is binding syntax only a mother could love, but luckily the designer did it for me, it's going to change in the next beta and it does most of what I want, i.e. bind the input of the Error property on the Terminate activity to something produced by another activity. It's not the same as binding the event sink activity parameters directly, thereby allowing me to shed this dirty global variable feeling, but it's oh so close.

The VS05b2, WF/WinFXb1 sample code is available for your enjoyment.

Props to Dennis Pilarinos and Anandhi Somasekaran from the WF group for help figuring this out.