.NET Aspire, now branded simply as Aspire, is Microsoft's code-first toolchain for building, running and deploying distributed applications. It replaces scattered compose files, launch scripts and copied connection strings with a C# AppHost that describes every service, database and cloud resource in your system. This guide targets Aspire 13.5, the current release in September 2026. It covers the resource model, service defaults, the dashboard, integrations, the Aspire CLI, testing and the deployment paths that take one model to Azure, Docker Compose and Kubernetes.
What Is .NET Aspire?#
Aspire is an opinionated stack for cloud-native .NET development, and more recently for polyglot development too. It gives you three things:
- Orchestration. A small program called the AppHost declares your application as a graph of resources and runs the whole graph locally with one command.
- Integrations. Curated NuGet packages model infrastructure such as PostgreSQL, Redis, Kafka or Azure Service Bus in the AppHost. Matching client packages wire the SDKs into your services with health checks, telemetry and resilience already configured.
- Tooling. The Aspire CLI, a dashboard for logs, traces and metrics, a test host and a deployment pipeline that turns the same model into Bicep, Docker Compose files or Helm charts.
Aspire 13.0, released on November 11, 2025, dropped the ".NET" prefix because Python and JavaScript apps became first-class citizens, and version numbers jumped from 9.x straight to 13. Later minor releases added a TypeScript AppHost, now generally available, plus Go and Bun integrations. Aspire 13.5 shipped on August 18, 2026, and the latest patch at the time of writing is 13.5.4.
The Aspire CLI and AppHost SDK require the .NET 10 SDK, but the projects you orchestrate can target .NET 8, .NET 9, .NET 10 or a .NET 11 preview. The support policy is strict: only the latest Aspire release is supported, and a minor version loses support when the next one ships. Plan to upgrade Aspire every few months, even while your services stay on an LTS runtime.
Aspire is not a runtime framework, a service mesh or a hosting platform. Your services reference ordinary libraries and run fine without the AppHost, and production traffic never flows through it.
How Aspire Works: The AppHost and the Resource Model#
The AppHost is a console program built on the Aspire.AppHost.Sdk. It calls DistributedApplication.CreateBuilder, adds resources and then calls Build().Run(). Each resource is a node in the application model. Aspire ships resource types for .NET projects, containers, executables, parameters, connection strings, cloud resources and language-specific apps such as Vite, Node.js, Python and Go.
Resources carry annotations that describe endpoints, environment variables, volumes, health checks and relationships. Three relationships do most of the work:
WithReferenceinjects connection information into a consumer. A database reference becomes aConnectionStrings__catalogdbvariable, and a project reference becomes service discovery configuration. Since Aspire 13, references also expose individual connection properties such asCATALOGDB_URIorCATALOGDB_HOST, which suits Python, Node.js and Java consumers that do not understand .NET connection strings.WaitFordelays a resource until its dependency is running and healthy.WaitForCompletionwaits until a dependency exits successfully, which is ideal for database migration jobs.WithExternalHttpEndpointsmarks endpoints that should be reachable from outside the environment when you deploy.
The same model runs in two modes. In run mode (aspire run), the local orchestrator starts processes and containers, allocates ports, injects configuration and streams telemetry to the dashboard. In publish mode (aspire publish and aspire deploy), pipeline steps registered by compute environments translate the model into deployment artifacts. Branch on builder.ExecutionContext.IsPublishMode when the modes need different resources.
Service discovery ties the pieces together. When service A references service B, the AppHost injects endpoint configuration, and the discovery library resolves logical URIs such as https+http://catalog-api at runtime, preferring HTTPS and falling back to HTTP. Discovery configuration exists only for resources you reference explicitly, which keeps coupling visible in the AppHost.
Getting Started with Aspire 13#
You need the .NET 10 SDK and a container runtime such as Docker Desktop or Podman. Install the CLI with the official script or as a .NET tool. Aspire 13.5 also distributes it through npm, Homebrew, WinGet and other package managers.
# Install the Aspire CLI with the script...
curl -sSL https://aspire.dev/install.sh | bash
# ...or as a .NET global tool
dotnet tool install -g Aspire.Cli
# Create a starter solution: AppHost, ServiceDefaults, API and Blazor front end
aspire new aspire-starter --name Shop --output ./shop
cd shop
# Build and start every resource, then open the dashboard URL printed in the terminal
aspire runThe generated AppHost is short. The version below is a realistic evolution of it, with PostgreSQL, Redis, a migration worker and two services.
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres")
.WithDataVolume()
.WithLifetime(ContainerLifetime.Persistent)
.WithPgAdmin();
var catalogDb = postgres.AddDatabase("catalogdb");
var cache = builder.AddRedis("cache")
.WithLifetime(ContainerLifetime.Persistent);
// Runs EF Core migrations once, then exits
var migrations = builder.AddProject<Projects.Shop_MigrationService>("migrations")
.WithReference(catalogDb)
.WaitFor(catalogDb);
var api = builder.AddProject<Projects.Shop_CatalogApi>("catalog-api")
.WithReference(catalogDb)
.WithReference(cache)
.WaitFor(cache)
.WaitForCompletion(migrations)
.WithHttpHealthCheck("/health");
builder.AddProject<Projects.Shop_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(api)
.WaitFor(api);
builder.Build().Run();Persistent containers with data volumes survive AppHost restarts, so the inner loop does not pay for a fresh database on every run. The Projects.* types are generated from the AppHost's project references, which keeps the model strongly typed. For small or polyglot repositories, Aspire 13 also supports a single-file AppHost with #:package directives and projects referenced by path:
#:sdk Aspire.AppHost.Sdk@13.5.4
#:package Aspire.Hosting.Redis@13.5.4
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
builder.AddProject("api", "../src/Api/Api.csproj")
.WithReference(cache)
.WaitFor(cache);
builder.Build().Run();Service Defaults: OpenTelemetry, Health Checks, Resilience and Service Discovery#
Every service in an Aspire solution references a shared ServiceDefaults project. It is plain source code that you own, not a black-box package, and it exposes two extension methods: AddServiceDefaults on the host builder and MapDefaultEndpoints on the web application.
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
// Client integrations: connection names match the AppHost resource names
builder.AddNpgsqlDbContext<CatalogDbContext>("catalogdb");
builder.AddRedisDistributedCache("cache");
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler();
app.MapGet("/products/{id:int}", async (int id, CatalogDbContext db, CancellationToken ct) =>
await db.Products.FindAsync([id], ct) is { } product
? Results.Ok(product)
: Results.NotFound());
app.MapDefaultEndpoints();
app.Run();In the current template, AddServiceDefaults does four things:
- OpenTelemetry. It enables logging with formatted messages and scopes, metrics for ASP.NET Core,
HttpClientand the runtime, and tracing for ASP.NET Core andHttpClient. Health probe requests are filtered out of traces. The OTLP exporter switches on only whenOTEL_EXPORTER_OTLP_ENDPOINTis set, which the AppHost does automatically in run mode. - Health checks. It registers a trivial
selfcheck taggedlive. Client integrations add their own checks, such as a PostgreSQL connectivity check. - Service discovery. It registers the configuration-based endpoint resolver.
- Resilience.
ConfigureHttpClientDefaultsadds the standard resilience handler (retry, circuit breaker and timeouts) and service discovery to everyHttpClientthe app creates.
MapDefaultEndpoints maps /health, where every check must pass, and /alive, where only checks tagged live must pass. The template maps them only in the Development environment, because health endpoints can leak information or become a denial-of-service target. For production, map them deliberately, for example on a management port that is not routed publicly:
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
// Kestrel must also listen on 8081, e.g. ASPNETCORE_HTTP_PORTS=8080;8081
app.MapHealthChecks("/health").RequireHost("*:8081");
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = registration => registration.Tags.Contains("live")
}).RequireHost("*:8081");
return app;
}Consumers call other services by logical name. The base address below resolves through service discovery locally and through the injected configuration after deployment:
builder.Services.AddHttpClient<CatalogClient>(client =>
client.BaseAddress = new Uri("https+http://catalog-api"));Because the ServiceDefaults project is yours, extend it rather than working around it. Typical additions are EF Core or gRPC instrumentation, an Azure Monitor exporter that the template leaves commented out, or restricting service discovery to HTTPS through ServiceDiscoveryOptions.AllowedSchemes. For deeper coverage of the telemetry side, see the OpenTelemetry in .NET guide. For the resilience pipeline, see resilience with Polly.
The Aspire Dashboard#
aspire run starts a web dashboard that shows every resource with its state, endpoints, environment variables and health, plus console logs, structured logs, distributed traces and metrics. You can start, stop and restart resources, run custom resource commands and follow a request across services. Aspire 13.5 added sharper filtering and interactive terminal sessions through the experimental WithTerminal API.
The dashboard is an OTLP endpoint, so it also runs standalone next to any OpenTelemetry-instrumented app, including apps that do not use an AppHost:
docker run --rm -it -d --name aspire-dashboard \
-p 18888:18888 -p 4317:18889 -p 4318:18890 \
mcr.microsoft.com/dotnet/aspire-dashboard:latestThe UI listens on port 18888, OTLP arrives over gRPC on 4317 and HTTP on 4318, and the login token is printed to the container logs. Telemetry lives in memory with fixed limits, so the dashboard is meant for development and short-term diagnostics. In production, send OTLP to Azure Monitor, an OpenTelemetry Collector or another durable backend. For AI-assisted work, aspire agent init configures an MCP server and skill files so coding agents can list resources, read logs and inspect traces.
Aspire Integrations: Redis, PostgreSQL and Azure Services#
Integrations come in pairs. A hosting integration extends the AppHost with a resource type, such as AddPostgres from Aspire.Hosting.PostgreSQL. A client integration registers the SDK in your service, such as AddNpgsqlDbContext from Aspire.Npgsql.EntityFrameworkCore.PostgreSQL, with health checks, logging, tracing and metrics enabled by default. The pair shares only a connection name. Client integrations also work without an AppHost, provided that a connection string with that name exists in configuration.
| Resource | Hosting package (AppHost) | Client package (service) | Client registration |
|---|---|---|---|
| PostgreSQL | Aspire.Hosting.PostgreSQL | Aspire.Npgsql or Aspire.Npgsql.EntityFrameworkCore.PostgreSQL | AddNpgsqlDataSource, AddNpgsqlDbContext |
| Redis | Aspire.Hosting.Redis | Aspire.StackExchange.Redis plus caching variants | AddRedisClient, AddRedisDistributedCache, AddRedisOutputCache |
| Azure Service Bus | Aspire.Hosting.Azure.ServiceBus | Aspire.Azure.Messaging.ServiceBus | AddAzureServiceBusClient |
| Azure Blob Storage | Aspire.Hosting.Azure.Storage | Aspire.Azure.Storage.Blobs | AddAzureBlobServiceClient |
Use aspire add postgres or aspire add redis to add hosting packages with matching versions. Azure hosting integrations follow a consistent pattern. They generate Bicep for the real Azure service when you publish, and RunAsEmulator or RunAsContainer swap in a local emulator or an open-source container for run mode:
var builder = DistributedApplication.CreateBuilder(args);
var serviceBus = builder.AddAzureServiceBus("messaging")
.RunAsEmulator();
serviceBus.AddServiceBusQueue("orders");
var blobs = builder.AddAzureStorage("storage")
.RunAsEmulator() // Azurite locally, a Storage account in Azure
.AddBlobs("blobs");
var ordersDb = builder.AddAzurePostgresFlexibleServer("pg")
.RunAsContainer() // postgres container locally, Flexible Server in Azure
.AddDatabase("ordersdb");
builder.AddProject<Projects.Orders_Worker>("orders-worker")
.WithReference(serviceBus)
.WithReference(blobs)
.WithReference(ordersDb)
.WaitFor(serviceBus);
builder.Build().Run();AsExisting and PublishAsExisting reference Azure resources that already exist instead of provisioning new ones. To provision real Azure resources during local runs, store Azure:SubscriptionId and Azure:Location with aspire secret set rather than in a committed appsettings file.
Secrets belong in parameters. builder.AddParameter("stripe-key", secret: true) reads Parameters:stripe-key from environment variables, user secrets or other configuration, and the dashboard prompts for missing values. At deployment time the value is resolved or emitted as a placeholder.
The Aspire CLI#
The CLI has grown from a thin wrapper into the main interface for both people and coding agents. New C# templates set AspireUseCliBundle, so dotnet run on the AppHost delegates to aspire run.
| Command | What it does |
|---|---|
aspire new, aspire init | Create a solution from a template, or add Aspire to an existing repository |
aspire run, aspire start, aspire stop | Run the AppHost in the foreground, in the background, or stop it |
aspire add, aspire update | Add integration packages, and update packages or the CLI itself |
aspire describe, aspire logs, aspire otel | Inspect resources, console logs and telemetry from the terminal |
aspire publish, aspire deploy, aspire destroy | Generate artifacts, deploy them, or tear down a deployment |
aspire do | Run a specific pipeline step and its dependencies |
aspire secret, aspire certs, aspire doctor | Manage user secrets and dev certificates, and diagnose the environment |
aspire agent | Configure MCP and skills so AI coding agents can drive the app |
Testing with DistributedApplicationTestingBuilder#
Aspire.Hosting.Testing starts your real AppHost, including containers, inside a test process. That makes it the closest thing to an end-to-end test you can run on a laptop or CI agent. The aspire-xunit, aspire-mstest and aspire-nunit templates set up the project. A typical xUnit v3 test looks like this:
using System.Net;
namespace Shop.Tests;
public class StorefrontTests
{
private static readonly TimeSpan StartupTimeout = TimeSpan.FromSeconds(90);
[Fact]
public async Task Home_page_returns_ok_once_dependencies_are_healthy()
{
var ct = TestContext.Current.CancellationToken;
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.Shop_AppHost>(ct);
appHost.Services.ConfigureHttpClientDefaults(http =>
http.AddStandardResilienceHandler());
await using var app = await appHost.BuildAsync(ct).WaitAsync(StartupTimeout, ct);
await app.StartAsync(ct).WaitAsync(StartupTimeout, ct);
await app.ResourceNotifications
.WaitForResourceHealthyAsync("webfrontend", ct)
.WaitAsync(StartupTimeout, ct);
using var client = app.CreateHttpClient("webfrontend");
using var response = await client.GetAsync("/", ct);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}CreateHttpClient resolves the resource's allocated endpoint, and GetConnectionStringAsync("catalogdb") hands you the real connection string so you can seed or assert on data directly. These tests are slower than unit tests because containers must start. Share one running app per test class through a fixture, keep the number of scenarios focused, and make sure your CI agents have a container runtime. For in-process testing of a single service, WebApplicationFactory and Testcontainers remain the better tools. The integration testing guide compares the approaches.
Best Practices#
- Keep the AppHost declarative. Describe topology, not business logic. Complex setup belongs in dedicated resources, such as a migration worker, that the AppHost waits on.
- Model every dependency with
WaitForand health checks. Startup races between services and databases are the most common local failure, and Aspire prevents them only if you declare the edges. - Use persistent containers and data volumes for stateful dev resources. They make
aspire runfast and keep test data between sessions. - Treat ServiceDefaults as platform code. Version it, review changes and add organization-wide defaults such as exporters and HTTPS-only discovery in one place.
- Expose health endpoints deliberately in production. Map them on an internal port or behind authorization, and point orchestrator probes at them.
- Put secrets in parameters. Mark them
secret: trueand keep local values in user secrets. - Pin and update Aspire as a unit. Keep the AppHost SDK and every
Aspire.*package on one version, and runaspire updatewhen a minor release ships.
Common Pitfalls#
- Expecting
/healthto exist after deployment. The template maps health endpoints only in Development, so probes that target them fail in production until you map them yourself. - Mismatched connection names.
AddNpgsqlDbContextresolves a connection string named after the AppHost resource. A typo produces a configuration error at startup, not a compile error. - Hard-coding URLs and ports. Use logical names such as
https+http://catalog-api. Hard-coded localhost ports break as soon as Aspire allocates different ones or the app is deployed. - Treating Aspire as a runtime dependency. Services should run without the AppHost when given configuration. If a service only works under
aspire run, you have coupled it to development tooling. - Changing a database password after its volume exists. PostgreSQL applies the password only when it initializes an empty data directory, so a new parameter value produces authentication failures until you delete the volume.
- Shipping preview integrations without review. Kubernetes and AKS support is still in preview. Pin the package versions and review the generated charts before they reach production.
When to Use Aspire: Aspire vs Docker Compose vs Scripts#
| Concern | Aspire AppHost | Docker Compose | Scripts and README steps |
|---|---|---|---|
| Definition language | C#, or TypeScript in a TypeScript AppHost | YAML | Shell or PowerShell |
| Running .NET projects | As local processes you can debug from the IDE | Only as containers | Manually, one terminal per service |
| Connection strings and discovery | Injected by WithReference | Hand-written environment variables | Copied between files |
| Startup ordering | WaitFor backed by health checks | depends_on with health conditions | Sleep loops and retries |
| Telemetry | Built-in dashboard over OTLP | Bring your own stack | None |
| End-to-end tests | DistributedApplicationTestingBuilder | External scripts or Testcontainers | Manual |
| Deployment artifacts | Bicep, Compose files or Helm charts | The Compose file itself | None |
Aspire is a strong fit when you have more than two or three services, when developers struggle to run the full system locally, or when you want consistent telemetry and resilience defaults across teams. It adds little for a single API with one database. It also does not replace a platform team's GitOps pipeline; there, use aspire publish to generate artifacts that existing tooling applies.
Frequently Asked Questions#
Is .NET Aspire the same product as Aspire?#
Yes. Aspire 13.0 dropped the ".NET" prefix in November 2025 when Python and JavaScript became first-class, but it is the same project and repository. Most documentation, packages and APIs kept their names, and the C# AppHost remains the primary experience.
Do I need Aspire in production?#
No. The AppHost is a development and deployment tool, and nothing in your production request path depends on it. Your services keep the ServiceDefaults code and client integrations, which are ordinary libraries configured through standard .NET configuration.
Which .NET versions can I use with Aspire 13?#
The Aspire CLI and AppHost SDK require the .NET 10 SDK. The services you orchestrate can target .NET 8, .NET 9 or .NET 10, and templates in 13.5 can also target a .NET 11 preview. Keep production services on an LTS release unless you need a newer feature.
Should I deploy with azd or aspire deploy?#
For new projects, start with aspire deploy, which the Aspire documentation now recommends and which receives new Azure deployment features first. azd remains supported and is reasonable for existing pipelines, but it relies on the deprecated deployment manifest.
Can Aspire deploy to Kubernetes?#
Yes. AddKubernetesEnvironment generates a Helm chart, and aspire deploy installs it into your current kubectl context. The Kubernetes and AKS packages are still prereleases in Aspire 13.5, so review the generated chart and pin versions before you rely on it in production.
Summary#
- Aspire is a code-first stack: an AppHost resource model, paired hosting and client integrations, a dashboard, a CLI and a deployment pipeline.
- Aspire 13.5 is current. It requires the .NET 10 SDK but orchestrates services targeting .NET 8 through a .NET 11 preview, and only the latest release is supported.
- ServiceDefaults gives every service OpenTelemetry, health checks, service discovery and HTTP resilience, with health endpoints mapped only in Development by default.
DistributedApplicationTestingBuilderruns the real system in tests; keep those scenarios focused.aspire publishandaspire deploytarget Container Apps, App Service, Docker Compose and Kubernetes, with Kubernetes still in preview.