Service Bus with .NET SDK

The Azure.Messaging.ServiceBus NuGet package is the official .NET SDK for Azure Service Bus. It provides a fully async, AMQP-based API for all messaging operations. This topic covers every major SDK feature with production-ready code — client setup, sending, pull and push receiving, sessions, dead-letter queues, scheduled messages, administration, and error handling.

SDK Installation

dotnet add package Azure.Messaging.ServiceBus
dotnet add package Azure.Identity

SDK Client Hierarchy

ServiceBusClient  (one per application)
  |-- CreateSender("queue-or-topic")         --> ServiceBusSender
  |-- CreateReceiver("queue")               --> ServiceBusReceiver
  |-- CreateReceiver("topic","sub")         --> ServiceBusReceiver
  |-- CreateProcessor("queue")              --> ServiceBusProcessor
  |-- CreateSessionProcessor("queue")       --> ServiceBusSessionProcessor
  |-- AcceptSessionAsync("queue","id")      --> ServiceBusSessionReceiver
  |-- AcceptNextSessionAsync("queue")       --> ServiceBusSessionReceiver

ServiceBusAdministrationClient
  |-- CreateQueueAsync / DeleteQueueAsync
  |-- CreateTopicAsync / CreateSubscriptionAsync
  |-- CreateRuleAsync / DeleteRuleAsync
  |-- GetQueueRuntimePropertiesAsync

1. Creating the Client

Connection String (Development)

using Azure.Messaging.ServiceBus;

string connectionString =
    "Endpoint=sb://myshopns.servicebus.windows.net/;" +
    "SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=<key>";

await using var client = new ServiceBusClient(connectionString);

Managed Identity (Production — No Secrets)

using Azure.Identity;
using Azure.Messaging.ServiceBus;

string fqNamespace = "myshopns.servicebus.windows.net";
await using var client = new ServiceBusClient(fqNamespace, new DefaultAzureCredential());

Client Options — Transport and Retry

var options = new ServiceBusClientOptions
{
    TransportType = ServiceBusTransportType.AmqpWebSockets,
    RetryOptions  = new ServiceBusRetryOptions
    {
        Mode       = ServiceBusRetryMode.Exponential,
        MaxRetries = 5,
        Delay      = TimeSpan.FromSeconds(1),
        MaxDelay   = TimeSpan.FromSeconds(30)
    }
};
await using var client = new ServiceBusClient(connectionString, options);

2. Sending Messages

Single Message to a Queue

await using var sender = client.CreateSender("orders");

var order = new { orderId = 101, product = "Laptop", amount = 1200.00 };
string json = System.Text.Json.JsonSerializer.Serialize(order);

var message = new ServiceBusMessage(BinaryData.FromString(json))
{
    MessageId   = "order-101",
    Subject     = "NewOrder",
    ContentType = "application/json",
    TimeToLive  = TimeSpan.FromHours(24)
};
message.ApplicationProperties["Region"]   = "India";
message.ApplicationProperties["Amount"]   = 1200.00;

await sender.SendMessageAsync(message);
Console.WriteLine("Message sent.");

Batch Sending (High Volume)

using ServiceBusMessageBatch batch = await sender.CreateMessageBatchAsync();

for (int i = 1; i <= 50; i++)
{
    var msg = new ServiceBusMessage($"Order {i}") { MessageId = $"order-{i}" };
    if (!batch.TryAddMessage(msg))
    {
        await sender.SendMessagesAsync(batch);
        batch.Dispose();
        batch = await sender.CreateMessageBatchAsync();
        batch.TryAddMessage(msg);
    }
}
await sender.SendMessagesAsync(batch);

3. Pull-Based Receiving

await using var receiver = client.CreateReceiver("orders",
    new ServiceBusReceiverOptions { ReceiveMode = ServiceBusReceiveMode.PeekLock });

ServiceBusReceivedMessage msg =
    await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(10));

if (msg != null)
{
    try
    {
        Console.WriteLine($"Body: {msg.Body} | Delivery#: {msg.DeliveryCount}");
        // business logic here
        await receiver.CompleteMessageAsync(msg);
    }
    catch
    {
        await receiver.AbandonMessageAsync(msg);
    }
}

4. Push-Based Receiving — ServiceBusProcessor

await using var processor = client.CreateProcessor("orders",
    new ServiceBusProcessorOptions
    {
        MaxConcurrentCalls         = 5,
        AutoCompleteMessages       = false,
        MaxAutoLockRenewalDuration = TimeSpan.FromMinutes(5)
    });

processor.ProcessMessageAsync += async args =>
{
    Console.WriteLine($"[{args.Message.MessageId}] {args.Message.Body}");
    try
    {
        // process order
        await args.CompleteMessageAsync(args.Message);
    }
    catch
    {
        await args.AbandonMessageAsync(args.Message);
    }
};

processor.ProcessErrorAsync += args =>
{
    Console.WriteLine($"Error [{args.ErrorSource}]: {args.Exception.Message}");
    return Task.CompletedTask;
};

await processor.StartProcessingAsync();
Console.ReadLine();
await processor.StopProcessingAsync();

Concurrent Processing Diagram

Queue: orders         MaxConcurrentCalls = 5
  |
  |-- msg1 --> Thread 1 --> CompleteAsync
  |-- msg2 --> Thread 2 --> CompleteAsync
  |-- msg3 --> Thread 3 --> AbandonAsync (retry)
  |-- msg4 --> Thread 4 --> CompleteAsync
  |-- msg5 --> Thread 5 --> CompleteAsync

