Send & Receive Messages in Azure Service Bus
Overview
This guide teaches you how to send and receive messages using the Azure Service Bus SDK for .NET. Understanding these fundamental operations is essential for building reliable messaging-based applications. You'll learn about the PeekLock pattern, which ensures messages are processed reliably even if processing fails.
What You'll Learn
- Installing and configuring the Azure Service Bus SDK
- Sending messages to a queue with various properties
- Receiving messages using the PeekLock pattern
- Completing, abandoning, and dead-lettering messages
- Building robust message processing workflows
The PeekLock Pattern
When receiving messages, Service Bus uses a PeekLock pattern that ensures reliability:
- Peek - Message is visible but not removed
- Process - Your code processes the message
- Complete - Message is deleted (on success) OR
- Abandon - Message returns to queue (on failure) OR
- Dead Letter - Message moves to dead-letter queue (after max retries)
Install SDK
dotnet add package Azure.Messaging.ServiceBus
Send Messages
Simple Message Send
using Azure.Messaging.ServiceBus;
string connectionString = "Endpoint=sb://mynamespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=...";
string queueName = "order-queue";
await using var client = new ServiceBusClient(connectionString);
await using var sender = client.CreateSender(queueName);
// Create message
var message = new ServiceBusMessage("Order #12345");
// Send
await sender.SendMessageAsync(message);
Console.WriteLine("Message sent!");
Send with Properties
var message = new ServiceBusMessage("Order #12345");
message.ContentType = "application/json";
message.CorrelationId = "order-123";
message.Subject = "NewOrder";
message.Properties["CustomerId"] = "cust-456";
message.Properties["Priority"] = "High";
await sender.SendMessageAsync(message);
Receive Messages (PeekLock)
The PeekLock pattern ensures messages aren't deleted until processed successfully.
await using var client = new ServiceBusClient(connectionString);
await using var receiver = client.CreateReceiver(queueName, new ServiceBusReceiverOptions
{
ReceiveMode = ServiceBusReceiveMode.PeekLock
});
while (true)
{
var messages = await receiver.ReceiveMessagesAsync(maxMessages: 1);
foreach (var message in messages)
{
Console.WriteLine($"Received: {message.Body.ToString()}");
// Process the message
await ProcessOrderAsync(message);
// Complete the message (delete from queue)
await receiver.CompleteMessageAsync(message);
}
}
Message Processing Pattern
Complete (Success)
// Message processed successfully
await receiver.CompleteMessageAsync(message);
Abandon (Retry)
// Message failed but retry
await receiver.AbandonMessageAsync(message);
Dead Letter (Failed)
// Message failed permanently
await receiver.DeadLetterMessageAsync(message, "Error reason");
Asynchronous Processing
// Process in background
await foreach (var message in receiver.ReceiveMessagesAsync())
{
try
{
await ProcessMessageAsync(message);
await receiver.CompleteMessageAsync(message);
}
catch (Exception ex)
{
Console.WriteLine($"Failed: {ex.Message}");
await receiver.AbandonMessageAsync(message);
}
}
Key Concepts
| Concept | Description |
|---|---|
| PeekLock | Message is hidden but not deleted |
| Complete | Remove message after successful processing |
| Abandon | Release lock, message returns to queue |
| Dead Letter | Move to dead-letter queue after max retries |
Best Practices
- Use async/await - Don't block threads
- Process within lock time - Complete before lock expires
- Handle exceptions - Use try/catch with abandon/dead-letter
- Log messages - Track processing for debugging
Real-Time Scenarios
Scenario 1: Order Processing System
A complete e-commerce order processing workflow:
public class OrderProcessor
{
public async Task ProcessOrdersAsync()
{
await using var client = new ServiceBusClient(_connectionString);
await using var receiver = client.CreateReceiver("orders-queue");
await foreach (var message in receiver.ReceiveMessagesAsync())
{
try
{
var order = JsonSerializer.Deserialize<Order>(message.Body);
// Validate order
if (!ValidateOrder(order))
{
await receiver.DeadLetterMessageAsync(message, "Invalid order");
continue;
}
// Process payment
await _paymentService.ProcessPaymentAsync(order);
// Update inventory
await _inventoryService.ReserveItemsAsync(order);
// Send confirmation
await _notificationService.SendOrderConfirmationAsync(order);
await receiver.CompleteMessageAsync(message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order");
await receiver.AbandonMessageAsync(message);
}
}
}
}
Scenario 2: Sending JSON Data for Processing
Sending structured data for complex processing:
public async Task SendOrderAsync(Order order)
{
await using var client = new ServiceBusClient(_connectionString);
await using var sender = client.CreateSender("orders-queue");
// Create JSON message with metadata
var json = JsonSerializer.Serialize(order);
var message = new ServiceBusMessage(json)
{
ContentType = "application/json",
CorrelationId = order.OrderId,
Subject = "NewOrder",
TimeToLive = TimeSpan.FromMinutes(30),
Properties =
{
["CustomerId"] = order.CustomerId,
["Priority"] = order.Priority.ToString(),
["TotalAmount"] = order.TotalAmount.ToString()
}
};
// Add reply for async response
message.ReplyTo = "orders-response-queue";
await sender.SendMessageAsync(message);
}
Scenario 3: Batch Processing for High Throughput
Process multiple messages efficiently:
public async Task ProcessBatchAsync()
{
await using var client = new ServiceBusClient(_connectionString);
await using var receiver = client.CreateReceiver("orders-queue");
// Receive up to 100 messages at once
var messages = await receiver.ReceiveMessagesAsync(maxMessages: 100, maxWaitTime: TimeSpan.FromSeconds(5));
var successIds = new List<string>();
var failedIds = new List<string>();
foreach (var message in messages)
{
try
{
await ProcessMessageAsync(message);
successIds.Add(message.MessageId);
}
catch (Exception)
{
failedIds.Add(message.MessageId);
}
}
// Complete successful, abandon failed
foreach (var id in successIds)
{
await receiver.CompleteMessageAsync(id);
}
// Failed messages will be reprocessed automatically
}
Scenario 4: Scheduled/Retry Pattern
Handle messages that need to be retried later:
public async Task HandleWithRetryAsync(ServiceBusReceivedMessage message)
{
var retryCount = GetRetryCount(message);
if (retryCount >= 3)
{
// Too many retries - send to dead letter
await receiver.DeadLetterMessageAsync(message, "Max retries exceeded");
return;
}
try
{
await ProcessAsync(message);
await receiver.CompleteMessageAsync(message);
}
catch (Exception ex)
{
// Abandon and schedule for retry
await receiver.AbandonMessageAsync(message, new Dictionary<string, object>
{
["RetryCount"] = retryCount + 1,
["LastError"] = ex.Message,
["NextRetry"] = DateTime.UtcNow.AddMinutes(Math.Pow(2, retryCount)).ToString()
});
}
}
Scenario 5: Integration with Azure Functions
Service Bus trigger for Azure Functions:
public class OrderFunction
{
[Function("ProcessOrder")]
public async Task Run(
[ServiceBusTrigger("orders-queue")] ServiceBusReceivedMessage message,
ILogger log)
{
try
{
var order = JsonSerializer.Deserialize<Order>(message.Body);
log.LogInformation("Processing order: {OrderId}", order.OrderId);
await _orderService.ProcessAsync(order);
}
catch (Exception ex)
{
log.LogError(ex, "Failed to process order");
throw; // Throwing causes message to be dead-lettered
}
}
}
Scenario 6: Handling Different Message Types
Process different message types on the same queue:
public async Task ProcessMixedMessagesAsync()
{
await using var client = new ServiceBusClient(_connectionString);
await using var receiver = client.CreateReceiver("events-queue");
await foreach (var message in receiver.ReceiveMessagesAsync())
{
// Route based on subject/content type
switch (message.Subject)
{
case "OrderCreated":
await HandleOrderCreatedAsync(message);
break;
case "OrderShipped":
await HandleOrderShippedAsync(message);
break;
case "OrderCancelled":
await HandleOrderCancelledAsync(message);
break;
default:
log.LogWarning("Unknown message type: {Subject}", message.Subject);
await receiver.DeadLetterMessageAsync(message, "Unknown message type");
continue;
}
await receiver.CompleteMessageAsync(message);
}
}
Common Patterns
Request-Response Pattern
// Send request
var sessionId = Guid.NewGuid().ToString();
var requestQueue = client.CreateSender("requests");
var responseQueue = client.CreateReceiver("responses", new ServiceBusReceiverOptions
{
SessionIds = { sessionId }
});
var request = new ServiceBusMessage(json) { ReplyToSessionId = sessionId };
await requestQueue.SendMessageAsync(request);
// Wait for response
var response = await responseQueue.ReceiveMessageAsync(TimeSpan.FromSeconds(30));
Message Scheduling
// Send message to be processed later
var message = new ServiceBusMessage("Process at 2 PM");
message.ScheduledEnqueueTime = DateTimeOffset.UtcNow.AddHours(2);
await sender.SendMessageAsync(message);
Next Steps
- Understand Tiers: Basic vs Standard
- Learn about Connection Strings & Keys
- Explore Topics & Subscriptions for pub/sub patterns
Azure Integration Hub - Beginner Level