๐Ÿ“ฌ Part 1 — Building a Queue-Based Microservice with Azure Service Bus and .NET

Modern microservices need reliable, decoupled communication. Instead of services directly calling each other (leading to tight coupling and cascading failures), we can use Azure Service Bus as a message broker.

In this first part of the series, we’ll build a simple system:

  • Order API (producer) → ASP.NET Core Web API that accepts orders and enqueues them into Service Bus.
  • Order Worker (consumer) → .NET Worker Service that listens to the queue, processes messages, and handles retries.

By the end, you’ll have a working end-to-end demo with queues, error handling, and DLQ (Dead Letter Queue).


๐Ÿ—️ Architecture Overview

Client → HTTP POST /api/orders → Order API → Azure Service Bus Queue → Order Worker → Processing
  • OrderApi: Places orders into orders-queue.
  • OrderWorker: Reads messages from the queue and processes them.
  • DLQ: Stores failed messages after max retry attempts.

⚙️ Step 1 — Prerequisites

  • .NET 7+ SDK
  • Azure subscription with Service Bus
  • (Optional) Docker, if you want to containerize

Create Service Bus resources:

# Create namespace
az servicebus namespace create -g myRg -n mysbnamespace --location eastus

# Create queue
az servicebus queue create -g myRg --namespace-name mysbnamespace -n orders-queue --max-delivery-count 10

# Fetch connection string
az servicebus namespace authorization-rule keys list -g myRg --namespace-name mysbnamespace --name RootManageSharedAccessKey

⚙️ Step 2 — Project Setup

Create solution with two projects:

dotnet new sln -n ServiceBusSample
dotnet new webapi -n OrderApi
dotnet new worker -n OrderWorker
dotnet sln add OrderApi/OrderApi.csproj OrderWorker/OrderWorker.csproj

Add Service Bus SDK to both:

dotnet add OrderApi package Azure.Messaging.ServiceBus
dotnet add OrderWorker package Azure.Messaging.ServiceBus

๐Ÿ“ค Step 3 — Order API (Producer)

Define Model

Models/Order.cs

public record Order(int Id, string Product, int Quantity, decimal Price, DateTime CreatedAt);

Add ServiceBus Sender

Services/ServiceBusSenderService.cs

using Azure.Messaging.ServiceBus;

public class ServiceBusSenderService : IAsyncDisposable
{
    private readonly ServiceBusClient _client;
    private readonly ServiceBusSender _sender;

    public ServiceBusSenderService(IConfiguration config)
    {
        _client = new ServiceBusClient(config["ServiceBus:ConnectionString"]);
        _sender = _client.CreateSender(config["ServiceBus:QueueName"]);
    }

    public async Task SendOrderMessageAsync(Order order)
    {
        var json = System.Text.Json.JsonSerializer.Serialize(order);
        var message = new ServiceBusMessage(json)
        {
            ContentType = "application/json",
            Subject = "OrderPlaced",
            MessageId = Guid.NewGuid().ToString()
        };
        await _sender.SendMessageAsync(message);
    }

    public async ValueTask DisposeAsync()
    {
        await _sender.DisposeAsync();
        await _client.DisposeAsync();
    }
}

Expose API Endpoint

Controllers/OrdersController.cs

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly ServiceBusSenderService _sender;

    public OrdersController(ServiceBusSenderService sender) => _sender = sender;

    [HttpPost]
    public async Task<IActionResult> Post([FromBody] Order order)
    {
        await _sender.SendOrderMessageAsync(order);
        return Accepted(new { orderId = order.Id, status = "queued" });
    }
}

Config

appsettings.json

"ServiceBus": {
  "ConnectionString": "<YOUR_SERVICEBUS_CONNECTION_STRING>",
  "QueueName": "orders-queue"
}

๐Ÿ“ฅ Step 4 — Order Worker (Consumer)

Worker Service Processor

OrderProcessorService.cs

using Azure.Messaging.ServiceBus;
using Microsoft.Extensions.Hosting;

