What Aspire Is
Explain what the Aspire AppHost, ServiceDefaults and integrations do, and why the deployed API never depends on Aspire at run time.
// AppHost
var db = builder.AddPostgres("postgres").AddDatabase("ordersdb");
builder.AddProject("api", "../Api/Api.csproj").WithReference(db).WaitFor(db);
// API
config.GetConnectionString("ordersdb");Aspire is a set of libraries and tools for running a distributed .NET application on a developer machine: an orders API, its PostgreSQL database, a Redis cache and a worker, started with one command, wired to each other and observable in one dashboard. Version 13 dropped the ".NET" from the name (it was ".NET Aspire" before); the packages in this course are 13.6.3.
The four pieces
| Piece | What it is | Runs in production? |
|---|---|---|
| AppHost | A small C# console project whose Program.cs declares resources: containers, projects, parameters and how they reference each other | No. It is a development and CI orchestrator |
| ServiceDefaults | A shared class library with AddServiceDefaults(): OpenTelemetry, health checks, service discovery and HTTP resilience | Yes, as ordinary code compiled into each service |
| Integrations | Hosting packages (Aspire.Hosting.PostgreSQL) used by the AppHost, and client packages (Aspire.Npgsql.EntityFrameworkCore.PostgreSQL) used by services | Client packages yes, as libraries; hosting packages no |
| Dashboard | An OTLP endpoint and UI showing logs, traces, metrics and resource state | No, it keeps telemetry in memory |
What happens on run
Running the AppHost (dotnet run on it, or aspire run) starts the containers through Docker or Podman, launches each project as a normal process, and starts the dashboard. Each WithReference becomes environment variables on the consuming process: ConnectionStrings__ordersdb for a database, services__inventory__http__0 for another service's endpoint, plus OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME pointing telemetry at the dashboard. Containers get a random host port unless you pin one, and generated passwords are kept in the AppHost's user secrets so they stay stable across runs.
Not a runtime dependency
The API reads builder.Configuration.GetConnectionString("ordersdb"). It does not know whether the value came from an AppHost, a Kubernetes Secret or appsettings.Development.json. That is the design: Aspire produces configuration, and the configuration system you already know consumes it. In production nothing from Aspire runs alongside the API; the platform must supply the same keys.
When a team adopts it
Aspire pays off once there are two or more services plus infrastructure: it replaces a docker-compose file, a page of README set-up steps and per-developer connection strings with code that is compiled, reviewed and identical for everyone. For one API with one database, a compose file or Testcontainers may be all you need. Aspire does not replace your deployment platform; it can generate artifacts for one, covered later in this stage.
Where it goes wrong
- The API works under the AppHost and crashes in staging, because nobody set
ConnectionStrings__ordersdbthere. List every key the AppHost injects and check each one exists in each environment. - The ServiceDefaults template maps
/healthand/aliveonly in Development, so Kubernetes probes get 404 in production until you change it. - Telemetry silently disappears in production because
OTEL_EXPORTER_OTLP_ENDPOINTis not set: the defaults only add the OTLP exporter when that key exists.
Example
When to use it
- Replace a 40-line docker-compose file and a README of manual steps with an AppHost that starts the orders API, Postgres, Redis and a worker with one command.
- Onboard a new developer who has Docker installed: clone, run the AppHost, and every connection string and service URL is injected without copying secrets around.
- Audit which configuration keys staging must supply by reading the AppHost's WithReference calls, before the first deploy fails on a missing ConnectionStrings__ordersdb.
More examples
A minimal AppHost for an orders system
This file is the whole local environment. <code>dotnet run</code> on the AppHost starts a Postgres and a Redis container, starts both APIs as processes and injects <code>ConnectionStrings__ordersdb</code>, <code>ConnectionStrings__cache</code> and <code>services__inventory__http__0</code> into the orders API. A new developer clones the repository, has Docker running, and is productive without copying a single connection string.
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres")
.WithDataVolume();
var ordersDb = postgres.AddDatabase("ordersdb");
var cache = builder.AddRedis("cache");
var inventory = builder.AddProject("inventory", "../Inventory.Api/Inventory.Api.csproj");
builder.AddProject("api", "../Orders.Api/Orders.Api.csproj")
.WithReference(ordersDb).WaitFor(ordersDb)
.WithReference(cache).WaitFor(cache)
.WithReference(inventory)
.WithExternalHttpEndpoints();
builder.Build().Run();The solution layout
The AppHost references the service projects; the services never reference the AppHost. That one-way dependency is what keeps Aspire out of production: the container image for <code>Orders.Api</code> contains the API and ServiceDefaults, and nothing that knows how to start Postgres. The <code>deploy/</code> folder is where the real platform configuration lives.
Orders.slnx
src/
Orders.AppHost/ Aspire.AppHost.Sdk; Program.cs declares resources (dev and CI only)
Orders.ServiceDefaults/ AddServiceDefaults() and MapDefaultEndpoints(), referenced by every service
Orders.Api/ ASP.NET Core API: references ServiceDefaults, reads ConnectionStrings:ordersdb
Inventory.Api/ second service, called through service discovery as http://inventory
Orders.Migrator/ one-shot console app that applies EF Core migrations
tests/
Orders.Api.Tests/
deploy/
k8s/ the production manifests, owned by the team, not by the AppHostSame keys, supplied by Kubernetes
Production provides exactly the keys the AppHost injected locally, from the platform's own sources: a Secret for the connection strings, a cluster DNS name for the inventory service, and the company's OpenTelemetry collector instead of the dashboard. The image is the same one that ran under the AppHost; only its configuration changes.
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
spec:
replicas: 3
selector:
matchLabels:
app: orders-api
template:
metadata:
labels:
app: orders-api
spec:
containers:
- name: api
image: registry.example.com/orders/api:1.14.2
env:
- name: ConnectionStrings__ordersdb
valueFrom:
secretKeyRef:
name: orders-db
key: connection-string
- name: ConnectionStrings__cache
valueFrom:
secretKeyRef:
name: orders-redis
key: connection-string
- name: services__inventory__http__0
value: http://inventory.orders.svc.cluster.local
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://otel-collector.observability:4317
- name: OTEL_SERVICE_NAME
value: orders-api
Discussion