Using Windows Communication Foundation with Windows Workflow Foundation – Part 2

by Maurice de Beijer
August 2008

Both Windows Communications Foundation (WCF) and Windows Workflow Foundation (WF) were released at the same time as part of the .NET 3.0 release. But even though they were released together, they didn’t really work together. Fortunately, this shortcoming was rectified with the 3.5 release of the .NET Framework. In Part 1 of these articles I covered the SendActivity, which enables us to send requests from a workflow to a WCF-compatible service. In this second article, I will be taking a closer look at its companion, the ReceiveActivity.

Exposing workflows as Windows Communication Foundation services

As its name suggests, the ReceiveActivity is all about receiving requests into a workflow and thereby exposing the workflow as a service. Just like the SendActivity, it is completely built on top of the WCF stack, allowing it to be used with a variety of communication protocols.

Again, the best way to explain how to use this activity is by starting with a simple sample. Just like in the previous article, I am using Visual Studio 2008 and the .NET Framework 3.5. The first step is to create a new project (see Figure 1). This time, however, we are going to select the WCF project types instead of the Workflow project types, and choose the Sequential Workflow Service Library project type to create a new project called SimpleReceive. This may seem a little odd at first, as we are going to create a workflow project and not a WCF project; we are just exposing our workflow through WCF. Figure 2 shows the workflow designer with the workflow that was automatically created for us.

1_CreateSimpleReceive.png

Figure 1. The New Project dialog box to create the workflow service

2_TheInitialWorkflow.png

Figure 2. The designer showing the newly created workflow

Let’s take a look at what the project template provided for us before continuing. If we open the Solution Explorer, we will find a project containing an App.config file, an IWorkflow1.vb file, and finally a Workflow1.vb file. The Workflow1.vb file is the default workflow, as shown in Figure 2. This workflow already contains a ReceiveActivity. This ReceiveActivity already implements the GetData() function of the IWorkflow1 interface, something that can be seen in the property sheet by examining the ServiceOperationInfo property. This IWorkflow1 service interface is defined in the IWorkflow1.vb file and looks just like any other WCF interface with its ServiceContract and OperationContract attributes decorating the interface and the GetData() function. The App.config file contains the required WCF address, binding, and contract settings so we can expose our workflow as a service.

If you have not done any WCF development using Visual Studio 2008, you might be surprised to learn that you can press F5 and run the project just as it is. Two utilities, delivered as part of Visual Studio 2008, make this possible. The first is the WCF Service Host. This is a small lightweight host application that will launch and start exposing your service for use on your local machine, very much like the ASP.NET Development web server you can use when developing web applications from Visual Studio. Figure 3 shows this WCF Service Host in action. This WCF Service Host will automatically start as soon as a solution containing a WCF service project is started and makes life much easier, as you no longer have to write an application hosting your service first.

3_TheWCFServiceHost.png

Figure 3. The WCF Service Host hosting our workflow.

The second utility is the WCF Test Client, shown in Figure 4. This test client enables us to make service calls to the workflow that we just published using the WCF Service Host. This test client is somewhat limited in what we can do with it, as it can only work with simple parameters and creates a new proxy for every request; but it’s still a nice way to get started quickly.

4_TheWCFTestClient.png

Figure 4. The WCF Test Client after calling the GetData() function.

Adding a second operation to the IWorkflow1service

Let’s take a look at what we need to do now that we have a running workflow exposed as a service. The first step in adding a service operation is opening the service contract, located in IWorkflow1.vb, and adding a new operation. In this case we are going to add a second operation named SayHello and decorate it with the OperationContract attribute. The IWorkflow1.vb file should now look like Listing 1.

<ServiceContract()> _
Public Interface IWorkflow1
    <OperationContract()> _
    Function GetData(ByVal value As Int32) As String
    ' TODO: Add your service operations here
    <OperationContract()> _
    Function SayHello(ByVal name As String) As String
End Interface

Listing 1. The IWorkflow1 interface with a second operation

