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
Do not call the callback APIs from inside a Handle
method in an IHandleMessages
class as this can cause deadlocks or other unexpected behavior.
Because 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 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.
If the server process returns multiple responses, NServiceBus does not know which response message will be last. To prevent memory leaks, the callback is invoked only for the first response. Callbacks won't survive a process restart (e.g. 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 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.
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.
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);
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.
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 strongly-typed manner.
Send and Callback
var message = new Message();
var response = await endpoint.Request<Status>(message);
log.Info($"Callback received with response:{response}");
When replying with an enum value, the replying endpoint also needs to reference the NServiceBus.
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, 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.
To avoid creating an excessive number of queues, the ID must be kept stable. For example, it may be retrieved from a configuration file or from the environment (e.g. role ID in Azure or machine name for on-premises deployments).
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);
});