Processing Azure Service Bus messages locally with .NET Aspire

You don’t need a cloud namespace to prototype a queue-driven worker. With .NET Aspire, you can spin up an Azure Service Bus emulator, wire a Worker Service to a queue, and monitor it all from the Aspire dashboard—no external dependencies.

This post shows a minimal setup:

  • Aspire AppHost that runs the Service Bus emulator
  • A queue (my-queue) + a dead-letter queue
  • A Worker Service that consumes messages
  • Built-in enqueue commands to test locally

Folder layout

10_AzureServiceBus/
├─ AppHost/ # Aspire orchestration
├─ ServiceDefaults/ # shared logging, health, etc.
├─ WorkerService/ # background processor
└─ README.md

To create it:

dotnet new worker -n WorkerService
dotnet new aspire-apphost -n AppHost
dotnet new aspire-servicedefaults -n ServiceDefaults

AppHost: Service Bus emulator + queues

var builder = DistributedApplication.CreateBuilder(args);

// Add Azure Service Bus
var serviceBus = builder.AddAzureServiceBus("servicebus")
                        .RunAsEmulator(e => e.WithLifetime(ContainerLifetime.Persistent))
                        .WithCommands();

var serviceBusQueue = serviceBus.AddServiceBusQueue("my-queue");
serviceBus.AddServiceBusQueue("dead-letter-queue");

// Add the worker and reference the queue
builder.AddProject<Projects.WorkerService>("workerservice")
    .WithReference(serviceBusQueue)
    .WaitFor(serviceBusQueue);

builder. Build().Run();

What’s happening

  • RunAsEmulator(...) starts a local Service Bus emulator container (persisting data across restarts).
  • AddServiceBusQueue("my-queue") declares the queue your worker reads from.
  • WithCommands() exposes handy dashboard commands (like Send message) so you can test without writing a sender.
  • WithReference(serviceBusQueue) injects the right connection settings into the worker.

Worker Service: receive & process messages

WorkerService/Program.cs

using WorkerService;

var builder = Host.CreateApplicationBuilder(args);

// Aspire defaults (logging, health, OTEL, etc.)
builder.AddServiceDefaults();

// Register Service Bus client pointing at "my-queue"
builder.AddAzureServiceBusClient("my-queue");

builder.Services.AddHostedService<Worker>();

var host = builder. Build();
host. Run();

WorkerService/Worker.cs

using Azure.Messaging.ServiceBus;

namespace WorkerService;

public class Worker(ILogger<Worker> logger, ServiceBusClient serviceBusClient) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await using var processor = serviceBusClient.CreateProcessor("my-queue");

        processor.ProcessMessageAsync += MessageHandler;
        processor.ProcessErrorAsync += ErrorHandler;

        await processor.StartProcessingAsync(stoppingToken);

        await Task.Delay(-1, stoppingToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);

        await processor.StopProcessingAsync(stoppingToken);
    }

    private async Task MessageHandler(ProcessMessageEventArgs args)
    {
        logger.LogInformation("Received message: {message}", args.Message.Body.ToString());
        await args.CompleteMessageAsync(args.Message);
    }

    private Task ErrorHandler(ProcessErrorEventArgs args)
    {
        logger.LogError(args.Exception, "Error processing message");
        return Task.CompletedTask;
    }
}

Why this setup?

  • Local, fast feedback: The emulator behaves like real Service Bus for dev scenarios.
  • No secrets: Aspire wires connection strings for you via references.
  • Dash-first DX: Use commands to enqueue messages from the dashboard UI.

Run & test

From the solution root:

dotnet run --project AppHost

In the Aspire dashboard:

  • Find servicebusmy-queue.
  • Use the Send message command (provided by .WithCommands()).
  • Type a payload like:
    {"event":"hello","id":"123"}
    • Open workerservice logs — you’ll see:
    Received message: {"event":"hello","id":"123"}

    Messages that can’t be processed will flow to dead-letter-queue (you can add a processor for it later).

    Production path

    • Swap the emulator for a real Azure Service Bus connection (Aspire supports Azure resources & secrets).
    • Add retry policies, dead-letter handlers, and structured logging.
    • Expose OpenTelemetry to your preferred backend (Aspire already emits signals).

    [Source code]

    That’s all folks!

    Cheers!
    Gašper Rupnik

    {End.}

    Leave a comment

    Website Powered by WordPress.com.

    Up ↑