WCF request response via callbacks

Component: Callbacks
NuGet Package NServiceBus.Callbacks (2.x)
Target NServiceBus Version: 6.x

Introduction

This sample shows how to perform a WCF request response by leveraging an NServicebus Callback.

Callbacks should only be used in exceptional situations, for example, to introduce messaging behind a synchronous API in a legacy component that can't be changed. They allow to gradually transition applications towards messaging. See the when to use callbacks section for more information.

WCF helpers

Shared contract

A generic interface that is shared between both Client and Server to give a strong typed API.

[ServiceContract]
public interface ICallbackService<in TRequest, TResponse>:
    IDisposable
{
    [OperationContract]
    Task<TResponse> SendRequest(TRequest request);
}
For simplicity, this interface is located in the same assembly as the server-side helpers. This results in a reference to NServiceBus assemblies on the client side. In a real world solution this interface would most likely be moved to another assembly to avoid the need for a NServiceBus reference on the client side.

Receiving endpoint helpers

WCF mapper

Maps a request-response message pair to a ServiceHost listening on a BasicHttpBinding.

The url used for the binding will be of the format http://localhost:8080/BusService/{RequestMessage}_{Response}. For a message EnumMessage that has a response of Status the url would be http://localhost:8080/BusService/EnumMessage_Status

If a different binding or url structure is required it can be customized:

public class WcfMapper :
    IDisposable
{
    IEndpointInstance endpointInstance;
    string server;
    List<ServiceHost> serviceHosts = new List<ServiceHost>();

    public WcfMapper(IEndpointInstance endpointInstance, string server)
    {
        this.endpointInstance = endpointInstance;
        this.server = server;
    }

    public void StartListening<TMessage, TResponse>()
    {
        var host = new ServiceHost(new CallbackService<TMessage, TResponse>(endpointInstance));
        var binding = new BasicHttpBinding();
        var address = AddressBuilder.GetAddress<TMessage, TResponse>(server);
        var contract = typeof(ICallbackService<TMessage, TResponse>);
        host.AddServiceEndpoint(contract, binding, address);
        host.Open();
        serviceHosts.Add(host);
    }

    public void Dispose()
    {
        foreach (var serviceHost in serviceHosts)
        {
            serviceHost.Abort();
            serviceHost.Close();
        }
    }
}

CallbackService

The server side implementation of ICallbackService. This class handles the correlation of the request to the response.

In NServiceBus version 5 and below, the callback APIs for Enums, Ints and message responses differ slightly. Some logic is required to call the correct API for each response type. In version 6 and above this API has been simplified and no logic is required.
[ServiceBehavior(
    InstanceContextMode = InstanceContextMode.Single,
    Name = "CallbackService")]
class CallbackService<TRequest, TResponse> :
    ICallbackService<TRequest, TResponse>
{
    IEndpointInstance endpointInstance;

    public CallbackService(IEndpointInstance endpointInstance)
    {
        this.endpointInstance = endpointInstance;
    }

    public Task<TResponse> SendRequest(TRequest request)
    {
        var sendOptions = new SendOptions();
        sendOptions.RouteToThisEndpoint();
        return endpointInstance.Request<TResponse>(request, sendOptions);
    }

    public void Dispose()
    {
        ((IDisposable)this).Dispose();
    }
}

Client helpers

ClientChannelBuilder

The ClientChannelBuilder creates a proxy at run-time to allow strong typed execution of a mapped WCF service.

public static class ClientChannelBuilder
{
    public static ChannelFactory<ICallbackService<TMessage, TResponse>> GetChannelFactory<TMessage, TResponse>(string server)
    {
        var myBinding = new BasicHttpBinding();
        var address = AddressBuilder.GetAddress<TMessage, TResponse>(server);
        var myEndpoint = new EndpointAddress(address);
        return new ChannelFactory<ICallbackService<TMessage, TResponse>>(myBinding, myEndpoint);
    }
}

If generating a static proxy, using the Visual Studio "Add Service Reference" feature, no ClientChannelBuilder is required.

Receiving endpoint configuration

Mapping specific request-response pairs

This method maps some specific known request-response pairs to be listened to via a given url prefix.

static IDisposable StartWcfHost(IEndpointInstance endpointInstance)
{
    var wcfMapper = new WcfMapper(endpointInstance, "http://localhost:8080");
    wcfMapper.StartListening<EnumMessage, Status>();
    wcfMapper.StartListening<ObjectMessage, ReplyMessage>();
    wcfMapper.StartListening<IntMessage, int>();
    return wcfMapper;
}

Apply mapping to endpoint

Apply the request-response at bus startup.

var endpointInstance = await Endpoint.Start(endpointConfiguration)
    .ConfigureAwait(false);
using (StartWcfHost(endpointInstance))
{
    Console.WriteLine("Press any key to exit");
    Console.ReadKey();
}
await endpointInstance.Stop()
    .ConfigureAwait(false);

Client configuration

SendHelper

A helper that builds and cleans up both the ChannelFactory and the channel.

static async Task<TResponse> Send<TRequest, TResponse>(TRequest request)
{
    using (var channelFactory = ClientChannelBuilder.GetChannelFactory<TRequest, TResponse>(serverUrl))
    using (var client = channelFactory.CreateChannel())
    {
        return await client.SendRequest(request)
            .ConfigureAwait(false);
    }
}
For the purposes of this sample, a new ChannelFactory and ICommunicationObject is created for every call. Depending on the specific use case it may be required to apply different scoping, lifetime and cleanup rules for these instances.

Sending

The request, sending, and handling of the response.

static async Task SendObject()
{
    var message = new ObjectMessage
    {
        Property = "The Property Value"
    };
    var response = await Send<ObjectMessage, ReplyMessage>(message)
        .ConfigureAwait(false);
    Console.WriteLine($"Response: {response.Property}");
}

Samples


Last modified