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:

  1. Peek - Message is visible but not removed
  2. Process - Your code processes the message
  3. Complete - Message is deleted (on success) OR
  4. Abandon - Message returns to queue (on failure) OR
  5. 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

ConceptDescription
PeekLockMessage is hidden but not deleted
CompleteRemove message after successful processing
AbandonRelease lock, message returns to queue
Dead LetterMove to dead-letter queue after max retries

Best Practices

  1. Use async/await - Don't block threads
  2. Process within lock time - Complete before lock expires
  3. Handle exceptions - Use try/catch with abandon/dead-letter
  4. 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


Azure Integration Hub - Beginner Level