November 2009

Volume 24 Number 11

Foundations - Workflow Services for Local Communication

By Matt Milner | November 2009

In a previous column (see “Workflow Communications” in the September 2007 issue of MSDN Magazine at msdn.microsoft.com/magazine/cc163365.aspx), I wrote about the core communication architecture in Windows Workflow Foundation 3 (WF3). One topic I did not cover is the local communications activities that are one abstraction on top of this communication architecture. If you look at .NET Framework 4 Beta 1, you will notice no HandleExternalEvent activity. In fact, with WF4, the communications activities included are built on Windows Communication Foundation (WCF). This month, I’ll show you how to use WCF for communication between a workflow and a host application in Windows Workflow Foundation 3. Gaining this knowledge should help with your development efforts using WF3 and prepare you for WF4, where WCF is the only abstraction over queues (referred to as “bookmarks” in WF4) that ships with the framework. (For basic information on Workflow Services in WF3, see my Foundations column in the Visual Studio 2008 Launch issue of MSDN Magazine, at msdn.microsoft.com/magazine/cc164251.aspx.)

Overview

Communication between host applications and workflows proves challenging for some developers because they can easily overlook the fact that the workflow and host often execute on different threads. The design of the communication architecture is intended to shield developers from having to worry about managing thread context, marshaling data and other low-level details. One abstraction over the queuing architecture in WF is the WCF messaging integration that was introduced in Version 3.5 of the .NET Framework. Most examples and labs show how the activities and extensions to WCF can be used to expose a workflow to clients that are external to the hosting process, but this same communication framework can be used to communicate within the same process.

Implementing the communication involves several steps, but the work does not amount to much more than what you would have to do with the local communication activities.

Before you can do anything else, you need to define (or minimally begin to define in an iterative approach) the contracts for communication using WCF service contracts. Next, you need to use those contracts in your workflows to model the communication points in the logic. Finally, to hook it all together, the workflow and other services need to be hosted as WCF services with endpoints configured.

Modeling Communication

The first step in modeling communication is to define the contracts between your host application and the workflow. WCF services use contracts to define the collection of operations that make up the service and the messages that are sent and received. In this case, because you are communicating from the host to the workflow and from the workflow to the host, you need to define two service contracts and related data contracts, as shown in Figure 1.

Figure 1 Contracts for Communication

[ServiceContract(
    Namespace = "urn:MSDN/Foundations/LocalCommunications/WCF")]
public interface IHostInterface
{
[OperationContract]
void OrderStatusChange(Order order, string newStatus, string oldStatus);
}

[ServiceContract(
    Namespace="urn:MSDN/Foundations/LocalCommunications/WCF")]
public interface IWorkflowInterface
{
    [OperationContract]
    void SubmitOrder(Order newOrder);

    [OperationContract]
    bool UpdateOrder(Order updatedOrder);
}

[DataContract]
public class Order
{
    [DataMember]
    public int OrderID { get; set; }
    [DataMember]
    public string CustomerName { get; set; }
    [DataMember]
    public double OrderTotal { get; set; }
    [DataMember]
    public string OrderStatus { get; set; }
    }

With the contracts in place, modeling the workflow using the Send and Receive activities works as it does for remote communication. That is one of the beautiful things about WCF: remote or local, the programming model is the same. As a simple example, Figure 2 shows a workflow with two Receive activities and one Send activity modeling the communication between the workflow and the host. The receive activities are configured with the IWorkflowInterface service contract, and the Send activity uses the IHostInterface contract.

So far, using WCF for local communications is not much different from using WCF for remote communications and is very similar to using the local communications activities and services. The main difference comes in how the host code is written to start the workflow and handle communication coming from the workflow.

Figure 2 Workflow Modeled Against Contracts

Hosting the Services

Because we want communication to flow both ways using WCF, we need to host two services—the workflow service to run the workflow and a service in the host application to receive messages from the workflow. In my example, I built a simple Windows Presentation Foundation (WPF) application to act as the host and used the App class’s OnStartup and OnExit methods to manage the hosts. Your first inclination might be to create the WorkflowServiceHost class and open it right in the OnStartup method. Since the Open method does not block after the host is open, you can continue processing, load the user interface and begin interacting with the workflow. Because WPF ( and other client technologies) uses a single thread for processing, this soon leads to problems because both the service and the client call cannot use the same thread, so the client times out. To avoid this, the WorkflowServiceHost is created on another thread using the ThreadPool, as shown in Figure 3.

Figure 3 Hosting the Workflow Service