Next, we need to implement the service operation in our workflow. To do so, we need to drag a ReceiveActivity from the toolbox (see Figure 5) onto the Workflow1 and place it just below the receiveActivity1 that implements the GetData operation. Once the second ReceiveActivity is on the workflow, it shows a red exclamation mark at the right top with an error message “Activity 'receiveActivity2' does not have a service operation specified.” Click the error message to select the ServiceOperationInfo property. Clicking the button with the ellipses will bring up the Choose Operation dialog box (see Figure 6), enabling you to bind the ReceiveActivity to a service operation. This dialog box will show us all the service contracts already used on the workflow and all operations available. In this case we need to select the SayHello operation by selecting it and clicking OK.

5_TheToolbox.png

Figure 5. Selecting the ReceiveActivity from the Toolbox

6_TheChooseOperationDialog.png

Figure 6. Choosing the SayHello operation to bind to the ReceiveActivity

The Choose Operation dialog box can be a little confusing at first. When we open it, a green checkmark is displayed before the GetData operation. This checkmark only indicates that this operation is currently implemented somewhere in the workflow, not that this is the GetData operation. Instead, the current operation is the one with the selection bar over it; in this case, SayHello. This problem is worse when we open the dialog box again after selecting the SayHello operation, as now both operations show a green checkmark. In this case it is very easy to change the operation selection without noticing. The best solution is always to double-check by looking at the operation name, and its parameters, at the bottom half of the screen.

After selecting the SayHello operation, we should see two new properties appear in the property sheet. These work just like they did with the SendActivity and enable us to bind the parameters and return value to workflow properties. In this case, we are going to add two new properties to the workflow, SayHelloName and SayHelloReturnValue, both of type string, and bind to them by double-clicking the yellow icon in the property sheet. The result should look like Figure 7. Please refer to Part 1 for a more in-depth explanation of property bindings.

7_ThePropertySheet.png

Figure 7. The input and output parameters bound to workflow properties.

Pressing F5 to run our application should result in the WCF Test Client opening up again. The only difference is that this time, there should be two operations under the IWorkflow1 node: the first should be GetData and the second should be SayHello.

Now that we have the basic structure of our workflow hosted as a WCF service, we still need to implement the two service operations. The simplest way to do so now is to add two CodeActivities to the workflow, one in the GetData and the second inside of the SayHello operation. Let’s name the first GetDataImplementation and the second SayHelloImplementation. Now add the two event handlers shown in Listing 2 and bind the ExecuteCode handlers to these functions.

Private Sub GetDataImplementation_ExecuteCode( _
    ByVal sender As System.Object, _
    ByVal e As System.EventArgs)
    ReturnValue = String.Format("The input value was {0}", InputValue)
End Sub
Private Sub SayHelloImplementation_ExecuteCode( _
    ByVal sender As System.Object, _
    ByVal e As System.EventArgs)
    SayHelloReturnValue = String.Format("Hello there '{0}'", SayHelloName)
End Sub

Listing 2. The two CodeActivity ExecuteCode event handlers

If we now press F5 to debug our workflow, we are going to run into a problem. First we need to select the GetData operation and, after entering a value for the input parameter, click Invoke. The operation will execute and, after a brief pause, the result will appear just as it is supposed to. However, when we select the SayHello operation, enter a name and press Invoke, we receive an error. The error message is:

“Failed to invoke the service. The service may be offline or inaccessible. Refer to the stack trace for details.”

The details start with the following message:

“There is no context attached to incoming message for the service and the current operation is not marked with "CanCreateInstance = true". In order to communicate with this service check whether the incoming binding supports the context protocol and has a valid context initialized.”

Part of the details are about the CanCreateInstance property not being set to true. In this case that is correct, as this is the second ReceiveActivity in the workflow and setting this property to true means the workflow runtime creates a new workflow to handle the request, something we don’t want in this case. The error message also mentions that there is context attached to the incoming message. In fact, this is the real problem, because the WCF test Client creates a new proxy for every request, so it has lost all knowledge of the first request. Let’s take a look at what happens when we call the workflow ourselves using a different client.

