[ServiceContract(CallbackContract = typeof(IService1Callback))]
public interface IService1
{
[OperationContract]
string GetData(int value);
}
[ServiceContract]
public interface IService1Callback
{
[OperationContract]
void Reply(string message);
}
along with a client like this:
class Program
{
static void Main(string[] args)
{
var callback = new Service1Callback();
var proxy = new Service1Client(new InstanceContext(callback));
Console.WriteLine(proxy.GetData(42));
Console.ReadLine();
}
}
class Service1Callback : IService1Callback
{
public void Reply(string message)
{
Console.WriteLine(message);
}
}
But with a workflow service this doesn’t work because, without the ServiceContract attribute, there is no way to specify the CallbackContract. Yet workflows support duplex communications. So if we can’t use a CallbackContract how can we do so?
Workflow Services and Durable Duplex
Instead of the normal WCF duplex services workflow services use a mechanism that is called durable duplex. This is a more disconnected way of doing duplex communications that has a big advantage in that both communications work independently of each other. And that means the usual drawbacks of duplex communications don’t apply here. We can even use a completely different binding on the callback than on the original request. There is however a downside as the client has to create its own ServiceHost and act as a complete WCF service, a little more involved than a normal duplex request. Also settings a workflow, and the client, up for durable duplex takes a bit more configuring. So lets take a look how to do so.
Creating the Workflow Service
First step is to create the workflow service. This starts just like any other WCF Workflow Service Application. The durable duplex mechanism requires a context binding so the first change to the project is to use the wsHttpContextBinding. An easy change using the WCF protocolMapping:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<protocolMapping>
<add scheme="http" binding="wsHttpContextBinding" />
</protocolMapping>
<!-- Remainder deleted -->
</system.serviceModel>
</configuration>
Next step is adding a client console application and making sure we can call the workflow service. Add a console application, add a service reference to the workflow service and add the following code to the
Main() function:
static void Main(string[] args)
{
var proxy = new ServiceClient();
Console.WriteLine(proxy.GetData(42));
Console.ReadLine();
}
No big deal so far. Now lets add the duplex part.
First of all we need to add a ServiceHost to the client exposing a callback contract and a callback service definition for the workflow service to call back to. Unlike a normal duplex service, where you define the contract on the service, we need to define this contract on the client using the following definition:
[ServiceContract]
interface IServiceCallback
{
[OperationContract(IsOneWay = true)]
void Update(string message);
}
[ServiceBehavior(InstanceContextMode= InstanceContextMode.Single)]
class ServiceCallback : IServiceCallback
{
public void Update(string message)
{
Console.WriteLine(message);
}
}
Next step is to update the Main() function to host this service:
static void Main(string[] args)
{
var address = "http://localhost:8080/ServiceCallback";
var serviceCallback = new ServiceCallback();
var host = new ServiceHost(serviceCallback, new Uri(address));
host.Open();
var proxy = new ServiceClient();
Console.WriteLine(proxy.GetData(42));
Console.ReadLine();
proxy.Close();
host.Close();
}
Note that this looks just like a service would when self hosting.
Next step is to get the workflow service to call back to the client. For this we need to do a could of things. First of all we need to let the workflow service know where to call back to. This is done by adding a CallbackContextMessageProperty with the callback address to the request header.
This can be done with an OperationContextScope by replacing the proxy.GetDate() call with the following code in the Main() function:
var proxy = new ServiceClient();
using (new OperationContextScope((IContextChannel)proxy.InnerChannel))
{
var context = new CallbackContextMessageProperty(address, new Dictionary<string, string>());
OperationContext.Current.OutgoingMessageProperties.Add(CallbackContextMessageProperty.Name, context);
Console.WriteLine(proxy.GetData(42));
}
Next we need to update the workflow itself.
The first thing we need to add is a CorrelationHandle variable to hold the callback correlation.
Next we need to initialize the callbackHandle using the CorrelationInitializer on the GetData Receive activity.
Next we need a Send activity to send the response to the client setting its CorrelatesWith to the same callbackHandle just initialized.
Finally we set the Send activity properties to match the client callback service as follows:
Note that we leave both the AddressUri and EndpointAddress empty, there is no config either, because this is provide through the request context and the callback correlation handle. We are also setting and set the binding to basicHttpBinding because that was what the client was using.
And finally we need to add a message to pass back like this:
So with all this complete we can run our client and service and have the service call back into the client.
A few things that might trip you up.
Forgetting to set the callback CorrelationHandle results in no messages being sent to the client. In fact an InvalidOperationException occurs with message "Endpoint with Name='<not specified>' and ServiceContract '<not specified>' has a null or empty Uri property. A Uri for this Endpoint must be provided.". This is caused by the Send activity not being able to find its callback address.
Not passing an empty context from the client. The CallbackContextMessageProperty has a few constructor overloads, one just taking a single EndpointAddress and no context parameter. Seems like a good choice as we don’t need to specify any context. Unfortunately this results in an ArgumentNullException with message "Value cannot be null. Parameter name: context". Again when the Send activity tries to find its callback address
How about Silverlight?
And the is of course the biggest problem of them all: what if you can’t create a ServiceHost on the client? In that case you are done for and there is no way to use this durable duplex mechanism. And I guess that rules Silverlight out as a possible client [:(]
Digging Deeper into CallbackContextMessageProperty and CorrelationHandle
Yesterday I posted a long
blog post explaining how to do duplex communications in a Workflow service. Its a long story but the most important points where that workflow services don’t support the same style duplex communication as WCF with the callback channel defined in the ServiceContract but rather something that is called durable duplex where the callback contract is independent and the client has to create a ServiceHost and act as a full-blown WCF service. Also the callback address had to be passed by the client using CallbackContextMessageProperty and the workflow service had to use a callback correlation handle to connect the Receive activity with the Send activity used for the callback.
So what is the problem with using the CallbackContextMessageProperty and CorrelationHandle?
There is no real problem per se except that it takes quite a bit of doing to set this up, both on the client and in the workflow service. Because standard WCF callbacks are not supported by workflow services using a ServiceHost to handle the callback on the client is unavoidable. This is unfortunate because this prevents several scenarios, most notably, using Silverlight for the client application. However all the complexity with the CallbackContextMessageProperty and CorrelationHandle is only there to pass a single string, the callback address. And the Send activity has the capability to set the callback address dynamically using the EndpointAddress property.
Would it not be much simpler in this case to pass the callback address as a parameter to our initial service call?
As it turns out it is! And in the rest of this post I will show how to do just that.
The starting point is the DuplexDemo solution from the
previous post. You can download this code from
here.
First thing is to add a string variable “callbackAddress” and update the Receive activity to accept this as a second parameter.
And we need to set the Send activity EndpointAddress to the passed address using “New Uri(callbackAddress)”
Next we can remove the callbackHandle we no longer need. Remove this from the variables, the Receive CorrelationInitializers and the Send activity CorrelatesWith. That is the service done. Next we need to remove some code from the client removing the OperationContextScope, CallbackContextMessageProperty and adding the callback address as the second parameter to the GetData() call.
The code in the Main() function now looks like this:
static void Main(string[] args)
{
var address = "http://localhost:8080/ServiceCallback";
var serviceCallback = new ServiceCallback();
var host = new ServiceHost(serviceCallback, new Uri(address));
host.Open();
var proxy = new ServiceClient();
Console.WriteLine(proxy.GetData(42, address));
Console.ReadLine();
proxy.Close();
host.Close();
}
So much simpler [:)]
But besides being simpler there is another benefit. Because the callback address is being passed as a normal piece of data instead of some hidden context header we can check if it is passed. So we can have our workflow check if a callback address is passed and if not just skip the callback altogether. This way the workflow service is still usable from clients, like Silverlight, where creating a ServiceHost is not an option.
Of course they don’t receive the data being passed in the callback so that is something to keep in mind when deigning your workflow.
Conclusion.
I like the simpler approach this offers. It doesn’t mean that durable duplex should never be used, it certainly has its place, but when this simpler approach is all that is needed so much the better. All in all this works but it is kind of hard to setup the first time because of all the intricacies with the CallbackContextMessageProperty and the callback correlation handle.