Client side Callbacks

Project Hosting | Nuget: NServiceBus.Callbacks (Version: 1.x)
Target NServiceBus Version: 6.x

Callbacks allow to use messaging behind a synchronous API that can't be changed. A common use case is introducing messaging to existing synchronous Web or WCF applications. The advantage of using callbacks is that they allow to gradually transition applications towards messaging.

Handling responses in the context of a message being sent

When sending a message, a callback can be registered that will be invoked when a response arrives.

If the server process returns multiple responses, NServiceBus cannot know which response message will be the last. To prevent memory leaks, the callback is invoked only for the first response. Callbacks won't survive a process restart (common scenarios are a crash or an IIS recycle) as they are held in memory, so they are less suitable for server-side development where fault-tolerance is required. In those cases, sagas are preferred.

To handle responses from the processing endpoint, the sending endpoint must have it's own queue. Therefore, the sending endpoint cannot be configured as a SendOnly endpoint. Messages arriving in this queue are handled using a message handler, similar to that of the processing endpoint, as shown:

Edit
public class MyMessageHandler :
    IHandleMessages<MyMessage>
{
    public async Task Handle(MyMessage message, IMessageHandlerContext context)
    {
        // do something in the client process
    }
}

Prerequisites for callback functionality

In NServiceBus Version 5 and below callbacks are built into the core NuGet.

In NServiceBus Version 6 and above callbacks are shipped as NServiceBus.Callbacks NuGet package.

Using Callbacks

The callback functionality can be split into three categories based on the type of information being used; integers, enums and objects. Each of these categories involves two parts; send+callback and the response.

Int

The integer response scenario allows any integer value to be returned in a strong typed manner.

Send and Callback

Edit
var message = new Message();
var response = await endpoint.Request<int>(message)
    .ConfigureAwait(false);
log.Info($"Callback received with response:{response}");

Response

Edit
public class Handler :
    IHandleMessages<Message>
{
    public Task Handle(Message message, IMessageHandlerContext context)
    {
        return context.Reply(10);
    }
}

Enum

The enum response scenario allows any enum value to be returned in a strong typed manner.

Send and Callback

Edit
var message = new Message();
var response = await endpoint.Request<Status>(message)
    .ConfigureAwait(false);
log.Info($"Callback received with response:{response}");

Response

Edit
public class Handler :
    IHandleMessages<Message>
{
    public Task Handle(Message message, IMessageHandlerContext context)
    {
        return context.Reply(Status.OK);
    }
}

Object

The Object response scenario allows an object instance to be returned.

The Response message

This feature leverages the message Reply mechanism of the bus and hence the response need to be a message.

Edit
public class ResponseMessage :
    IMessage
{
    public string Property { get; set; }
}

Send and Callback

Edit
var message = new Message();
var response = await endpoint.Request<ResponseMessage>(message)
    .ConfigureAwait(false);
log.Info($"Callback received with response:{response.Property}");

Response

Edit
public class Handler :
    IHandleMessages<Message>
{
    public Task Handle(Message message, IMessageHandlerContext context)
    {
        var responseMessage = new ResponseMessage
        {
            Property = "PropertyValue"
        };
        return context.Reply(responseMessage);
    }
}

Cancellation

This API was added in the externalized Callbacks feature.

The asynchronous callback can be canceled by registering a CancellationToken provided by a CancellationTokenSource. The token needs to be passed into the Request method as shown below.

Edit
var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(5));
var message = new Message();
try
{
    var response = await endpoint.Request<int>(message, cancellationTokenSource.Token)
        .ConfigureAwait(false);
}
catch (OperationCanceledException)
{
    // Exception that is raised when the CancellationTokenSource is canceled
}

When to use callbacks

Using callbacks in IHandleMessages<T> classes can cause deadlocks and/or other unexpected behavior, so do not call the callback APIs from inside a Handle method in an IHandleMessages<T> class.
Due to the fact that callbacks won't survive restarts, use callbacks when the data returned is not business critical and data loss is acceptable. Otherwise, use request/response with a message handler for the reply messages.

When using callbacks in a ASP.NET Web/MVC/Web API, the NServiceBus callbacks can be used in combination with the async support in Asp.Net to avoid blocking the web server thread and allowing processing of other requests. When response is received, it is handled and returned to the client side. Web clients will still be blocked while waiting for response. This scenario is common when migrating from traditional blocking request/response to messaging.

Message routing

Callback responses are routed based on the ReplyTo header of the request. In order to use callbacks, instance Id needs to be explicitly specified in the requester endpoint configuration:

Edit
endpointConfiguration.MakeInstanceUniquelyAddressable("uniqueId");

The endpoint queue name will include the specified suffix. When upgrading from lower versions the original input queue (without a suffix) will still exist, and the additional queue with the suffix will be created.

Uniquely addressable endpoints can consume messages from both shared and unique queues. Replies will automatically be sent to the instance specific queue.

This approach makes it possible to deploy multiple callback-enabled instances of a given endpoint even to the same machine.

This Id needs to be stable and it should never be hardcoded. An example approach might be reading it from the configuration file or from the environment (e.g. role Id in Azure).

Samples


Last modified