public class OrderProcessorService : BackgroundService
{
    private readonly ServiceBusProcessor _processor;
    private readonly ILogger<OrderProcessorService> _logger;

    public OrderProcessorService(ServiceBusClient client, IConfiguration config, ILogger<OrderProcessorService> logger)
    {
        _logger = logger;
        _processor = client.CreateProcessor(config["ServiceBus:QueueName"], new ServiceBusProcessorOptions
        {
            MaxConcurrentCalls = 4,
            AutoCompleteMessages = false
        });

        _processor.ProcessMessageAsync += HandleMessageAsync;
        _processor.ProcessErrorAsync += ErrorHandler;
    }

    private async Task HandleMessageAsync(ProcessMessageEventArgs args)
    {
        try
        {
            var body = args.Message.Body.ToString();
            _logger.LogInformation("Processing order: {Body}", body);

            // Simulate work
            await Task.Delay(500);

            await args.CompleteMessageAsync(args.Message);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing message");
            await args.AbandonMessageAsync(args.Message);
        }
    }

    private Task ErrorHandler(ProcessErrorEventArgs args)
    {
        _logger.LogError(args.Exception, "Error from processor");
        return Task.CompletedTask;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _processor.StartProcessingAsync(stoppingToken);
        await Task.Delay(Timeout.Infinite, stoppingToken);
    }
}

Program.cs

using Azure.Messaging.ServiceBus;

var host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((ctx, services) =>
    {
        services.AddSingleton(new ServiceBusClient(ctx.Configuration["ServiceBus:ConnectionString"]));
        services.AddHostedService<OrderProcessorService>();
    })
    .Build();

await host.RunAsync();

Config

appsettings.json

"ServiceBus": {
  "ConnectionString": "<YOUR_SERVICEBUS_CONNECTION_STRING>",
  "QueueName": "orders-queue"
}

๐Ÿšจ Step 5 — Dead Letter Queue (DLQ)

Messages that fail after multiple retries are moved to DLQ. You can read them:

var receiver = client.CreateReceiver("orders-queue", new ServiceBusReceiverOptions
{
    SubQueue = SubQueue.DeadLetter
});

var messages = await receiver.ReceiveMessagesAsync(10);
foreach (var msg in messages)
{
    Console.WriteLine($"DLQ: {msg.MessageId} - {msg.DeadLetterReason}");
    await receiver.CompleteMessageAsync(msg);
}

๐Ÿงช Step 6 — Testing Locally

  1. Run OrderApi → POST order:
curl -X POST http://localhost:5000/api/orders \
  -H "Content-Type: application/json" \
  -d '{"id":1,"product":"Keyboard","quantity":2,"price":29.99}'
  1. Run OrderWorker → see logs:
Processing order: {"Id":1,"Product":"Keyboard","Quantity":2,"Price":29.99,"CreatedAt":"..."}

๐Ÿ“Œ Best Practices

  • Use Managed Identity instead of connection strings in production.
  • Implement idempotency (deduplicate MessageId).
  • Always monitor and handle DLQs.
  • Configure retry + exponential backoff.
  • Add Application Insights for telemetry.
  • Use autoscaling (Functions/AKS with KEDA).

๐ŸŽฏ Conclusion

In this part, we built a queue-based messaging system with:

  • ASP.NET Core API to enqueue messages
  • Worker Service to consume and process them
  • DLQ handling for failures

This pattern is the foundation of reliable, decoupled microservices.


๐Ÿ‘‰ In Part 2, we’ll extend this to Topics & Subscriptions (Pub/Sub) where multiple services (Inventory, Email, Billing) listen to the same event.

Comments

Popular posts from this blog

๐Ÿ“ฌ Part 4 — Sessions, Duplicate Detection & Transactions in Azure Service Bus with .NET

๐Ÿ“ฌ Part 3 — Dead Letter Queue, Retries, and Monitoring in Azure Service Bus with .NET

๐Ÿ›’ Part 5 — Real-World E-Commerce Microservices with Azure Service Bus & .NET