Skip to content

.NET Aspire Integration

Verified by tests

ServiceBusSubscriptionExtensionsTests, ServiceBusReadinessCheckTests, AspireConfigurationGeneratorTests — library CI run #31657041675 (2026-08-13)

.NET Aspire is Microsoft's cloud-native application stack for building distributed applications with batteries-included infrastructure. Whizbang integrates seamlessly with Aspire to provide automatic service discovery, infrastructure provisioning, and local development environments.

Why Aspire + Whizbang?

Aspire solves infrastructure complexity for Whizbang applications:

Challenge Without Aspire With Aspire
Service Bus Setup Manual topic/subscription creation Automatic provisioning from AppHost
Connection Strings Copy-paste from Azure Portal Auto-injected via configuration
Local Development Install/configure Service Bus locally Built-in emulator with zero config
Service Discovery Manual endpoint configuration Automatic service-to-service discovery
Health Checks Manual endpoint setup Built-in dashboards with live monitoring
Observability Configure OpenTelemetry manually Auto-wired distributed tracing

Whizbang + Aspire Benefits: - ✅ Zero Manual Infrastructure - Topics, subscriptions, filters provisioned automatically - ✅ Emulator Support - Local Service Bus emulator for dev/test - ✅ Configuration as Code - AppHost defines infrastructure declaratively - ✅ Multi-Service Orchestration - Run distributed systems locally with dotnet run - ✅ Production Parity - Same code runs locally (emulator) and in Azure


Architecture

Aspire AppHost Pattern

flowchart TD
    subgraph AppHost["AppHost (Program.cs)"]
        subgraph SBResource["Azure Service Bus Resource"]
            Topic["Topic: "whizbang.events""]
            SubInventory["Subscription: &quot;inventory-service&quot;<br/>Filter: Destination = &quot;inventory&quot;"]
            SubNotification["Subscription: &quot;notification-service&quot;<br/>Filter: Destination = &quot;notifications&quot;"]
            SubAnalytics["Subscription: &quot;analytics-service&quot;<br/>Filter: Destination = &quot;analytics&quot;"]

            Topic --> SubInventory
            Topic --> SubNotification
            Topic --> SubAnalytics
        end

        subgraph ServiceProjects["Service Projects (with references)"]
            InventoryService["Inventory Service"]
            NotificationService["Notification Service"]
            AnalyticsService["Analytics Service"]
        end

        InventoryService --> SubInventory
        NotificationService --> SubNotification
        AnalyticsService --> SubAnalytics
    end

    Runtime["Aspire Runtime<br/>- Starts Service Bus emulator (or connects to Azure)<br/>- Provisions topics and subscriptions via Bicep/API<br/>- Injects connection strings into services<br/>- Starts all service projects<br/>- Exposes dashboard at http://localhost:15888"]

    AppHost -->|"dotnet run (AppHost)"| Runtime

Setup

1. Create Aspire AppHost Project

Create Aspire AppHost Project

# Create solution structure
dotnet new sln -n MyDistributedApp
dotnet new aspire-apphost -n MyDistributedApp.AppHost
dotnet sln add MyDistributedApp.AppHost

# Add service projects
dotnet new webapi -n InventoryService
dotnet new webapi -n NotificationService
dotnet sln add InventoryService NotificationService

2. Add Whizbang NuGet Packages

AppHost Project: Add Whizbang NuGet Packages

cd MyDistributedApp.AppHost
dotnet add package Whizbang.Hosting.Azure.ServiceBus

Service Projects: Add Whizbang NuGet Packages (2)

cd ../InventoryService
dotnet add package Whizbang.Core
dotnet add package Whizbang.Transports.AzureServiceBus

3. Configure AppHost

AppHost/Program.cs: Configure AppHost

using Whizbang.Hosting.Azure.ServiceBus;

var builder = DistributedApplication.CreateBuilder(args);

// Add Service Bus resource (emulator for local dev)
var serviceBus = builder.AddAzureServiceBus("messaging")
  .RunAsEmulator();  // Local development; in publish mode Aspire
                     // provisions an Azure namespace via generated Bicep

// Create topic for all events
var topic = serviceBus.AddServiceBusTopic("whizbang-events");

// Add subscriptions with Whizbang correlation filters
var inventorySub = topic.AddServiceBusSubscription("inventory-service")
  .WithDestinationFilter("inventory");  // ⭐ Whizbang extension method

var notificationSub = topic.AddServiceBusSubscription("notification-service")
  .WithDestinationFilter("notifications");

