.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: "inventory-service"<br/>Filter: Destination = "inventory""]
SubNotification["Subscription: "notification-service"<br/>Filter: Destination = "notifications""]
SubAnalytics["Subscription: "analytics-service"<br/>Filter: Destination = "analytics""]
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
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):
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