# Azure Service Bus Standard

> **Scope:** blazor-azure
> **Layer:** 2 (on keyword)
> **Keywords:** service bus, messaging, queue, topic, subscription
> **Load When:** service bus or azure messaging keywords detected

Enterprise messaging patterns with Azure Service Bus.

---

## Overview

Azure Service Bus provides:
- **Queues**: Point-to-point messaging with FIFO guarantee
- **Topics/Subscriptions**: Publish-subscribe patterns
- **Dead-letter queues**: Failed message handling
- **Message sessions**: Ordered processing
- **Scheduled messages**: Delayed delivery

**Use Cases:** Event-driven architectures, microservices communication, async workflows

---

## Core Principles

1. **At-Least-Once Delivery**: Messages delivered at least once
2. **Managed Identity**: Use Managed Identity over connection strings
3. **Idempotent Handlers**: Handle duplicate messages gracefully
4. **Dead-Letter Management**: Monitor and reprocess failed messages
5. **Sessions for Ordering**: Use sessions when message order matters

---

## Installation

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

---

## Queue Patterns

### DI Configuration

```csharp
// Program.cs
using Azure.Identity;
using Azure.Messaging.ServiceBus;

builder.Services.AddSingleton(sp =>
{
    var fullyQualifiedNamespace = builder.Configuration["ServiceBus:FullyQualifiedNamespace"]!;
    return new ServiceBusClient(fullyQualifiedNamespace, new DefaultAzureCredential());
});
```

### Send Message

```csharp
public class OrderQueueService
{
    private readonly ServiceBusClient _serviceBusClient;
    private readonly string _queueName = "orders";

    public OrderQueueService(ServiceBusClient serviceBusClient)
    {
        _serviceBusClient = serviceBusClient;
    }

    public async Task SendOrderAsync(string orderId, string orderData)
    {
        await using var sender = _serviceBusClient.CreateSender(_queueName);

        var message = new ServiceBusMessage(orderData)
        {
            MessageId = orderId,
            ContentType = "application/json",
            Subject = "OrderCreated"
        };

        // Add custom properties
        message.ApplicationProperties.Add("OrderId", orderId);
        message.ApplicationProperties.Add("Priority", "High");

        await sender.SendMessageAsync(message);
    }

    public async Task ScheduleOrderAsync(string orderId, string orderData, DateTimeOffset scheduleTime)
    {
        await using var sender = _serviceBusClient.CreateSender(_queueName);

        var message = new ServiceBusMessage(orderData)
        {
            MessageId = orderId
        };

        await sender.ScheduleMessageAsync(message, scheduleTime);
    }
}
```

### Receive Messages

```csharp
public class OrderProcessorService : BackgroundService
{
    private readonly ServiceBusClient _serviceBusClient;
    private readonly ILogger<OrderProcessorService> _logger;
    private readonly string _queueName = "orders";

    public OrderProcessorService(
        ServiceBusClient serviceBusClient,
        ILogger<OrderProcessorService> logger)
    {
        _serviceBusClient = serviceBusClient;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await using var processor = _serviceBusClient.CreateProcessor(_queueName, new ServiceBusProcessorOptions
        {
            MaxConcurrentCalls = 10,
            AutoCompleteMessages = false,
            PrefetchCount = 20
        });

        processor.ProcessMessageAsync += ProcessMessageAsync;
        processor.ProcessErrorAsync += ProcessErrorAsync;

        await processor.StartProcessingAsync(stoppingToken);

        // Wait until cancellation
        await Task.Delay(Timeout.Infinite, stoppingToken);

        await processor.StopProcessingAsync();
    }

    private async Task ProcessMessageAsync(ProcessMessageEventArgs args)
    {
        var body = args.Message.Body.ToString();
        var orderId = args.Message.ApplicationProperties["OrderId"].ToString();

        try
        {
            _logger.LogInformation("Processing order {OrderId}", orderId);

            // Process order
            await ProcessOrderAsync(body);

            // Complete the message
            await args.CompleteMessageAsync(args.Message);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to process order {OrderId}", orderId);

            // Dead-letter the message
            await args.DeadLetterMessageAsync(args.Message, "ProcessingFailed", ex.Message);
        }
    }

    private Task ProcessErrorAsync(ProcessErrorEventArgs args)
    {
        _logger.LogError(args.Exception, "Service Bus error: {ErrorSource}", args.ErrorSource);
        return Task.CompletedTask;
    }

    private Task ProcessOrderAsync(string orderData)
    {
        // Implementation...
        return Task.CompletedTask;
    }
}
```

---

## Topic/Subscription Patterns

### Send to Topic

```csharp
public class EventPublisherService
{
    private readonly ServiceBusClient _serviceBusClient;
    private readonly string _topicName = "order-events";

    public EventPublisherService(ServiceBusClient serviceBusClient)
    {
        _serviceBusClient = serviceBusClient;
    }

    public async Task PublishOrderCreatedAsync(string orderId)
    {
        await using var sender = _serviceBusClient.CreateSender(_topicName);

        var message = new ServiceBusMessage(JsonSerializer.Serialize(new
        {
            OrderId = orderId,
            Timestamp = DateTime.UtcNow
        }))
        {
            Subject = "OrderCreated",
            CorrelationId = Guid.NewGuid().ToString()
        };

        await sender.SendMessageAsync(message);
    }
}
```

### Subscribe to Topic