var analyticsSub = topic.AddServiceBusSubscription("analytics-service")
  .WithDestinationFilter("analytics");

// Add service projects with Service Bus references
var inventoryService = builder.AddProject<Projects.InventoryService>("inventory-service")
  .WithReference(serviceBus)
  .WithReference(inventorySub);  // Grants read access to subscription

var notificationService = builder.AddProject<Projects.NotificationService>("notification-service")
  .WithReference(serviceBus)
  .WithReference(notificationSub);

var analyticsService = builder.AddProject<Projects.AnalyticsService>("analytics-service")
  .WithReference(serviceBus)
  .WithReference(analyticsSub);

builder.Build().Run();

What .WithDestinationFilter() Does: - Provisions Azure Service Bus Correlation Filter on the subscription - Filters messages based on ApplicationProperties["Destination"] value - Enables multi-tenant and multi-service routing patterns - Works in both emulator and production


Service Configuration

1. Add Aspire Service Defaults

InventoryService/Program.cs: Add Aspire Service Defaults

var builder = WebApplication.CreateBuilder(args);

// Add Aspire service defaults (health checks, telemetry, service discovery)
builder.AddServiceDefaults();  // ⭐ Essential for Aspire integration

// Get Service Bus connection string injected by Aspire
var connectionString = builder.Configuration.GetConnectionString("messaging")
  ?? throw new InvalidOperationException("Service Bus connection not found");

// Register Whizbang transport
builder.Services.AddAzureServiceBusTransport(connectionString);

// Register receptors, perspectives, etc.
builder.Services.AddWhizbang();

var app = builder.Build();
app.MapDefaultEndpoints();  // Health checks, metrics

app.Run();

How It Works: 1. Aspire injects ConnectionStrings:messaging into app configuration 2. Service reads connection string and registers transport 3. Transport auto-detects emulator vs. production connection 4. Aspire dashboard shows service health and telemetry

2. Verify Aspire Integration

Verify Aspire Integration

# Run AppHost
cd MyDistributedApp.AppHost
dotnet run

# Aspire dashboard opens at http://localhost:15888
# View:
# - Resources (Service Bus, services)
# - Service health status
# - Distributed traces
# - Logs (structured and correlated)

Correlation Filters

WithDestinationFilter Extension

Purpose: Route messages to specific services based on Destination property.

Implementation: WithDestinationFilter Extension

public static IResourceBuilder<AzureServiceBusSubscriptionResource> WithDestinationFilter(
  this IResourceBuilder<AzureServiceBusSubscriptionResource> subscription,
  string destination
) {
  return subscription.WithProperties(sub => {
    sub.Rules.Add(new AzureServiceBusRule("DestinationFilter") {
      CorrelationFilter = new() {
        Properties = { ["Destination"] = destination }
      }
    });
  });
}

Usage Pattern: WithDestinationFilter Extension (2)

// AppHost - provision filters
var inventorySub = topic.AddServiceBusSubscription("inventory-service")
  .WithDestinationFilter("inventory");  // Only messages with Destination = "inventory"

// Publisher - set Destination property
var destination = new TransportDestination(
  Address: "whizbang-events",
  RoutingKey: "inventory-service",
  Metadata: new Dictionary<string, JsonElement> {
    ["Destination"] = JsonSerializer.SerializeToElement("inventory")  // ⭐ Filter value
  }
);

await transport.PublishAsync(envelope, destination);

Result: Only messages with Destination = "inventory" routed to inventory-service subscription.


Emulator vs Production

Development (Emulator)

Development (Emulator)

var serviceBus = builder.AddAzureServiceBus("messaging")
  .RunAsEmulator();  // Starts container with Service Bus emulator

Characteristics: - Runs in Docker container - Accessed via localhost:5672 (AMQP) - No Admin API (port 443 not supported) - Filters provisioned by Aspire at startup - Zero Azure credentials required

Connection String:

Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true

Production (Azure)

Production (Azure)

// Without RunAsEmulator(), publish mode (azd / aspire publish) provisions
// an Azure Service Bus namespace from generated Bicep automatically
var serviceBus = builder.AddAzureServiceBus("messaging");

Characteristics: - Provisions Azure Service Bus Namespace - Generates Bicep infrastructure-as-code - Uses Azure identity for authentication - Full Admin API support for filter management

Connection String (injected by Azure):

Endpoint=sb://my-namespace.servicebus.windows.net/;...


Configuration Generation

AspireConfigurationGenerator

