Getting Started
Architecture
Transports
Persistence
ServiceInsight
ServicePulse
ServiceControl
Monitoring
Samples

Client-side callbacks

NuGet Package: NServiceBus.Callbacks (4.x)
Target Version: NServiceBus 8.x

Callbacks enable the use of 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 gradually transitioning applications towards messaging.

When to use callbacks

When using callbacks in an ASP.NET Web/MVC/Web API application, 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 a response is received, it is handled and returned to the client. Web clients will still be blocked while waiting for response. This scenario is common when migrating from traditional blocking request/response to 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.

To handle responses from the processing endpoint, the sending endpoint must have its 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 a separate 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.

Integers

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

Send and callback

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

Response

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

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 strongly-typed manner.

Send and Callback

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

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, so the response must 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);
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

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);
}
catch (OperationCanceledException)
{
    // Exception that is raised when the CancellationTokenSource is canceled
}

Message routing

Callbacks are tied to the endpoint instance making the request, so all responses need to be routed back to the specific instance making the request. This means that callbacks require the endpoint to configure a unique instance ID:

endpointConfiguration.MakeInstanceUniquelyAddressable("uniqueId");

This will make each instance of the endpoint uniquely addressable by creating an additional queue that includes the instance ID in the name.

Selecting an appropriate ID depends on the transport being used and whether or not the endpoint is scaled out:

  • For broker transports like Azure ServiceBus, RabbitMQ, etc., the instance ID must be unique for each instance, otherwise the instances will end up sharing a single callback queue and a reply could be received by the wrong instance.
  • For federated transports like MSMQ, where every instance is running on a separate machine and can never share queues, then it is okay to use a single ID like replies, callbacks, etc.

Uniquely addressable endpoints will consume messages from their dedicated, instance-specific queues in addition to the main queue that all instances share. Replies will automatically be routed to the correct instance-specific queue.

Discarding reply messages

Reply messages that no longer have an associated callback will be moved to the error queue.

It is possible to leverage a custom recoverability policy with a discard action to prevent messages from being moved to the error queue.

endpointConfiguration.Recoverability().CustomPolicy((config, context) =>
{
    if (context.Exception is InvalidOperationException invalidOperationException && 
        invalidOperationException.Message.StartsWith("No handlers could be found", StringComparison.OrdinalIgnoreCase))
    {
        return RecoverabilityAction.Discard("Callback no longer active");
    }
    return DefaultRecoverabilityPolicy.Invoke(config, context);
});

Samples