WCF request response via Callbacks

Component: Callbacks
NuGet Package NServiceBus (5.x)

Introduction

This samples shows how to perform a WCF request response by leveraging Callback of NServiceBus.

WCF Helpers

Shared contract

An 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 the sake of 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}. So 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
{
    IBus bus;
    string server;
    List<ServiceHost> serviceHosts = new List<ServiceHost>();

    public WcfMapper(IBus bus, string server)
    {
        this.bus = bus;
        this.server = server;
    }

    public void StartListening<TMessage, TResponse>()
    {
        var host = new ServiceHost(new CallbackService<TMessage, TResponse>(bus));
        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 Request to Response.

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

    public CallbackService(IBus bus)
    {
        this.bus = bus;
    }

    public async Task<TResponse> SendRequest(TRequest request)
    {
        if (typeof(TResponse).IsEnum)
        {
            var enumLocal = bus.SendLocal(request);
            return await enumLocal.Register<TResponse>()
                .ConfigureAwait(false);
        }
        if (typeof(TResponse) == typeof(int))
        {
            var intLocal = bus.SendLocal(request);
            object intValue = await intLocal.Register()
                .ConfigureAwait(false);
            return (TResponse)intValue;
        }
        return await bus.SendLocal(request).Register(ar =>
        {
            object[] messages = ar.Messages;
            return (TResponse)messages[0];
        })
        .ConfigureAwait(false);

    }

    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(IBus bus)
{
    var wcfMapper = new WcfMapper(bus, "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.

using (var bus = Bus.Create(busConfiguration).Start())
using (StartWcfHost(bus))
{
    Console.WriteLine("Press any key to exit");
    Console.ReadKey();
}

Client Configuration

SendHelper

A helper that build 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, for every call it creates a new ChannelFactory and ICommunicationObject. Depending on the specific use case it may be required to apply different scoping, lifetime and cleanup rules for these instances.

Sending

The request send 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