Purpose: Generate C# code for AppHost based on service requirements.

Use Case: Services define their messaging requirements programmatically, generator creates AppHost config.

Example: AspireConfigurationGenerator

using Whizbang.Core.Transports.AzureServiceBus;

// Service defines requirements
var requirements = new[] {
  new TopicRequirement("whizbang-events", "inventory-service"),
  new TopicRequirement("whizbang-events", "notification-service"),
  new TopicRequirement("order-events", "shipping-service")
};

// Generate AppHost code
var code = AspireConfigurationGenerator.GenerateAppHostCode(
  requirements,
  serviceName: "OrderService"
);

Console.WriteLine(code);

Generated Output: AspireConfigurationGenerator (2)

// === Whizbang Service Bus Configuration ===
// Service Bus topics for OrderService service

var orderEventsTopic = serviceBus.AddServiceBusTopic("order-events");
orderEventsTopic.AddServiceBusSubscription("shipping-service");

var whizbangEventsTopic = serviceBus.AddServiceBusTopic("whizbang-events");
whizbangEventsTopic.AddServiceBusSubscription("inventory-service");
whizbangEventsTopic.AddServiceBusSubscription("notification-service");

// ==========================================

Use Case: Copy-paste into AppHost to provision topics/subscriptions.


Readiness Checks

ServiceBusReadinessCheck

Purpose: Verify Service Bus connectivity before accepting traffic.

Pattern: ServiceBusReadinessCheck

using Whizbang.Core.Transports;
using Whizbang.Hosting.Azure.ServiceBus;

builder.Services.AddSingleton<ITransportReadinessCheck, ServiceBusReadinessCheck>();

How It Works: ServiceBusReadinessCheck (2)

public async Task<bool> IsReadyAsync(CancellationToken ct) {
  // 1. Check if transport initialized
  if (!_transport.IsInitialized) {
    return false;
  }

  // 2. Check cache (30-second TTL)
  if (_lastSuccessfulCheck.HasValue &&
      DateTimeOffset.UtcNow - _lastSuccessfulCheck.Value < _cacheDuration) {
    return true;  // Cached result
  }

  // 3. Verify ServiceBusClient is open
  if (_client.IsClosed) {
    return false;
  }

  // 4. Cache successful check
  _lastSuccessfulCheck = DateTimeOffset.UtcNow;
  return true;
}

Benefits: - Prevents accepting requests before Service Bus connection is ready - Cached checks avoid excessive health check overhead - Integrates with Aspire dashboard for real-time status


Multi-Service Patterns

Fan-Out Events

Fan-Out Events

// AppHost - multiple services subscribe to same topic
var topic = serviceBus.AddServiceBusTopic("order-events");

topic.AddServiceBusSubscription("inventory-service")
  .WithDestinationFilter("inventory");

topic.AddServiceBusSubscription("notification-service")
  .WithDestinationFilter("notifications");

topic.AddServiceBusSubscription("analytics-service")
  .WithDestinationFilter("analytics");

topic.AddServiceBusSubscription("audit-service")
  .WithDestinationFilter("audit");

// Publisher - send to multiple destinations
await transport.PublishAsync(envelope, new TransportDestination("order-events", Metadata: CreateDestination("inventory")));
await transport.PublishAsync(envelope, new TransportDestination("order-events", Metadata: CreateDestination("notifications")));
await transport.PublishAsync(envelope, new TransportDestination("order-events", Metadata: CreateDestination("audit")));

Result: Single event published to multiple services via correlation filters.

Service-to-Service Communication

Service-to-Service Communication

// AppHost - inventory service references notification service
var notificationService = builder.AddProject<Projects.NotificationService>("notification-service")
  .WithReference(serviceBus);

var inventoryService = builder.AddProject<Projects.InventoryService>("inventory-service")
  .WithReference(serviceBus)
  .WithReference(notificationService);  // Service discovery

// InventoryService - call NotificationService
var notificationEndpoint = builder.Configuration["services:notification-service:https:0"];
var httpClient = new HttpClient { BaseAddress = new Uri(notificationEndpoint) };

await httpClient.PostAsync("/notify", content);  // Service-to-service HTTP

Aspire provides: - Automatic service endpoint discovery - Load balancing across instances - Health-based routing


Dashboard and Observability

Aspire Dashboard

Run AppHost and open http://localhost:15888.

Features: - Resources Tab: View Service Bus, services, dependencies - Console Logs Tab: Structured logs with correlation IDs - Traces Tab: Distributed tracing across services - Metrics Tab: Service health, request rates, latencies