Add a new ConsoleApplication project to the solution and call it SimpleReceiveClient. Once this project is created, add a service reference to workflow by right-clicking the project and choosing “Add Service Reference”. When the dialog box appears, choose the Discover button. Doing so will show the workflow service in the Services pane. Change the namespace to SimpleReceive and click OK to add the reference. Figure 8 shows the results in the Solution Explorer.

8_TheSolutionWithTheClientApplication.png

Figure 8. The solution with a console client

Once the service reference has been added, we can update the Sub Main located in Module1.vb. We need to add the code shown in Listing 3. Before we run the solution again, we need to make sure the SimpleReceiveClient is the startup project by right-clicking the project and choosing “Set as StartUp Project”.

Sub Main()
    Using proxy As New SimpleReceive.Workflow1Client()
        Console.WriteLine(proxy.GetData(5))
        Console.WriteLine(proxy.SayHello("Maurice"))
    End Using
    Console.ReadLine()
End Sub

Listing 3. The Sub Main of our test client

When we run the application, we should notice a few different things. First of all, the WCF Test Client no longer appears, as the console application is now the startup project. The WCF Service Host still starts to host your workflow and the related service. And finally, the client is able to make both calls to the service without any errors. The reason the console client is able to do both service calls whereas the WCF Test Client could not is that the console client uses the same proxy object for both requests, while the WCF Test Client creates a new proxy for each call. However, workflows are often long-running, and keeping the proxy alive is probably not a viable solution, so let’s investigate this further.

Invoking multiple operations on long-running workflows

In order to test invoking multiple operations on the same instance of a long-running workflow, we should split the two calls on the client. To do so, we need to change the code in Module1.vb to the code in Listing 4.

Sub Main()
    CallGetData()
    CallSayHello()
    Console.ReadLine()
End Sub
Sub CallGetData()
    Using proxy As New SimpleReceive.Workflow1Client()
        Console.WriteLine(proxy.GetData(5))
    End Using
End Sub
Sub CallSayHello()
    Using proxy As New SimpleReceive.Workflow1Client()
        Console.WriteLine(proxy.SayHello("Maurice"))
    End Using
End Sub

Listing 4. Using multiple proxy objects to call a workflow, take 1

The code in Listing 4 mimics what a real client would do, and what the WCF Test Client does, when calling a long running workflow by creating a new proxy object before each call. In this case, we get the same error as we did when using the WCF Test Client. In order to understand why this is the case, we need to understand that a number of specific bindings were added to WCF to support routing multiple calls to the same workflow instance. These are the so called context bindings, and we are using one, the wsHttpContextBinding, in this case. These context bindings make sure the context, or the required information to route multiple requests to the same workflow, is passed along with each request. In the case of the first client, using the same proxy for both requests, everything worked because the context was returned by the first request and automatically added to the second request. In the second test we used multiple proxy objects, and while the first did receive the context information, the second proxy was unaware of it, so could not send it with the second request. To solve this problem, we need to save the context from the first request and add it to the second proxy before we make the second request. Listing 5 shows how to do this by retrieving the IContextManager from the proxy innerChannel and setting it on the second proxy object. Before we can do so, however, we need to add a reference to System.WorkflowServices, part of the .NET Framework 3.5, as this assembly contains the required interface.

Sub Main()
    CallGetData()
    CallSayHello()
    Console.ReadLine()
End Sub
Private _context As Dictionary(Of String, String)
Sub CallGetData()
    Using proxy As New SimpleReceive.Workflow1Client()
        Console.WriteLine(proxy.GetData(5))
        Dim contextManager As IContextManager
        contextManager = proxy.InnerChannel.GetProperty(Of IContextManager)()
        _context = contextManager.GetContext()
    End Using
