Aspire — Cloud-Native
Key Points
- .NET Aspire is Microsoft's cloud-native dev experience for distributed apps. Define resources (services, DBs, queues, caches) in AppHost project; run locally with
aspire run. - AppHost is the orchestrator — declares the topology of your app. Uses standard .NET DI; not YAML.
- Built-in dev dashboard — service graph, logs, traces, metrics. No setup. Live, hot-reload-friendly.
- Service discovery built-in — services reference each other by logical name; AppHost wires endpoints.
- Deployment:
azd upfor Azure (Container Apps); Bicep/manifest export for K8s. Aspire 9+ has rich deployment story.
Concepts (deep dive)
What Aspire is
.NET Aspire is a distributed-app development experience for .NET — not a runtime, not a service mesh, not a deployment target. It's the layer that sits between your services during development: it starts every part of your system (your .NET projects, a Postgres container, a Redis container, a RabbitMQ broker), wires them together so services find each other by name, threads OpenTelemetry through everything, and lights up a dashboard that shows logs, traces, and metrics across the entire topology — all from aspire run or F5.
┌───────────────────────────────────────┐
│ AppHost project (C# code) │
│ builder.AddRedis("cache") │
│ builder.AddPostgres("db")… │
│ builder.AddProject<Api>("api") │
│ .WithReference(redis, db) │
└───────────────┬───────────────────────┘
│ aspire run / F5
┌─────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ redis (docker)│ │ pg (docker) │ │ api (.NET) │
│ + auto config │ │ + auto config │ │ wired up via │
│ for clients │ │ for clients │ │ service disco │
└───────────────┘ └───────────────┘ └───────────────┘
▲ │
│ OpenTelemetry │
│ ◄────────────────────────┘
┌───────────────────────┐
│ Aspire dashboard │
│ logs / traces / │
│ metrics / resources │
└───────────────────────┘
Why Aspire exists: the "F5 to run the whole stack" experience for a distributed .NET app used to be a folder full of docker-compose.yml, hand-wired connection strings in five appsettings.Development.json files, and no unified view of what was happening across services. Aspire makes the topology code (typed C#, refactorable, debuggable), and the runtime view (the dashboard) is built in. It's a productivity layer; the deployed app still runs on Container Apps, AKS, or wherever you target.
AppHost as the topology
The AppHost project is a regular .NET console app whose Program.cs declares what the distributed system is made of. Resources (AddRedis, AddPostgres, AddProject<…>) become nodes in a graph; WithReference becomes edges. At runtime, Aspire turns the graph into actual processes and containers, and automatically injects connection strings and discovery URLs into the consuming projects' configuration. No appsettings rituals, no docker-compose.yml.
The same AppHost — without changes — also feeds the deployment story: azd up reads the graph and provisions equivalent Azure resources (Container Apps, Service Bus, Cosmos DB) wired together the same way.
AppHost project
// MyApp.AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddRedis("cache");
var postgres = builder.AddPostgres("db")
.WithDataVolume()
.AddDatabase("orders");
var queue = builder.AddRabbitMQ("rabbit");
var api = builder.AddProject<Projects.MyApp_Api>("api")
.WithReference(postgres)
.WithReference(redis)
.WithReference(queue);
var web = builder.AddProject<Projects.MyApp_Web>("web")
.WithReference(api);
builder.Build().Run();
aspire run (or F5 in VS) starts all of it: containers for redis/postgres/rabbit, .NET projects for api/web, opens dashboard.
ServiceDefaults
Every Aspire-aware project references MyApp.ServiceDefaults — a shared library setting up: - OpenTelemetry (traces, metrics, logs). - Health checks. - Service discovery (AddServiceDiscovery). - HttpClient resilience.
public static class Extensions
{
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http => http
.AddStandardResilienceHandler()
.AddServiceDiscovery());
return builder;
}
}
Service discovery
Hardcoded base addresses (https://localhost:5001) are the root of "works in dev, breaks in prod" — they leak the topology into application code. Aspire's service-discovery scheme replaces the host with a logical name (https+http://api) that the runtime resolves to whatever endpoint AppHost actually wired up: a local port in dev, a K8s service in cluster, a Container Apps URL in Azure. The application code never changes between environments.
https+http://api resolves to whatever AppHost wired up (local Docker port, K8s service, etc.). Same code in dev and prod.
Connection strings
// In API project
builder.AddNpgsqlDbContext<AppDb>("orders"); // Aspire integration
builder.AddRedisClient("cache");
builder.AddRabbitMQClient("rabbit");
Configuration injected from AppHost. No hardcoded connection strings.
Dev dashboard
The dashboard is what makes Aspire feel different from docker compose up. Every service registered in AppHost streams logs, OTel traces, and OTel metrics to the dashboard out of the box — no agent setup, no Jaeger, no Grafana. A failing request in the web tier is one click from the corresponding trace through the API and into the database. Same OTel pipeline lights up Application Insights / Jaeger / Honeycomb in production.
Shows: - Resources: all running services with status, logs, env, endpoints. - Traces: distributed traces across services. - Logs: aggregated structured logs. - Metrics: counters, gauges per service.
Backed by OpenTelemetry — same data shipping to prod observability.
Aspire integrations (NuGet packages)
An integration is a paired set of NuGet packages: a hosting package the AppHost uses to declare the resource, and a client package the consuming project uses to consume it. The hosting package knows how to start the container (image, env vars, ports); the client package knows how to register the typed client in DI and pick up the auto-injected connection string. Adding a Postgres database to your topology is one line in AppHost and one line in the consumer.
| Package | Adds |
|---|---|
Aspire.Hosting.Postgres | Postgres container |
Aspire.Hosting.SqlServer | SQL Server container |
Aspire.Hosting.Redis | Redis container |
Aspire.Hosting.Azure.ServiceBus | Service Bus emulator + cloud |
Aspire.Hosting.Azure.Storage | Azurite |
Aspire.Hosting.Azure.CosmosDB | Cosmos emulator + cloud |
Aspire.Hosting.RabbitMQ | RabbitMQ |
Aspire.Hosting.Kafka | Kafka |
Aspire.Hosting.Ollama | Ollama for local LLMs |
Each integration: container resource + auto-configured client in consuming projects.
AI integrations
Aspire bundles Ollama for local LLM dev. See Ollama & Aspire.
Resource customization
var redis = builder.AddRedis("cache")
.WithImage("redis", "7-alpine")
.WithDataVolume()
.WithEnvironment("REDIS_PASSWORD", "secret")
.WithLifetime(ContainerLifetime.Persistent); // survives across runs
Persistent state in dev
Container data persists across aspire run cycles. Faster onboarding (DB schema not recreated each time).
Deployment
The AppHost graph is the bridge between dev and prod. azd reads it and produces an Azure deployment (Container Apps by default, with managed identities and the right RBAC for each resource); aspire publish emits Bicep or K8s manifests for review. The promise: the same code that runs your topology locally describes how it deploys.
azd up (Azure Container Apps)
azd reads AppHost; provisions Azure resources (Container Apps, Service Bus, Cosmos, etc.); deploys.
Bicep generation
Outputs Bicep templates for the topology — review and apply manually.
Kubernetes
Aspire 9+ has aspire publish with Kubernetes target — emits Helm charts / manifests.
What Aspire isn't
- Not a service mesh — uses .NET service discovery; orchestration is dev-time.
- Not a runtime — production runs on Container Apps / K8s / VM.
- Not Docker Compose replacement — but for .NET apps, often easier.
Aspire vs Docker Compose
| Aspect | Aspire | Compose |
|---|---|---|
| Definition | C# / typed | YAML |
| Service discovery | Built-in | Manual |
| Dashboard | Yes | None |
| .NET integrations | First-class | Generic |
| Cross-language | Limited | Universal |
For .NET-heavy apps, Aspire wins. For polyglot, Compose still useful.
Multiple environments
if (builder.Environment.IsDevelopment())
builder.AddAzureCosmosDB("cosmos").RunAsEmulator(); // emulator
else
builder.AddAzureCosmosDB("cosmos"); // real Azure
Same code; environment-specific behavior.
Testing
public class IntegrationTests : IAsyncLifetime
{
private DistributedApplication _app = null!;
public async Task InitializeAsync()
{
var builder = await DistributedApplicationTestingBuilder.CreateAsync<Projects.MyApp_AppHost>();
_app = await builder.BuildAsync();
await _app.StartAsync();
}
[Fact]
public async Task GetUsers_returns_ok()
{
var http = _app.CreateHttpClient("api");
var r = await http.GetAsync("/users");
Assert.True(r.IsSuccessStatusCode);
}
public Task DisposeAsync() => _app.DisposeAsync().AsTask();
}
Real distributed integration tests with all dependencies.
Performance / cold start
Aspire dev experience is fast on warm caches. First run downloads images. Subsequent: ~5-10s startup for typical app.
When NOT Aspire
- Single-project app — overkill.
- Polyglot stack — limited non-.NET support.
- Existing Helm/Compose-driven dev — disruption may not be worth.
Code: correct vs wrong
❌ Wrong: hardcoded connection strings
✅ Correct: AppHost-injected
❌ Wrong: ad-hoc HttpClient base addresses
✅ Correct: service discovery
Design patterns for this topic
Pattern 1 — "AppHost as topology"
- Intent: declarative app composition.
Pattern 2 — "ServiceDefaults shared lib"
- Intent: OTel + resilience + discovery in one line.
Pattern 3 — "Aspire integrations for resources"
- Intent: typed; auto-wired clients.
Pattern 4 — "azd for Azure deploy"
- Intent: AppHost → Container Apps in one command.
Pattern 5 — "Persistent volumes for dev DB"
- Intent: fast iteration.
Pros & cons / trade-offs
| Aspect | Pros | Cons |
|---|---|---|
| Aspire dev | Fast loop; observability | New tool to learn |
| azd deploy | Quick to Azure | Azure-only convenience |
| ServiceDefaults | Consistent | Opinionated |
| Compose | Polyglot | More YAML |
When to use / when to avoid
- Use for new multi-project .NET apps.
- Use for distributed app dev experience.
- Avoid for single-project apps.
- Avoid if non-.NET dominates.
Interview Q&A
Q1. What's Aspire? .NET cloud-native dev experience. Declarative app topology in C# AppHost; runs locally with dashboard.
Q2. AppHost role? Defines services, dependencies, dev-time resources (containers). Wires service discovery and config.
Q3. ServiceDefaults? Shared lib applying OTel, resilience, discovery. Each project references it.
Q4. Service discovery in Aspire? https+http://api URL pattern. AppHost resolves to runtime endpoint.
Q5. Dev dashboard? http://localhost:18888 — resources, traces, metrics, logs across all services.
Q6. Aspire vs Compose? Aspire: typed C#; .NET-first; dashboard. Compose: YAML; polyglot; broader.
Q7. Deployment target? azd up to Container Apps. Aspire 9+ supports K8s manifest export.
Q8. AI integrations? Ollama, Azure OpenAI, etc. as Aspire resources.
Q9. Persistent dev volumes? .WithDataVolume() keeps DB state across runs.
Q10. Test integrations? DistributedApplicationTestingBuilder boots full topology in tests.
Q11. When NOT Aspire? Single-project apps; non-.NET dominant.
Q12. Aspire and OpenTelemetry? Dashboard backed by OTel. Same telemetry pipeline locally and in prod.
Gotchas / common mistakes
- ⚠️ Hardcoded URLs — defeats discovery.
- ⚠️ Missing ServiceDefaults reference — no OTel.
- ⚠️ Production assumptions dev-only resources (in-memory DB).
- ⚠️ Aspire as runtime — it's not.