ThreadPool.QueueUserWorkItem((o) =>
{

//host the workflow
workflowHost = new WorkflowServiceHost(typeof(
    WorkflowsAndActivities.OrderWorkflow));
workflowHost.AddServiceEndpoint(
    "Contracts.IWorkflowInterface", LocalBinding, WFAddress);
try
{
    workflowHost.Open();
}
catch (Exception ex)
{
    workflowHost.Abort();
    MessageBox.Show(String.Format(
        "There was an error hosting the workflow as a service: {0}",
    ex.Message));
}
});

The next challenge you encounter is choosing the appropriate binding for local communication. Currently, there is no in-memory or in-process binding that is extremely lightweight for these kinds of scenarios. The best option for a lightweight channel is to use the NetNamedPipeBinding with security turned off. Unfortunately, if you try to use this binding and host the workflow as a service, you get an error informing you that the host requires a binding with the Context channel present because your service contract may require a session. Further, there is no NetNamedPipeContextBinding included with the .NET Framework, which ships with only three context bindings: BasicHttpContextBinding, NetTcpContextBinding and WSHttpContextBinding. Fortunately, you can create your own custom bindings to include the context channel. Figure 4 shows a custom binding that derives from the NetNamedPipeBinding class and injects the ContextBindingElement into the binding. Communication in both directions can now use this binding in the endpoint registration by using different addresses.

Figure 4 NetNamedPipeContextBinding

public class NetNamedPipeContextBinding : NetNamedPipeBinding
{
    public NetNamedPipeContextBinding() : base(){}

    public NetNamedPipeContextBinding(
        NetNamedPipeSecurityMode securityMode):
        base(securityMode) {}

    public NetNamedPipeContextBinding(string configurationName) :
        base(configurationName) {}

    public override BindingElementCollection CreateBindingElements()
    {
        BindingElementCollection baseElements = base.CreateBindingElements();
        baseElements.Insert(0, new ContextBindingElement(
            ProtectionLevel.EncryptAndSign,
            ContextExchangeMechanism.ContextSoapHeader));

        return baseElements;
    }
}

With this new binding, you can create an endpoint on the WorkflowServiceHost and open the host with no more errors. The workflow is ready to receive data from the host using the service contract. To send that data, you need to create a proxy and invoke the operation, as shown in Figure 5.

Figure 5 Host Code to Start a Workflow

App a = (App)Application.Current;
    IWorkflowInterface proxy = new ChannelFactory<IWorkflowInterface>(
    a.LocalBinding, a.WFAddress).CreateChannel();

    proxy.SubmitOrder(
        new Order
        {
            CustomerName = "Matt",
            OrderID = 0,
            OrderTotal = 250.00
        });

Because you’re sharing the contracts, there is no proxy class, so you have to use the ChannelFactory<TChannel> to create the client proxy.

While the workflow is hosted and ready to receive messages, it still needs to be configured to send messages to the host. Most important, the workflow needs to be able to get a client endpoint when using the Send activity. The Send activity allows you to specify the endpoint name, which is typically a mapping to a named endpoint in the configuration file. Although putting the endpoint information in a configuration file works, you can also use the ChannelManagerService (as discussed in my August 2008 column at msdn.microsoft.com/magazine/cc721606.aspx) to hold the client endpoints used by your Send activities in the workflow. Figure 6 shows the hosting code to create the service, provide it with a named endpoint, and add it to the WorkflowRuntime hosted in the WorkflowServiceHost.

Figure 6 Adding the ChannelManagerService to the Runtime

ServiceEndpoint endpoint = new ServiceEndpoint
(
    ContractDescription.GetContract(typeof(Contracts.IHostInterface)),
        LocalBinding, new EndpointAddress(HostAddress)
);
endpoint.Name = "HostEndpoint";

WorkflowRuntime runtime =
    workflowHost.Description.Behaviors.Find<WorkflowRuntimeBehavior>().
WorkflowRuntime;

ChannelManagerService chanMan =
    new ChannelManagerService(
        new List<ServiceEndpoint>
        {
            endpoint
        });

runtime.AddService(chanMan);

Having the workflow service hosted provides the ability to send messages from the host to the workflow, but to get messages back to the host, you need a WCF service that can receive messages from the workflow. This service is a standard WCF service self-hosted in the application. Because the service is not a workflow service, you can use the standard NetNamedPipeBinding or reuse the NetNamedPipeContextBinding shown previously. Finally, because this service is invoked from the workflow, it can be hosted on the UI thread, making interaction with UI elements simpler. Figure 7 shows the hosting code for the service.

Figure 7 Hosting the Host Service

