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.
.jpg)
Figure 1. The New
Project dialog box to create the workflow service
.jpg)
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.
.jpg)
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.
.jpg)
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.
.jpg)
Figure 5. Selecting
the ReceiveActivity from the Toolbox
.jpg)
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.
.jpg)
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.
.jpg)
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.
.jpg)
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="http://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.