Whizbang Integration

Automatic Tracing: - All IDispatcher.SendAsync calls create spans - Transport PublishAsync and SubscribeAsync tracked - Correlation IDs propagated across services

Example Trace:

OrderService.DispatcherInvokeReceptor (50ms)
  ├─ OrderReceptor.HandleAsync (45ms)
  │  ├─ Database.Insert (10ms)
  │  └─ Transport.PublishAsync (5ms)
  └─ InventoryService.ReceiveMessage (20ms)
     └─ InventoryReceptor.HandleAsync (18ms)
        └─ Database.Update (15ms)


Best Practices

DO ✅

  • Use .WithDestinationFilter() for multi-service routing
  • Run emulator for local development (zero Azure costs)
  • Add .AddServiceDefaults() to all service projects
  • Reference subscriptions via .WithReference() (grants access)
  • Let publish mode generate Bicep for the production namespace (drop RunAsEmulator())
  • Monitor Aspire dashboard during development
  • Test locally with emulator before deploying to Azure

DON'T ❌

  • ❌ Hardcode connection strings (use Aspire configuration)
  • ❌ Skip .AddServiceDefaults() (breaks health checks and telemetry)
  • ❌ Create topics/subscriptions manually (let Aspire provision)
  • ❌ Use Admin API with emulator (not supported)
  • ❌ Ignore readiness checks (services may accept traffic before ready)
  • ❌ Deploy to production without testing emulator first

Troubleshooting

Problem: "Connection string 'messaging' not found"

Symptoms: Service fails to start with missing connection string error.

Cause: Service not referenced in AppHost or missing .WithReference(serviceBus).

Solution: Problem: 'Connection string 'messaging' not found'

// AppHost - add reference to Service Bus
var inventoryService = builder.AddProject<Projects.InventoryService>("inventory-service")
  .WithReference(serviceBus);  // ⭐ Required for connection string injection

// Service - verify configuration key
var connectionString = builder.Configuration.GetConnectionString("messaging");
// Key must match resource name in AppHost ("messaging")

Problem: Messages Not Filtered Correctly

Symptoms: Subscriber receives all messages, not just filtered ones.

Causes: 1. Filter not provisioned (missing .WithDestinationFilter()) 2. Publisher not setting Destination property 3. Filter value mismatch

Solution: Problem: Messages Not Filtered Correctly

// AppHost - verify filter provisioning
var inventorySub = topic.AddServiceBusSubscription("inventory-service")
  .WithDestinationFilter("inventory");  // Filter value: "inventory"

// Publisher - set matching Destination property
var metadata = new Dictionary<string, JsonElement> {
  ["Destination"] = JsonSerializer.SerializeToElement("inventory")  // Must match filter
};

var destination = new TransportDestination("whizbang-events", "inventory-service", metadata);
await transport.PublishAsync(envelope, destination);

// Verify in Azure Portal:
// Service Bus Namespace → Topics → whizbang-events → Subscriptions → inventory-service → Rules
// Expected: DestinationFilter with Destination = "inventory"

Problem: Emulator Fails to Start

Symptoms: AppHost throws error starting Service Bus emulator.

Causes: 1. Docker not running 2. Port 5672 already in use 3. Emulator image not pulled

Solution: Problem: Emulator Fails to Start

# Verify Docker is running
docker ps

# Pull Service Bus emulator image
docker pull mcr.microsoft.com/azure-messaging/servicebus-emulator:latest

# Check port availability
lsof -i :5672  # Should be empty

# Run AppHost again
dotnet run

Problem: Service Not Appearing in Dashboard

Symptoms: Aspire dashboard shows Service Bus but not service projects.

Cause: Missing .AddServiceDefaults() in service Program.cs.

Solution: Problem: Service Not Appearing in Dashboard

// Service Program.cs - add service defaults
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();  // ⭐ Required for dashboard integration

var app = builder.Build();
app.MapDefaultEndpoints();  // Exposes health/metrics endpoints
app.Run();

Further Reading

Transports: - Azure Service Bus Transport - Service Bus integration details

Infrastructure: - Health Checks - Application health monitoring - Policies - Policy-based routing

Messaging: - Outbox Pattern - Reliable event publishing - Inbox Pattern - Exactly-once processing

External Resources: - .NET Aspire Documentation - Azure Service Bus Emulator


Version 1.0.0 - Foundation Release | Last Updated: 2024-12-12