Durable Duplex Correlation

Durable duplex correlation, also known as callback correlation, is useful when a workflow service has a requirement to send a callback to the initial caller. Unlike WCF duplex, the callback can happen at any time in the future and isn't tied to the same channel or the channel lifetime. The only requirement is that the caller have an active endpoint listening for the callback message. This allows two workflow services to communicate in a long-running conversation. This article provides an overview of durable duplex correlation.

Using Durable Duplex Correlation

To use durable duplex correlation, the two services must use a context-enabled binding that supports two-way operations, such as NetTcpContextBinding or WSHttpContextBinding. The calling service registers a ClientCallbackAddress with the desired binding on their client Endpoint. The receiving service receives this data in the initial call and then uses it on its own Endpoint in the Send activity that makes the call back to the calling service. In this example, two services communicate with each other. The first service invokes a method on the second service and then waits for a reply. The second service knows the name of the callback method, but the endpoint of the service that implements this method isn't known at design time.

Note

Durable duplex can only be used when the AddressingVersion of the endpoint is configured with WSAddressing10. If it's not, then an InvalidOperationException exception is thrown with the following message: "The message contains a callback context header with an endpoint reference for AddressingVersion. Callback context can only be transmitted when the AddressingVersion is configured with 'WSAddressing10'."

In the following example, a workflow service is hosted that creates a callback Endpoint using WSHttpContextBinding.

// Host WF Service 1.
string baseAddress1 = "http://localhost:8080/Service1";
WorkflowServiceHost host1 = new WorkflowServiceHost(GetWF1(), new Uri(baseAddress1));

// Add the callback endpoint.
WSHttpContextBinding Binding1 = new WSHttpContextBinding();
host1.AddServiceEndpoint("ICallbackItemsReady", Binding1, "ItemsReady");

// Add the service endpoint.
host1.AddServiceEndpoint("IService1", Binding1, baseAddress1);

// Open the first workflow service.
host1.Open();
Console.WriteLine("Service1 waiting at: {0}", baseAddress1);

The workflow that implements this workflow service initializes the callback correlation with its Send activity, and references this callback endpoint from the Receive activity that correlates with the Send. The following example represents the workflow that is returned from the GetWF1 method.

Variable<CorrelationHandle> CallbackHandle = new Variable<CorrelationHandle>();

Receive StartOrder = new Receive
{
    CanCreateInstance = true,
    ServiceContractName = "IService1",
    OperationName = "StartOrder"
};

Send GetItems = new Send
{
    CorrelationInitializers =
    {
        new CallbackCorrelationInitializer
        {
            CorrelationHandle = CallbackHandle
        }
    },
    ServiceContractName = "IService2",
    OperationName = "StartItems",
    Endpoint = new Endpoint
    {
        AddressUri = new Uri("http://localhost:8081/Service2"),
        Binding = new WSHttpContextBinding
        {
            ClientCallbackAddress = new Uri("http://localhost:8080/Service1/ItemsReady")
        }
    }
};

Receive ItemsReady = new Receive
{
    ServiceContractName = "ICallbackItemsReady",
    OperationName = "ItemsReady",
    CorrelatesWith = CallbackHandle,
};

Activity wf = new Sequence
{
    Variables =
    {
        CallbackHandle
    },
    Activities =
    {
        StartOrder,
        new WriteLine
        {
            Text = "WF1 - Started"
        },
        GetItems,
        new WriteLine
        {
            Text = "WF1 - Request Submitted"
        },
        ItemsReady,
        new WriteLine
        {
            Text = "WF1 - Items Received"
        }
     }
};

The second workflow service is hosted using a system-provided, context-based binding.

// Host WF Service 2.
string baseAddress2 = "http://localhost:8081/Service2";
WorkflowServiceHost host2 = new WorkflowServiceHost(GetWF2(), new Uri(baseAddress2));

// Add the service endpoint.
WSHttpContextBinding Binding2 = new WSHttpContextBinding();
host2.AddServiceEndpoint("IService2", Binding2, baseAddress2);

// Open the second workflow service.
host2.Open();
Console.WriteLine("Service2 waiting at: {0}", baseAddress2);

The workflow that implements this workflow service begins with a Receive activity. This receive activity initializes the callback correlation for this service, delays for a period of time to simulate long-running work, and then calls back into the first service using the callback context that was passed in the first call into the service. The following example represents the workflow that is returned from a call to GetWF2. The Send activity has a placeholder address of http://www.contoso.com; the actual address used at run time is the supplied callback address.

Variable<CorrelationHandle> ItemsCallbackHandle = new Variable<CorrelationHandle>();

Receive StartItems = new Receive
{
    CorrelationInitializers =
    {
        new CallbackCorrelationInitializer
        {
            CorrelationHandle = ItemsCallbackHandle
        }
    },
    CanCreateInstance = true,
    ServiceContractName = "IService2",
    OperationName = "StartItems"
};

Send ItemsReady = new Send
{
    CorrelatesWith = ItemsCallbackHandle,
    Endpoint = new Endpoint
    {
        // The callback address on the binding is used
        // instead of this placeholder address.
        AddressUri = new Uri("http://www.contoso.com"),

        Binding = new WSHttpContextBinding()
    },
    OperationName = "ItemsReady",
    ServiceContractName = "ICallbackItemsReady"
};

Activity wf = new Sequence
{
    Variables =
    {
        ItemsCallbackHandle
    },
    Activities =
    {
        StartItems,
        new WriteLine
        {
            Text = "WF2 - Request Received"
        },
        new Delay
        {
            Duration = TimeSpan.FromMinutes(90)
        },
        new WriteLine
        {
            Text = "WF2 - Sending items"
        },
        ItemsReady,
        new WriteLine
        {
            Text = "WF2 - Items sent"
        }
     }
};

When the StartOrder method is invoked on the first workflow, the following output is displayed, which shows the flow of execution through the two workflows.

Service1 waiting at: http://localhost:8080/Service1
Service2 waiting at: http://localhost:8081/Service2
Press enter to exit.
WF1 - Started
WF2 - Request Received
WF1 - Request Submitted
WF2 - Sending items
WF2 - Items sent
WF1 - Items Received

In this example, both workflows explicitly manage correlation using a CallbackCorrelationInitializer. Because there was only a single correlation in these sample workflows, the default CorrelationHandle management would have been sufficient.