.NET Aspire: Cloud-Native Apps Without the Boilerplate
Get started with .NET Aspire. Covers the AppHost orchestrator, service defaults, resource provisioning (Redis, SQL Server, RabbitMQ), distributed tracing dashboard, deployment to Azure Container Apps, and when to use Aspire.
What is .NET Aspire?
.NET Aspire (released with .NET 8, production-ready in .NET 9) is an opinionated stack for building observable, production-ready cloud-native applications. It solves the boilerplate that every distributed .NET app needs:
- Local orchestration (replaces hand-written docker-compose for development)
- Service discovery (services find each other by name, not hardcoded URLs)
- Built-in distributed tracing, metrics, and logging dashboard
- Standardised health checks
- Azure provisioning with
azd(Azure Developer CLI)
Aspire is not a framework — it doesn't change how you write ASP.NET Core. It's a development-time orchestrator and a set of opinionated integrations.
Project Structure
An Aspire solution adds two projects to your existing services:
OrderFlow/
├── OrderFlow.AppHost/ ← orchestrator (runs only in development)
│ └── Program.cs
├── OrderFlow.ServiceDefaults/ ← shared OpenTelemetry, health checks, etc.
│ └── Extensions.cs
├── OrderFlow.Orders/ ← your existing service
└── OrderFlow.Products/ ← your existing serviceSetup
# Create a new Aspire-enabled solution
dotnet new aspire-starter -n OrderFlow
# Or add Aspire to an existing solution
dotnet new aspire-apphost -n OrderFlow.AppHost
dotnet new aspire-servicedefaults -n OrderFlow.ServiceDefaultsRequires .NET 8 SDK + Docker Desktop (or Podman).
AppHost — The Orchestrator
The AppHost defines your entire application topology as C# code:
// OrderFlow.AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);
// Infrastructure
var redis = builder.AddRedis("cache");
var postgres = builder.AddPostgres("db")
.WithPgAdmin()
.AddDatabase("ordersdb");
var rabbitmq = builder.AddRabbitMQ("messaging")
.WithManagementPlugin(); // enables http://localhost:15672
// Services — Aspire handles service discovery automatically
var products = builder.AddProject<Projects.OrderFlow_Products>("products")
.WithReference(postgres);
var orders = builder.AddProject<Projects.OrderFlow_Orders>("orders")
.WithReference(postgres)
.WithReference(redis)
.WithReference(rabbitmq)
.WithReference(products); // orders can call products by name "products"
var notifications = builder.AddProject<Projects.OrderFlow_Notifications>("notifications")
.WithReference(rabbitmq);
builder.Build().Run();When you run the AppHost, Aspire:
- Starts all referenced containers (Redis, Postgres, RabbitMQ)
- Starts all services
- Injects connection strings and service URLs as environment variables
- Opens the Aspire dashboard at
http://localhost:15888
ServiceDefaults — Shared Configuration
// OrderFlow.ServiceDefaults/Extensions.cs
public static class Extensions
{
public static IHostApplicationBuilder AddServiceDefaults(
this IHostApplicationBuilder builder)
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler(); // retry, circuit breaker, timeout
http.AddServiceDiscovery(); // resolve "orders" → actual URL
});
return builder;
}
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
app.MapHealthChecks("/health");
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = _ => false // fast liveness — just "am I running?"
});
return app;
}
}In each service:
// Orders/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults(); // adds OTel, health checks, service discovery
// The connection string is injected by Aspire — no hardcoding
builder.AddNpgsqlDbContext<OrderDbContext>("ordersdb");
builder.AddRedisDistributedCache("cache");
var app = builder.Build();
app.MapDefaultEndpoints();
app.MapControllers();
app.Run();Service Discovery
Services reference each other by the name registered in the AppHost:
// orders service calling products service
builder.Services.AddHttpClient<IProductService, ProductService>(client =>
{
// "products" resolves to the actual URL via Aspire service discovery
client.BaseAddress = new Uri("http://products");
});In production (Azure Container Apps), Aspire replaces this with the actual service URL via environment variables.
Built-in Integrations
Aspire provides NuGet packages for common infrastructure — they handle connection string injection and health checks automatically:
# Database
dotnet add package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Aspire.Microsoft.EntityFrameworkCore.SqlServer
# Cache
dotnet add package Aspire.StackExchange.Redis
dotnet add package Aspire.StackExchange.Redis.DistributedCaching
# Messaging
dotnet add package Aspire.RabbitMQ.Client
dotnet add package Aspire.Azure.Messaging.ServiceBus
# Storage
dotnet add package Aspire.Azure.Storage.BlobsUsage:
// Replaces manual AddDbContext + connection string wiring
builder.AddNpgsqlDbContext<AppDbContext>("ordersdb");
// Replaces manual AddStackExchangeRedisCache + connection string
builder.AddRedisDistributedCache("cache");The Aspire Dashboard
Running the AppHost opens a web dashboard at http://localhost:15888 with:
- Resources — all services and containers, their status, health
- Console logs — structured logs from all services in one place
- Structured logs — searchable, filterable log events
- Traces — distributed traces across all services (Jaeger-like UI, built-in)
- Metrics — charts for custom and .NET runtime metrics
This replaces needing to set up Jaeger, Seq, and Prometheus separately for local development.
Deployment to Azure Container Apps
# Install Azure Developer CLI
winget install microsoft.azd
# Initialize Aspire deployment
azd init
# Deploy to Azure (provisions Container Apps, Container Registry, etc.)
azd upAspire generates the Azure infrastructure (Bicep or Terraform) from your AppHost definition. The same WithReference connections that work locally become Azure Container Apps environment variables in production.
Custom Resources (External Services)
// Reference an existing Azure SQL Database
var sql = builder.AddConnectionString("ordersdb",
"Server=my-server.database.windows.net;Database=orders;...");
// Reference an external service
var externalApi = builder.AddParameter("external-api-url");
var orders = builder.AddProject<Projects.OrderFlow_Orders>("orders")
.WithReference(sql)
.WithEnvironment("ExternalApi__BaseUrl", externalApi);When to Use Aspire
Use Aspire when:
- Building a new distributed .NET application
- You have multiple services + supporting infrastructure (Redis, SQL, queues)
- You want production-ready observability without manual setup
- You're deploying to Azure Container Apps
Consider skipping Aspire when:
- Single-service application — overkill
- Non-.NET services in the mix — Aspire orchestrates .NET projects natively
- Existing docker-compose setup that already works well
- Deploying to Kubernetes — Aspire has Kubernetes support but it's less mature
Interview Questions
Q: What problem does .NET Aspire solve? It eliminates the boilerplate of setting up local orchestration, service discovery, distributed tracing, health checks, and Azure deployment for distributed .NET applications. Instead of hand-writing docker-compose files and wiring up OpenTelemetry manually in each service, Aspire provides a standardised, code-first approach.
Q: What is the AppHost and does it run in production?
The AppHost is a development-time orchestrator. It starts containers, injects connection strings, and enables service discovery locally. It does not run in production — when deploying with azd, Aspire generates the actual Azure infrastructure from the AppHost definition.
Q: What is ServiceDefaults and why put it in a separate project?
ServiceDefaults is a shared project that adds standardised OpenTelemetry, health checks, service discovery, and HTTP resilience to every service. Putting it in a separate project means you configure it once and AddServiceDefaults() in every service — ensuring consistent observability across the entire system.
Q: How does service discovery work in Aspire?
In development, the AppHost injects each service's URL as an environment variable when other services reference it with WithReference. The AddServiceDiscovery() extension resolves logical names ("products") to the actual URL at runtime. In production on Azure Container Apps, the same mechanism uses Container Apps environment DNS.
Q: What is the Aspire dashboard and what does it show? A built-in web UI that aggregates structured logs, distributed traces, metrics, and resource status from all services. Replaces the need to run separate Jaeger, Seq, and Prometheus instances locally. Launches automatically when you run the AppHost.
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.