ServiceHost appHost = new ServiceHost(new HostService());
appHost.AddServiceEndpoint("Contracts.IHostInterface",
LocalBinding, HostAddress);

try
{
    appHost.Open();
}
catch (Exception ex)
{
    appHost.Abort();
    MessageBox.Show(String.Format(
        "There was an error hosting the local service: {0}",
    ex.Message));
}

With both services hosted, you can now run the workflow, send a message and receive a message back. However, if you try to send a second message using this code to the second receive activity in the workflow, you will receive an error about the context.

Handling Instance Correlation

One way to handle the context problem is to use the same client proxy for every invocation of the service. This enables the client proxy to manage the context identifiers (using the NetNamedPipeContextBinding) and send them back to the service with subsequent requests.

In some scenarios, it’s not possible to keep the same proxy around for all requests. Consider the case where you start a workflow, persist it to a database and close the client application. When the client application starts up again, you need a way to resume the workflow by sending another message to that specific instance. The other common use case is when you do want to use a single client proxy, but you need to interact with several workflow instances, each with a unique identifier. For example, the user interface provides a list of orders, each with a corresponding workflow, and when the user invokes an action on a selected order, you need to send a message to the workflow instance. Letting the binding manage the context identifier will not work in this scenario because it will always be using the identifier of the last workflow with which you interacted.

For the first scenario—using a new proxy for each call—you need to manually set the workflow identifier into the context by using the IContextManager interface. IContextManager is accessed through the GetProperty<TProperty> method on the IClientChannel interface. Once you have the IContextManager, you can use it to get or set the context.

The context itself is a dictionary of name-value pairs, the most important of which is the instanceId value. The following code shows how you retrieve the ID from the context so it can be stored by your client application for later, when you need to interact with the same workflow instance. In this example, the ID is being displayed in the client user interface rather than being stored in a database:

IContextManager mgr = ((IClientChannel)proxy).GetProperty<IContextManager>();
      
string wfID = mgr.GetContext()["instanceId"];
wfIdText.Text = wfID;

Once you make the first call to the workflow service, the context is automatically populated with the instance ID of the workflow by the context binding on the service endpoint.

When using a newly created proxy to communicate with a workflow instance that was previously created, you can use a similar method to set the identifier in the context to ensure your message is routed to the correct workflow instance, as shown here:

IContextManager mgr = ((IClientChannel)proxy).GetProperty<IContextManager>();
  mgr.SetContext(new Dictionary<string, string>{
    {"instanceId", wfIdText.Text}
  });

When you have a newly created proxy, this code works fine the first time but not if you try to set the context a second time for invoking another workflow instance. The error you get tells you that you cannot change the context when automatic context management is enabled. Essentially, you are told that you can’t have your cake and it eat too. If you want the context to be managed automatically, you can’t manipulate it manually. Unfortunately, if you want to manage the context manually, you fail to get automatic management, which means you cannot retrieve the workflow instance ID from the context as I showed previously.

To deal with this mismatch, you handle each case separately. For the initial call to a workflow, you use a new proxy, but for all subsequent calls to an existing workflow instance, you use a single client proxy and manage the context manually.

For the initial call, you should use a single ChannelFactory<TChannel> to create all the proxies. This results in better performance because the creation of the ChannelFactory has some overhead you do not want to duplicate for every first call. Using code like that shown earlier in Figure 5, you can use a single ChannelFactory<TChannel> to create the initial proxy. In your calling code, after using the proxy, you should follow the best practice of calling the Close method to release the proxy.

This is standard WCF code for creating your proxy using the channel factory method. Because the binding is a context binding, you get automatic context management by default, which means you can extract the workflow instance identifier from the context after making the first call to the workflow.

For making subsequent calls, you need to manage the context yourself, and this entails using WCF client code that is not as frequently used by developers. To set the context manually, you need to use an OperationContextScope and create the MessageContextProperty yourself. The MessageContextProperty is set on the message as it is being sent, which is equivalent to using the IContextManager to set the context, with the exception that using the property directly works even when the context management is disabled. Figure 8 shows the code to create the proxy using the same ChannelFactory<TChannel> that was used for the initial proxy. The difference is that in this case, the IContextManager is used to disable the automatic context management feature and a cached proxy is used rather than creating a new one on each request.

Figure 8 Disabling Automatic Context Management

App a = (App)Application.Current;