```csharp
public class NotificationSubscriberService : BackgroundService
{
    private readonly ServiceBusClient _serviceBusClient;
    private readonly string _topicName = "order-events";
    private readonly string _subscriptionName = "notification-service";

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await using var processor = _serviceBusClient.CreateProcessor(
            _topicName,
            _subscriptionName,
            new ServiceBusProcessorOptions
            {
                MaxConcurrentCalls = 5,
                AutoCompleteMessages = false
            });

        processor.ProcessMessageAsync += async args =>
        {
            if (args.Message.Subject == "OrderCreated")
            {
                await SendNotificationAsync(args.Message.Body.ToString());
                await args.CompleteMessageAsync(args.Message);
            }
        };

        processor.ProcessErrorAsync += args =>
        {
            _logger.LogError(args.Exception, "Notification subscriber error");
            return Task.CompletedTask;
        };

        await processor.StartProcessingAsync(stoppingToken);
        await Task.Delay(Timeout.Infinite, stoppingToken);
        await processor.StopProcessingAsync();
    }

    private Task SendNotificationAsync(string orderData)
    {
        // Send notification...
        return Task.CompletedTask;
    }
}
```

---

## Message Sessions (Ordered Processing)

```csharp
public async Task SendSessionMessageAsync(string sessionId, string message)
{
    await using var sender = _serviceBusClient.CreateSender("orders");

    var serviceBusMessage = new ServiceBusMessage(message)
    {
        SessionId = sessionId  // All messages with same SessionId processed in order
    };

    await sender.SendMessageAsync(serviceBusMessage);
}

// Process with sessions
var processor = _serviceBusClient.CreateSessionProcessor("orders", new ServiceBusSessionProcessorOptions
{
    MaxConcurrentSessions = 5,
    SessionIdleTimeout = TimeSpan.FromMinutes(1)
});

processor.ProcessMessageAsync += async args =>
{
    _logger.LogInformation("Processing message in session {SessionId}", args.SessionId);
    await ProcessMessageAsync(args.Message.Body.ToString());
    await args.CompleteMessageAsync(args.Message);
};
```

---

## Dead-Letter Queue Processing

```csharp
public async Task ReprocessDeadLetterMessagesAsync()
{
    var deadLetterQueuePath = $"orders/$DeadLetterQueue";

    await using var receiver = _serviceBusClient.CreateReceiver(deadLetterQueuePath);

    var messages = await receiver.ReceiveMessagesAsync(maxMessages: 100);

    foreach (var message in messages)
    {
        try
        {
            // Attempt to reprocess
            await ProcessMessageAsync(message.Body.ToString());

            // Complete from dead-letter queue
            await receiver.CompleteMessageAsync(message);

            // Optionally, resend to main queue
            await using var sender = _serviceBusClient.CreateSender("orders");
            await sender.SendMessageAsync(new ServiceBusMessage(message.Body)
            {
                MessageId = message.MessageId
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to reprocess dead-letter message {MessageId}", message.MessageId);
        }
    }
}
```

---

## Bicep Configuration

```bicep
resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = {
  name: serviceBusNamespaceName
  location: location
  sku: {
    name: 'Standard'
    tier: 'Standard'
  }
}

resource queue 'Microsoft.ServiceBus/namespaces/queues@2022-10-01-preview' = {
  parent: serviceBusNamespace
  name: 'orders'
  properties: {
    maxDeliveryCount: 10
    deadLetteringOnMessageExpiration: true
    defaultMessageTimeToLive: 'P14D' // 14 days
    lockDuration: 'PT5M' // 5 minutes
  }
}

resource topic 'Microsoft.ServiceBus/namespaces/topics@2022-10-01-preview' = {
  parent: serviceBusNamespace
  name: 'order-events'
  properties: {
    defaultMessageTimeToLive: 'P7D'
  }
}

resource subscription 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2022-10-01-preview' = {
  parent: topic
  name: 'notification-service'
  properties: {
    deadLetteringOnMessageExpiration: true
    maxDeliveryCount: 5
  }
}

// Grant Container App send/receive access
resource serviceBusRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  scope: serviceBusNamespace
  name: guid(serviceBusNamespace.id, principalId, 'Azure Service Bus Data Owner')
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '090c5cfd-751d-490a-894a-3ce6f1109419')
    principalId: principalId
  }
}
```

---

## Best Practices

### Idempotent Message Handling

```csharp
private async Task<bool> IsMessageProcessedAsync(string messageId)
{
    await using var db = await _dbFactory.CreateDbContextAsync();
    return await db.ProcessedMessages.AnyAsync(m => m.MessageId == messageId);
}

private async Task ProcessMessageAsync(ProcessMessageEventArgs args)
{
    if (await IsMessageProcessedAsync(args.Message.MessageId))
    {
        // Already processed, complete and skip
        await args.CompleteMessageAsync(args.Message);
        return;
    }

    // Process message...

    // Mark as processed
    await using var db = await _dbFactory.CreateDbContextAsync();
    db.ProcessedMessages.Add(new ProcessedMessage
    {
        MessageId = args.Message.MessageId,
        ProcessedAt = DateTime.UtcNow
    });
    await db.SaveChangesAsync();

    await args.CompleteMessageAsync(args.Message);
}
```

### Exponential Backoff on Errors

```csharp
private async Task ProcessWithRetryAsync(ServiceBusReceivedMessage message)
{
    int maxRetries = 3;
    int attempt = 0;

    while (attempt < maxRetries)
    {
        try
        {
            await ProcessMessageAsync(message.Body.ToString());
            return;
        }
        catch (Exception ex)
        {
            attempt++;
            if (attempt >= maxRetries) throw;

            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
        }
    }
}
```

---

## References

- [Azure Service Bus Documentation](https://learn.microsoft.com/azure/service-bus-messaging/)
- [Service Bus .NET SDK](https://learn.microsoft.com/dotnet/api/overview/azure/messaging.servicebus-readme)

---

*MORPH-SPEC by Polymorphism Tech*