5. Session Processor

await using var sp = client.CreateSessionProcessor(
    "order-session-queue",
    new ServiceBusSessionProcessorOptions
    {
        MaxConcurrentSessions        = 4,
        MaxConcurrentCallsPerSession = 1,
        AutoCompleteMessages         = false
    });

sp.ProcessMessageAsync += async args =>
{
    Console.WriteLine($"Session [{args.Message.SessionId}]: {args.Message.Body}");
    await args.CompleteMessageAsync(args.Message);
};
sp.ProcessErrorAsync += args =>
{
    Console.WriteLine($"Error: {args.Exception.Message}");
    return Task.CompletedTask;
};

await sp.StartProcessingAsync();
Console.ReadLine();
await sp.StopProcessingAsync();

6. Dead Letter Queue Access

await using var dlqRx = client.CreateReceiver("orders",
    new ServiceBusReceiverOptions { SubQueue = SubQueue.DeadLetter });

var dlqMsg = await dlqRx.ReceiveMessageAsync(TimeSpan.FromSeconds(5));
if (dlqMsg != null)
{
    Console.WriteLine($"Body   : {dlqMsg.Body}");
    Console.WriteLine($"Reason : {dlqMsg.DeadLetterReason}");
    Console.WriteLine($"Desc   : {dlqMsg.DeadLetterErrorDescription}");
    await dlqRx.CompleteMessageAsync(dlqMsg);
}

7. Scheduled Messages

await using var sender = client.CreateSender("orders");

long seqNo = await sender.ScheduleMessageAsync(
    new ServiceBusMessage("Future order") { MessageId = "future-001" },
    DateTimeOffset.UtcNow.AddHours(8)
);
Console.WriteLine($"Scheduled. SeqNo: {seqNo}");

await sender.CancelScheduledMessageAsync(seqNo);
Console.WriteLine("Cancelled.");

8. Administration Client

using Azure.Messaging.ServiceBus.Administration;

var admin = new ServiceBusAdministrationClient(connectionString);

await admin.CreateQueueAsync(new CreateQueueOptions("payments")
{
    MaxSizeInMegabytes                     = 2048,
    DefaultMessageTimeToLive               = TimeSpan.FromDays(7),
    LockDuration                           = TimeSpan.FromMinutes(1),
    MaxDeliveryCount                       = 10,
    EnableDeadLetteringOnMessageExpiration = true,
    RequiresDuplicateDetection             = true
});

await admin.CreateTopicAsync("order-events");
await admin.CreateSubscriptionAsync("order-events", "inventory-sub");

await admin.DeleteRuleAsync("order-events", "inventory-sub", "$Default");
await admin.CreateRuleAsync("order-events", "inventory-sub",
    new CreateRuleOptions
    {
        Name   = "india-filter",
        Filter = new SqlRuleFilter("Region = 'India' AND Amount > 1000"),
        Action = new SqlRuleAction("SET Priority = 'Urgent'")
    });

var stats = await admin.GetQueueRuntimePropertiesAsync("payments");
Console.WriteLine($"Active: {stats.Value.ActiveMessageCount}");
Console.WriteLine($"DLQ   : {stats.Value.DeadLetterMessageCount}");

9. Transactions in .NET

await using var receiver = client.CreateReceiver("order-queue");
await using var sender   = client.CreateSender("invoice-queue");

var orderMsg = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(10));
if (orderMsg != null)
{
    var invoiceMsg = new ServiceBusMessage($"Invoice for {orderMsg.MessageId}")
                     { MessageId = $"inv-{orderMsg.MessageId}" };

    using var txn = new ServiceBusTransactionContext();
    try
    {
        await sender.SendMessageAsync(invoiceMsg, txn);
        await receiver.CompleteMessageAsync(orderMsg, txn);
        await txn.CommitAsync();
        Console.WriteLine("Transaction committed.");
    }
    catch
    {
        await txn.RollbackAsync();
        Console.WriteLine("Transaction rolled back — order stays in queue.");
    }
}

10. Error Handling

Exception ReasonCauseAction
MessagingEntityNotFoundQueue/topic does not existVerify entity name
QuotaExceededQueue at storage limitDrain faster or increase size
UnauthorizedMissing RBAC role or wrong keyCheck policy rights
MessageSizeExceededBody too large for tierCompress or use Premium
MessageLockLostLock expired mid-processingEnable auto lock renewal
ServiceCommunicationError (transient)Network blipSDK retries automatically

Best Practices

PracticeReason
One ServiceBusClient per appThread-safe; reuses AMQP connections
Use await usingProperly closes AMQP links on exit
AutoCompleteMessages = falsePrevents message loss on handler crash
MaxAutoLockRenewalDurationAvoids lock expiry without manual renewal
DefaultAzureCredentialWorks with Managed Identity — no secrets in code
AmqpWebSockets transportPort 443 bypasses corporate firewalls

Summary

The Azure.Messaging.ServiceBus .NET SDK covers every Service Bus capability through a clean, async-first API. One ServiceBusClient per application handles all connections efficiently. The ServiceBusProcessor enables high-throughput concurrent consumption with automatic lock renewal. The ServiceBusAdministrationClient manages entity configuration programmatically. Using DefaultAzureCredential with Managed Identity eliminates secrets entirely in Azure-hosted workloads. Proper retry configuration, WebSocket transport, and explicit settlement build messaging solutions that are reliable, secure, and maintainable at production scale.

Leave a Comment