End Sub
Sub CallSayHello()
    Using proxy As New SimpleReceive.Workflow1Client()
        Dim contextManager As IContextManager
        contextManager = proxy.InnerChannel.GetProperty(Of IContextManager)()
        contextManager.SetContext(_context)
        Console.WriteLine(proxy.SayHello("Maurice"))
    End Using
End Sub

Listing 5. Saving and adding the WCF request context

The context that is passed along is an IDictionary with, in this case, a single key/value pair named instanceId identifying the workflow instance. In some cases, where correlation between multiple ReceiveActivities is required, a second value will be present, the conversationId, used for routing the message to the correct ReceiveActivity.

The Service Host

Normally, a WCF service is hosted by a ServiceHost object. In the case of a workflow being hosted, this is a little different, as a WorkflowServiceHost is used instead. This WorkflowServiceHost not only takes care of exposing the required end points, but also creates a WorkflowRuntime object so it can run the workflows. The most important thing the WorkflowServiceHost does is add the WorkflowRuntimeBehavior to the behaviors collection. This WorkflowRuntimeBehavior is the behavior that creates the WorkflowRuntime and configures it as needed. You can specify your own configuration for the workflow runtime using the application configuration file as shown in Listing 6.

<behaviors>
  <serviceBehaviors>
    <behavior name="SimpleReceive.Workflow1Behavior"  >
      <!-- Details omitted -->
      <workflowRuntime>
        <services>
          <add type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
               connectionString="Data Source=localhost\sqlexpress;Initial Catalog=WorkflowPersistence;Integrated Security=True;Pooling=False"
               LoadIntervalSeconds="1"
               UnLoadOnIdle="true" />
        </services>
      </workflowRuntime>
    </behavior>
  </serviceBehaviors>
</behaviors>

Listing 6: Adding the SqlWorkflowPersistenceService service to the runtime

Of course you can host the workflow yourself using the WorkflowServiceHost if you prefer. This is not very different from hosting a regular WCF service, with the exception that you use the WorkflowServiceHost instead of the default ServiceHost. Listing 7 shows how to do so and add the same SqlWorkflowPersistenceService to the WorkflowRuntime.

' Create the service host
Dim host As New WorkflowServiceHost(GetType(Workflow1))
' Retreive the WF behavior
Dim workflowRuntimeBehaviour As WorkflowRuntimeBehavior = _
    host.Description.Behaviors.Find(Of WorkflowRuntimeBehavior)()
' Retreive the WF runtime
Dim workflowRuntime As WorkflowRuntime = workflowRuntimeBehaviour.WorkflowRuntime
' Create and add the SqlWorkflowPersistenceService
Dim connectionString As String = "Data Source=localhost\sqlexpress;" + _
    "Initial Catalog=WorkflowPersistence;Integrated Security=True;Pooling=False"
Dim persistenceService As New SqlWorkflowPersistenceService(connectionString)
workflowRuntime.AddService(persistenceService)
' Start listening
host.Open()

Listing 7: Self-hosting the workflow service

Adding a second workflow to our service

So far we have been working with the workflow that was automatically created and configured when we created our workflow service. However, in all likelihood we will want to publish more than a single workflow from the same service, and we need to add all required configuration.

To add a new workflow, right-click the SimpleReceive project and choose Add, and then choose Sequential Workflow…. This will open the Add New Item dialog box with the Sequential Workflow selected. Name the workflow TheSecondWorkflow and click Add. This gives us an empty second workflow. Because we also want to start this workflow using a WCF request, we first need to add a ReceiveActivity to the workflow. Go ahead and drag one from the toolbox onto the workflow. The ReceiveActivity we just added shows a red exclamation mark indicating it is not valid yet; selecting it will reveal the message “Activity ‘receiveActivity1’does not have a service operation specified.”. Clicking it will take us to the ServiceOperationInfo property in the property sheet. Clicking the button brings up the “Choose Operation” dialog box, but unlike the previous times there are no operations listed. There are two possible ways to add a service contract to a workflow. The first, and this is what we did in the first workflow, is to define an interface in Visual Basic code and use that. This is done by clicking the “Import…” button that opens the type browser enabling us to select an existing interface. The second option is to define the service contract in the workflow itselfl this is done by using the Add Contract button. This last option means there is no actual Visual Basic source file with the service interface; everything is contained inside of the workflow. Let’s use the Add Contract option, as we have already used the Import in the first workflow.

