Client side Callbacks

Project Hosting
NuGet Package NServiceBus.Callbacks (2.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:

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. This package has to be referenced by the requesting endpoint.

Enabling callbacks

The requesting endpoint has to enable the callbacks via configuration:

endpointConfiguration.EnableCallbacks();

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.

This type of callback won't cause response messages to end up in the error queue if no callback is registered.

Send and Callback

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

Response

public class Handler :
    IHandleMessages<Message>
{
    public Task Handle(Message message, IMessageHandlerContext context)
    {
        return context.Reply(10);
    }
}
When replying with an int value, the replying endpoint also needs to reference the NServiceBus.Callbacks package.

If the endpoint only replies to callbacks enable the callbacks as shown below:

endpointConfiguration.EnableCallbacks(makesRequests: false);

Enum

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

Send and Callback

var message = new Message();
var response = await endpoint.Request<Status>(message)
    .ConfigureAwait(false);
log.Info($"Callback received with response:{response}");
When replying with an enum value, the replying endpoint also needs to reference the NServiceBus.Callbacks package.

If the endpoint only replies to callbacks enable the callbacks as shown below:

endpointConfiguration.EnableCallbacks(makesRequests: false);

Response

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.

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

Send and Callback

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

Response

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.

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. To use callbacks, a unique instance Id needs to be specified in the requester endpoint configuration:

endpointConfiguration.MakeInstanceUniquelyAddressable("uniqueId");

This will make each instance of the endpoint uniquely addressable by creating an additional queue using the instance Id in the name of the queue. Each instance needs to use a different instance Id to guarantee they each get their own queue.

Uniquely addressable endpoints will consume messages from both the 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