Azure Service Bus .NET SDK Deep Dive – Atomic Sends

Shows how to atomically send messages as a group, for more posts in this series go to Contents.

This topic is best illustrated by directly diving into some code.

await using var sender = serviceBusClient.CreateSender(destination);
var message = new ServiceBusMessage("Deep Dive 1");
await client.SendMessageAsync(message);
message = new ServiceBusMessage("Deep Dive 2");
await client.SendMessageAsync(message);

In the above code, a QueueClient is used to send two messages. By default, every time SendAsync is called, a message is sent to the destination queue. This has the following implications:

  • As soon as Azure Service Bus acknowledges a send operation, the message becomes available in the destination queue and therefore is ready for consumption by the receiver
  • Every SendAsync call is its own unit of failure, which means in the above example, while the first send operation might be successful it is possible that the second send operation fails to be delivered (due to network outage for example). In this case, the destination input queue will already see the first message but not yet the second message until the send operation is retried by the sender.

In many scenarios, the behavior above is actually the behavior you want. A message should be a unit of transaction, and you rarely ever need to group messages together. That being said in certain scenarios you might need to split things into multiple messages for the same destination, but you want either

  • All messages within the same group of messages successfully delivered and only then become visible in the input queue -or-
  • All operations failed as a group

Such an atomic grouping can be achieved by wrapping all the sends to the same destination within a TransactionScope.

await using var sender = serviceBusClient.CreateSender(destination);
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
    var message = new ServiceBusMessage("Deep Dive 1");
    await sender.SendMessageAsync(message);
    WriteLine(
        $"Sent message 1 in transaction '{Transaction.Current.TransactionInformation.LocalIdentifier}'");

    await Prepare.ReportNumberOfMessages(connectionString, destination);

    message = new ServiceBusMessage("Deep Dive 2");
    await sender.SendMessageAsync(message);
    WriteLine(
        $"Sent message 2 in transaction '{Transaction.Current.TransactionInformation.LocalIdentifier}'");

    WriteLine("About to complete transaction scope.");
    await Prepare.ReportNumberOfMessages(connectionString, destination);

    scope.Complete();
    WriteLine("Completed transaction scope.");
}

ReportNumberOfMessages gets the current message count of the destination and reports it back. If we execute the sends within the transaction scope we can see message are not available until the transaction scope is completed. Once it is completed all messages that are group together become immediately available.

To demonstrate how messages can leak when using regular sends it is possible to introduce another send that suppresses the current transaction scope.

using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
    var message = new ServiceBusMessage("Deep Dive 1");
    await sender.SendMessageAsync(message);
    WriteLine(
        $"Sent message 1 in transaction '{Transaction.Current.TransactionInformation.LocalIdentifier}'");

    await Prepare.ReportNumberOfMessages(connectionString, destination);

    message = new ServiceBusMessage("Deep Dive 2");
    await sender.SendMessageAsync(message);
    WriteLine(
        $"Sent message 2 in transaction '{Transaction.Current.TransactionInformation.LocalIdentifier}'");
    
    using (var suppress = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled))
    {

        message = new ServiceBusMessage("Deep Dive 3");
        await sender.SendMessageAsync(message);
        Console.WriteLine(
            $"Sent message 3 within suppressed scope");
    }

    WriteLine("About to complete transaction scope.");
    await Prepare.ReportNumberOfMessages(connectionString, destination);

    scope.Complete();
    WriteLine("Completed transaction scope.");
}

When running this we can see how one message becomes immediately available

Sent message 1 in transaction 'c83cc204-3aa0-4b65-a287-a52e6ecc5bf8:1'
#'0' messages in 'queue'
Sent message 2 in transaction 'c83cc204-3aa0-4b65-a287-a52e6ecc5bf8:1'
Sent message 3 within suppressed scope
About to complete transaction scope.
#'1' messages in 'queue'
Completed transaction scope.
#'3' messages in 'queue'

Wrapping sends to the same destination in a transaction scope is a truly powerful concept that leverages the transactional nature of Azure Service Bus to group messages together into execution or transaction scopes. This concept becomes even more powerful when combined with SendVia which will be covered another article.

Updated: 2021-03-23 to use the new SDK

About the author

Daniel Marbach

4 comments

By Daniel Marbach

Recent Posts