Clicking the Add Contract button adds a new service contract with name Contract1 and a single service operation named Operation1. Let’s rename the service operation to StartWorkflow and the contract to MySecondContract. Changing the return value or adding parameters can be done in the Parameters tab at the bottom of the window when the operation is selected. Change the return value to type String and add one parameter, name, also of type String. Click OK to confirm the new operation.

9_NewServiceOperation.png

Figure 9: Adding a new service operation

We also want this operation to start a new workflow, so we must set the CanCreateInstance property of the ReceiveActivity to True. If this is set to False, the context binding will not create a new workflow, but return a fault when the workflow context is not passed along with the WCF request. Before we can use our workflow through the WCF ServiceHost, we still need to add this service to the configuration file. To do so, we need to add a second service in the App.Config file and name it “SimpleReceive.TheSecondWorkflow”. As we don’t need a different behavior from our first workflow, we can reuse the same behaviorConfiguration. Next we need to specify a base address and add an endpoint with MySecondContract as the contract and one of the context bindings, wsHttpContextBinding in this case, as the binding. Listing 8 shows the new section in the App.Config file.

<system.serviceModel>
  <services>
    <service name="SimpleReceive.TheSecondWorkflow" behaviorConfiguration="SimpleReceive.Workflow1Behavior">
      <host>
        <baseAddresses>
          <add baseAddress="https://localhost:8731/Design_Time_Addresses/SimpleReceive/TheSecondWorkflow" />
        </baseAddresses>
      </host>
      <endpoint address=""
                binding="wsHttpContextBinding"
                contract="MySecondContract"
                >
        <identity>
          <dns value="localhost"/>
        </identity>
      </endpoint>
      <endpoint address="mex"
                binding="mexHttpBinding"
                contract="IMetadataExchange" />
    </service>
      <!-- Remainder omitted. -->

Listing 8: The new service definition in our configuration file

With the service all set up, the next thing we need to do is get the client to call the second workflow. The second workflow will not show up using the existing service reference because the second workflow is a completely new service, so we need to add a second service reference. Right-click the Service References node in the SimpleReceiveClient project and choose Add Service Reference…. This will open the Add Service Reference dialog box again, but this time, after we click the Discover button, there should be two services listed. Select the service pointing to SimpleReceive/TheSecondWorkflow/mex and change the Namespace to TheSecondWorkflow before clicking OK. This will generate the proxy classes required to work with the second workflow. Listing 9 shows how to call the second workflow. Note that it will not actually print anything, as we didn’t add any functionality to the workflow itself.

Sub Main()
    Using proxy As New TheSecondWorkflow.MySecondContractClient()
        Console.WriteLine(proxy.StartWorkflow("Maurice"))
    End Using
    Console.ReadLine()
End Sub

Listing 9: Calling the second workflow

Conclusion

Using a ReceiveActivity it is not hard to expose a workflow as a WCF service. The WCF Service Host makes life easy by providing us with a host application to use when developing. The WorkflowServiceHost creates the WorkflowRuntime and exposes the configured endpoints for us. It also configures the workflow runtime with the specified configuration if needed. Combine the ReceiveActicity with the SendActivity described in the previous article and you get the combined power of WCF and WF.

About Maurice de Beijer
Maurice de Beijer is an independent software consultant specializing in Workflow Foundation and .NET in general. He has been awarded the yearly Microsoft Most Valuable Professional award since 2005. Besides developing software Maurice also runs the Visual Basic section of the Software Development Network, the largest Dutch .NET user group. Maurice can be reached through his websites, either www.WindowsWorkflowFoundation.eu or www.TheProblemSolver.nl, or by emailing him at Maurice@TheProblemSolver.nl.