Aspire for .NET Developers: The Missing Dashboard
How .NET Aspire transforms local development with built-in service orchestration, a real-time dashboard, and one-click cloud deployment β the developer experience .NET always needed.
If you've ever juggled Docker Compose files, environment variables, and connection strings just to run your .NET app locally, Aspire is the answer you didn't know you were waiting for. It gives you a single dashboard to orchestrate, monitor, and debug all your services β databases, caches, message brokers, and your own microservices β from one place.
This guide walks through Aspire from zero to a fully orchestrated local development environment with live telemetry.
What Problem Does Aspire Solve?
Modern .NET applications are rarely a single project. A typical system looks like:
Setting Up Your First Aspire App
Step 1: Create the AppHost
The AppHost is Aspire's orchestrator. It defines what services your app needs and how they connect.
dotnet new aspire-starter -n MyApp
cd MyApp
This creates four projects:
MyApp/
βββ MyApp.AppHost/ # The orchestrator
βββ MyApp.ServiceDefaults/ # Shared config (telemetry, health checks)
βββ MyApp.ApiService/ # Your Web API
βββ MyApp.Web/ # Blazor frontend
Step 2: Define Your Resources
The Program.cs in the AppHost is where the magic happens:
var builder = DistributedApplication.CreateBuilder(args);
// Infrastructure
var postgres = builder.AddPostgres("postgres")
.WithPgAdmin();
var redis = builder.AddRedis("cache");
var rabbitmq = builder.AddRabbitMQ("messaging")
.WithManagementPlugin();
// Application database
var ordersDb = postgres.AddDatabase("orders");
// Services
var apiService = builder.AddProject<Projects.MyApp_ApiService>("api")
.WithReference(ordersDb)
.WithReference(redis)
.WithReference(rabbitmq);
var workerService = builder.AddProject<Projects.MyApp_Worker>("worker")
.WithReference(ordersDb)
.WithReference(rabbitmq);
builder.AddProject<Projects.MyApp_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(apiService)
.WithReference(redis);
builder.Build().Run();
That's it. 15 lines of C# replaces hundreds of lines of Docker Compose YAML, environment variable files, and startup scripts.
Step 3: Use the Resources in Your Services
In your API service, Aspire automatically injects connection strings:
// In MyApp.ApiService/Program.cs
var builder = WebApplication.CreateBuilder(args);
// These extension methods come from Aspire integrations
builder.AddNpgsqlDbContext<OrdersContext>("orders");
builder.AddRedisClient("cache");
builder.AddRabbitMQClient("messaging");
// Standard service configuration
builder.AddServiceDefaults();
var app = builder.Build();
app.MapDefaultEndpoints();
app.MapGet("/orders", async (OrdersContext db) =>
await db.Orders.ToListAsync());
app.Run();
No connection strings in appsettings.json. No environment variables to manage. Aspire handles all service discovery and connection configuration automatically.
The Dashboard: Your New Best Friend
Press F5 (or dotnet run --project MyApp.AppHost), and the Aspire dashboard opens in your browser.
What the Dashboard Shows
- Resources tab β all containers and projects with status, endpoints, and environment variables
- Console logs β stdout/stderr from every service, filterable and searchable
- Structured logs β OpenTelemetry-powered structured logging across all services
- Traces β distributed traces showing request flow across services
- Metrics β real-time metrics for every service (request rate, error rate, latency)
The traces view is particularly powerful. Click on any trace to see the full request path:
webfrontend βββββββββββββββββββββββββββββββββββββββΊ 245ms total β ββ HTTP GET api/orders βββββββββββββββββββββββΊ 42ms β β β ββ Redis GET orders:list ββββΊ 1ms (MISS) β β β ββ PostgreSQL SELECT βββββββββΊ 18ms β β query: SELECT * FROM orders WHERE... β β β ββ Redis SET orders:list ββββΊ 2ms β ββ Render Blazor component ββββββββββββββββββΊ 12ms
Adding Integrations
Aspire has integrations (NuGet packages) for dozens of services:
Database Integrations
// AppHost
var sql = builder.AddSqlServer("sql").AddDatabase("catalog");
var mongo = builder.AddMongoDB("mongo").AddDatabase("analytics");
var cosmos = builder.AddAzureCosmosDB("cosmos").AddDatabase("events");
// Service
builder.AddSqlServerDbContext<CatalogContext>("catalog");
builder.AddMongoDBClient("analytics");
Cloud Service Integrations
// Azure
var storage = builder.AddAzureStorage("storage");
var blobs = storage.AddBlobs("blobs");
var queues = storage.AddQueues("queues");
var servicebus = builder.AddAzureServiceBus("sb");
// AWS (community integration)
var sqs = builder.AddAWSSQSQueue("orders-queue");
Observability Integrations
The ServiceDefaults project configures OpenTelemetry automatically:
// In ServiceDefaults/Extensions.cs
public static IHostApplicationBuilder AddServiceDefaults(
this IHostApplicationBuilder builder)
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler();
http.AddServiceDiscovery();
});
return builder;
}
This gives every service:
- Distributed tracing with automatic span propagation
- Structured logging with correlation IDs
- Health check endpoints (
/health,/alive) - HTTP client resilience (retries, circuit breakers, timeouts)
- Service discovery (no hardcoded URLs)
Custom Resources
Need something Aspire doesn't have a built-in integration for? Create a custom resource:
// Add any container as a resource
var jaeger = builder.AddContainer("jaeger", "jaegertracing/all-in-one")
.WithHttpEndpoint(port: 16686, targetPort: 16686, name: "ui")
.WithEndpoint(port: 4317, targetPort: 4317, name: "otlp");
// Add an executable
var mockServer = builder.AddExecutable("mock-api", "node",
workingDirectory: "../mock-server",
"index.js")
.WithHttpEndpoint(port: 3001);
Deployment: From Local to Cloud
Aspire isn't just for local development. The same AppHost model deploys to Azure:
# Install the Azure Developer CLI
az extension add --name containerapp
# Deploy to Azure Container Apps
azd init
azd up
Aspire generates the infrastructure automatically:
- Each project becomes a Container App
- PostgreSQL becomes Azure Database for PostgreSQL
- Redis becomes Azure Cache for Redis
- RabbitMQ becomes Azure Service Bus
- Connection strings are injected via managed identity
Same code. Same AppHost. Different environment.
Testing with Aspire
Aspire includes a testing framework for integration tests:
[Fact]
public async Task GetOrders_ReturnsOk()
{
// Arrange - spin up the full distributed app
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.MyApp_AppHost>();
await using var app = await appHost.BuildAsync();
await app.StartAsync();
// Act
var httpClient = app.CreateHttpClient("api");
var response = await httpClient.GetAsync("/orders");
// Assert
response.EnsureSuccessStatusCode();
var orders = await response.Content
.ReadFromJsonAsync<List<Order>>();
Assert.NotNull(orders);
}
This spins up the entire distributed application β all containers, all projects β runs your test, and tears everything down. Integration tests that actually test integration.
Common Patterns
Health-Check-Driven Startup
Aspire can wait for dependencies before starting services:
var apiService = builder.AddProject<Projects.MyApp_ApiService>("api")
.WithReference(ordersDb)
.WaitFor(ordersDb) // Don't start API until DB is healthy
.WaitFor(redis); // Don't start API until cache is ready
Environment-Specific Configuration
if (builder.Environment.IsDevelopment())
{
var postgres = builder.AddPostgres("postgres")
.WithPgAdmin(); // Only add PgAdmin in dev
}
else
{
var postgres = builder.AddAzurePostgresFlexibleServer("postgres");
}
Shared Projects Across Services
var sharedLib = builder.AddProject<Projects.MyApp_Shared>("shared");
var api = builder.AddProject<Projects.MyApp_ApiService>("api")
.WithReference(sharedLib);
var worker = builder.AddProject<Projects.MyApp_Worker>("worker")
.WithReference(sharedLib);
Aspire vs. Docker Compose
| Feature | Docker Compose | Aspire |
|---|---|---|
| Language | YAML | C# |
| Service discovery | Manual DNS/env vars | Automatic |
| Connection strings | Manual env vars | Auto-injected |
| Health checks | Manual configuration | Built-in |
| Observability | Add Jaeger/Prometheus manually | Built-in dashboard |
| Debugging | Attach debugger manually | F5 debugging |
| Cloud deployment | Write Terraform/Bicep separately | azd up |
| IDE integration | None | Full IntelliSense |
When NOT to Use Aspire
Aspire is excellent for .NET-centric applications, but it's not for everything:
- Polyglot teams β if your services are Python, Go, and Java, Aspire adds minimal value
- Existing Docker Compose setups β if your compose file works and your team knows it, migration has a cost
- Non-Azure deployments β while Aspire works locally for any stack, cloud deployment is Azure-first
- Simple single-project apps β Aspire adds orchestration overhead that a single API doesn't need
Getting Started Today
# Install the Aspire workload
dotnet workload install aspire
# Create a new Aspire app
dotnet new aspire-starter -n MyApp
# Run it
cd MyApp
dotnet run --project MyApp.AppHost
The dashboard opens automatically. You'll see your services starting, logs streaming, and traces appearing β all in under a minute.
Aspire is the developer experience .NET always deserved. It takes the pain out of distributed development and replaces it with a dashboard that makes you feel like you're actually in control.
Published by the TechAI Explained Team.
π Support TechAI Explained
Free tutorials, open source, community-driven. Help us keep creating.