if (updateProxy == null)
{
    if (factory == null)
        factory = new ChannelFactory<IWorkflowInterface>(
            a.LocalBinding, a.WFAddress);

        updateProxy = factory.CreateChannel();
        IContextManager mgr =
            ((IClientChannel)updateProxy).GetProperty<IContextManager>();
        mgr.Enabled = false;
        ((IClientChannel)updateProxy).Open();
}

Once the proxy is created, you need to create an OperationContextScope and add the MessageContextProperty to the outgoing message properties on the scope. This enables the property to be included on the outgoing messages during the duration of the scope. Figure 9 shows the code to create and set the message property using the OperationContextScope.

Figure 9 Using OperationContextScope

using (OperationContextScope scope =
    new OperationContextScope((IContextChannel)proxy))
{
    ContextMessageProperty property = new ContextMessageProperty(
        new Dictionary<string, string>
        {
            {“instanceId”, wfIdText.Text}
        });

OperationContext.Current.OutgoingMessageProperties.Add(
    "ContextMessageProperty", property);

proxy.UpdateOrder(
    new Order
        {
            CustomerName = "Matt",
            OrderID = 2,
            OrderTotal = 250.00,
            OrderStatus = "Updated"
        });
}

This might seem like quite a bit of work just to talk between the host and the workflow. The good news is that much of this logic and the management of identifiers can be encapsulated in a few classes. However, it does involve coding your client in a particular way to ensure that the context is managed correctly for those cases in which you need to send more than one message to the workflow instance. In the code download for this article, I have included a sample host for a workflow using local communications that attempts to encapsulate much of the complexity, and the sample application shows how to use the host.

A Word About User Interface Interaction

One of the main reasons you send data from the workflow to the host is that you want to present it to a user in the application interface. Fortunately, with this model you have some options to take advantage of user interface features, including data binding in WPF. As a simple example, if you want your user interface to use data binding and update the user interface when data is received from the workflow, you can bind your user interface directly to the host’s service instance.

The key to using the service instance as the data context for your window is that the instance needs to be hosted as a singleton. When you host the service as a singleton, you have access to the instance and can use it in your UI. The simple host service shown in Figure 10 updates a property when it receives information from the workflow and uses the INotifyPropertyChangedInterface to help the data binding infrastructure pick up the changes immediately. Notice the ServiceBehavior attribute indicating that this class should be hosted as a singleton. If you look back to Figure 7, you can see the ServiceHost instantiated not with a type but with an instance of the class.

Figure 10 Service Implementation with INotifyPropertyChanged

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
internal class HostService : IHostInterface, INotifyPropertyChanged
{
    public void OrderStatusChange(Order order, string newStatus,
        string oldStatus)
    {
        CurrentMessage = String.Format("Order status changed to {0}",
            newStatus);
    }

private string msg;

public string CurrentMessage {
get { return msg; }
set
    {
        msg = value;
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(
                "CurrentMessage"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

To databind to this value, the DataContext of the window, or a particular control in the window, can be set with the instance. The instance can be retrieved by using the SingletonInstance property on the ServiceHost class, as shown here:

HostService host = ((App)Application.Current).appHost.SingletonInstance as HostService;
  if (host != null)
    this.DataContext = host;

Now you can simply bind elements in your window to properties on the object, as shown with this TextBlock:

<TextBlock Text="{Binding CurrentMessage}" Grid.Row="3" />

As I said, this is a simple example of what you can do. In a real application, you likely would not bind directly to the service instance but instead bind to some objects to which both your window and the service implementation had access.

Looking Ahead to WF4

WF4 introduces several features that will make local communications over WCF even easier. The primary feature is message correlation that does not rely on the protocol. That is, the use of a workflow instance identifier will still be an option, but a new option will enable messages to be correlated based on the content of the message. So, if each of your messages contain an order ID, a customer ID or some other piece of data, you can define correlations between those messages and not have to use a binding that supports context management. 

Additionally, the fact that both WPF and WF build on the same core XAML APIs in .NET Framework Version 4 might open up some interesting possibilities for integrating the technologies in new ways. As we get closer to the release of .NET Framework 4, I will provide more details on integrating WF with WCF and WPF, along with other content on the inner workings of WF4.            


Matt Milner is a member of the technical staff at Pluralsight, where he focuses on connected systems technologies (WCF, Windows Workflow Foundation, BizTalk, “Dublin,” and the Azure Services Platform). Matt is also an independent consultant specializing in Microsoft .NET application design and development. He regularly shares his love of technology by speaking at local, regional and international conferences such as Tech·Ed. Microsoft has recognized Milner as an MVP for his community contributions around connected systems technology. You can contact him via his blog at https://pluralsight.com/training/authors/details/matt-milner.