Advanced Workflow Services Talk (Demo 2 of 4)

A continuation of my series of demos from my advanced workflow services talk.  Here we focus on duplex message exchange patterns.

Duplex messaging is something that we model at the application level (as opposed to the infrastructure level) because we want to model that message exchange at the level of the application.  Here's some scenarios where I could use duplex messaging:

  • [concrete] I submit an order, and you tell me when it ships
  • [abstract] I ask you do to do some long running work, let me know when it is done
  • [abstract] I ask you to start doing something, you update me on the status

One may ask the question, "But, what about the wsHttpDualBinding, or WCF duplex bindings."  That's a valid question, but it's important to point out that those bindings are really used to describe the behavior of a given proxy instance (and associated service artifacts).  When my proxy dies, or the underlying connection goes away, I lose the ability for the service to call back to me.  Additionally, this binds me to listen in the same way that I sent out the initial message. 

By modeling this at the application layer, we do lose some of the "automagicity" of the WCF duplex behavior, but I get more flexibility, and I get the ability to sustain potentially repeated recycling of the services and clients.  Also, you could imagine a service that I call that turns around and calls a third party service.  That third party service could call back directly to the client that made the initial call.  Note, once we start doing duplex communication (and we'll encounter this in part 4, conversations), is that the definition of "client" and "service" become a bit muddier.

 

So, to the code:

 

Ingredients:

  • My service workflow, I listen for three different messages (start, add item, complete ), and then I will send the message back to the client:
  • image
    • You'll note that there is a loop so that we can keep adding items until we eventually get the complete order message and we then exit the loop.
  • A "client" workflow, which will call this service:
  • image
    • You'll note, some of the magic happens here.  After I start, add and complete the order, you'll see that instead of sending messages, I'll now flip around and wait on the receive in order to receive the shipping cost from the service.

 

Details

The first thing that we need to do in order to enable this duplex messaging to occur is that the "client" workflow has to explicitly provide its context token to the service so that the service can address the appropriate instance of the client workflow.

Note, in the real world, you'll probably need to supply more than just the context token, you will need some address and binding information.

Let's look at the contract of the service:

 [ServiceContract(Namespace ="https://microsoft.com/dpe/samples/duplex")]
public interface  IOrderProcessing
{
    [OperationContract()]
    void SubmitOrder(string customerName, IDictionary<string, string> context);

    [OperationContract(IsOneWay = true )]
    void AddItem(OrderItem orderItem);

    [OperationContract(IsOneWay = true )]
    void CompleteOrder();
}

You'll note that on the SubmitOrder method, I pass in a context token.  This is my callback correlation identifier, this is how I will figure out what instance on the client side I want to talk to.  Now, I need to do some work to get the context token in order to send, so let's look at how we do this:

On the client side, on the first Send activity, let's hook the BeforeSend event.

image

 

Let's look at the implementation of GrabToken:

    1:  private void GrabToken(object sender, SendActivityEventArgs e)
    2:  {
    3:      ContextToSend = receiveActivity1.Context;
    4:      Console.WriteLine("Received token to send along");
    5:  }
    6:   
    7:  public static DependencyProperty ContextToSendProperty = DependencyProperty.Register("ContextToSend", typeof(System.Collections.Generic.IDictionary<string, System.String>), typeof(OrderSubmitter.Workflow1));
    8:   
    9:  [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
   10:  [BrowsableAttribute(true)]
   11:  [CategoryAttribute("Parameters")]
   12:  public System.Collections.Generic.IDictionary<string, String> ContextToSend
   13:  {
   14:      get
   15:      {
   16:          return ((System.Collections.Generic.IDictionary<string, string>)(base.GetValue(OrderSubmitter.Workflow1.ContextToSendProperty)));
   17:      }
   18:      set
   19:      {
   20:          base.SetValue(OrderSubmitter.Workflow1.ContextToSendProperty, value);
   21:      }
   22:  }

First, note that on lines 7-22 we declare a dependency property call ContextToSend.  Think of this simply as a bindable storage space.  On line 3, we go and assign to that the value of receiveActivity1.Context.  "But Matt, couldn't I just build a context token off the workflow ID?"  You could, but you're only going to be correct in the "simple scenario."   You can see we then take that ContextToSend, and pass that into the context parameter for the service operation. Always walk up and ask a Receive activity for its context token, don't try to build one on your own.

Now, on the service side, we need to extract that, and we need to apply the value to the send activity in the service workflow that needs to call back.  We basically can do the reverse:

 private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
    //set callback context
    sendActivity1.Context = callbackContext;
}

This is inside a code activity in the first receive activity.  callbackContext is a dependency property that is bound to the inbound context on the Receive activity. 

 

The final trick is that both workflows have to be hosted inside a WorkflowServiceHost.  This makes sense for the "service" workflow, since it will be message activated.  On the client side, we have to do a little bit of work in order to get to the workflow runtime to spin up a workflow instance.  In the early betas, we had an easy way to get to the runtime, WorkflowServiceHost.WorkflowRuntime.  In order to conform more with the extensibility of WCF, this has been moved to the extensions of the service host.  We get there by:

    1:  static void Main(string[] args)
    2:  {
    3:      using (WorkflowServiceHost wsh = new WorkflowServiceHost(typeof(Workflow1)))
    4:      {
    5:          Console.WriteLine("Press <ENTER> to start the workflow");
    6:          Console.ReadLine();
    7:          wsh.Open();
    8:          WorkflowRuntime wr = wsh.Description.Behaviors.Find<WorkflowRuntimeBehavior>().WorkflowRuntime;
    9:          WorkflowInstance wi = wr.CreateWorkflow(typeof(Workflow1));
   10:          AutoResetEvent waitHandle = new AutoResetEvent(false);
   11:          wr.WorkflowCompleted += delegate { waitHandle.Set(); };
   12:          wr.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e) { Console.WriteLine("error {0}", e); waitHandle.Set(); };
   13:          wi.Start();
   14:          waitHandle.WaitOne();
   15:          Console.WriteLine("Workflow Completed");
   16:          Console.ReadLine();
   17:      }
   18:  }

On line 3, you'll see we new up a WorkflowServiceHost based on the service type (it will do this to find and open the respective endpoints).  On line 8, we reach in and grab the WorkflowRuntimeBehavior and get the WorkflowRuntime, and we use that to create an instance of the workflow. 

So, here's what we have done:

  • Figure out how to grab the context token from a Receive activity
  • Modify the contract to explicitly send the "callback info" to the service
  • On the service side, figure out how to grab that and apply it to a Send activity
  • Finally, on the client side, how to manually kick off workflows, rather than waiting for them to be message activated (the usual path we have is the infrastructure creating the workflow instance).