Most of the visible changes in Aspire 13 are features: new deployment targets, new languages, new integrations. Less obvious but more fundamental are the changes underneath: how Aspire internally coordinates deployment steps, what the AppHost itself looks like, and how the CLI surface is structured. This article describes the architectural decisions that make everything else possible.
The End of the Publisher Model
In Aspire 9 there was an extension model for deployment: the IDistributedApplicationPublisher interface. Anyone wanting to build a custom deployment path implemented this interface, registered it via WithPublishingCallback, and received a PublishingContext. The model was sequential, hard to test, and tightly coupled to the manifest format.
Aspire 13 removed these APIs entirely. The replacement is the pipeline model.
The Pipeline Model: PipelineStep and Dependency Graph
Instead of a central publisher, resources in the AppHost now contribute their own PipelineStep objects. Each step explicitly declares which other steps it depends on. Aspire builds a directed graph from these and runs independent steps in parallel.
resource.WithPipelineStepFactory((context, steps) =>
{
var build = steps.AddStep("build-image", async ctx => {
// build container image
});
var push = steps.AddStep("push-image", async ctx => {
// push image to registry
}).DependsOn(build);
var deploy = steps.AddStep("deploy", async ctx => {
// deploy to cluster
}).DependsOn(push);
});
The result is a deployment process that Aspire parallelizes wherever dependencies allow. Images for independent services are built and pushed simultaneously; provisioning and build run in parallel as long as no step needs the result of another.
The pipeline is inspectable before it runs:
aspire deploy --list-steps # show all steps and dependencies
aspire deploy --pipeline-log-level debug # verbose output per step
aspire deploy --environment staging # target a specific environment
Deployment State Persistence
A simple but effective change: Aspire 13 remembers between deployments what has already been entered.
In Aspire 9, azd up asked for subscription, region, and resource group on every run. In Aspire 13 these inputs are saved per project and environment in the user profile:
~/.aspire/deployments/<project-hash>/<environment>.json
The next aspire deploy call pre-fills these values. --clear-cache resets the saved state. Anyone switching between multiple environments passes the name via --environment:
aspire deploy --environment production
aspire deploy --environment staging --clear-cache
Single-File AppHost
Previously every Aspire project required its own C# project file with package references. From Aspire 13, an AppHost can be written as a single file without a separate .csproj:
#:sdk Aspire.AppHost.Sdk@13.0.0
#:package Aspire.Hosting.PostgreSQL@13.0.0
#:package Aspire.Hosting.Redis@13.0.0
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var db = builder.AddPostgres("db");
var api = builder.AddProject<Projects.Api>("api")
.WithReference(cache)
.WithReference(db);
builder.Build().Run();
The #:sdk and #:package directives replace the project file. The SDK automatically downloads the specified packages on first start. For smaller setups, prototypes, or demos, the project structure no longer acts as a barrier to entry.
TypeScript AppHost
From Aspire 13.4 (GA), the AppHost can also be written in TypeScript. Instead of a C# project, the entry point is a .mts file:
import { createBuilder } from "@aspire/hosting";
import { addPostgres } from "@aspire/hosting-postgresql";
import { addRedis } from "@aspire/hosting-redis";
const builder = await createBuilder(process.argv);
const cache = addRedis(builder, "cache");
const db = addPostgres(builder, "db");
builder.addProject("api", "../Api/Api.csproj")
.withReference(cache)
.withReference(db);
await builder.build().run();
The TypeScript API mirrors the C# surface: same concepts, same methods, generated from the same XML documentation comments. Aspire validates the TypeScript code before startup.
Anyone orchestrating a TypeScript frontend or a Python backend service no longer needs .NET in the AppHost toolchain – only the Aspire CLI.
The New Aspire CLI
Alongside the pipeline architecture, the CLI surface was restructured. In 9.x the primary path was azd up; from Aspire 13.4 the following commands are stable:
| Command | Function |
|---|---|
aspire deploy | Run deployment |
aspire publish | Generate deployment artifacts (Helm charts, Compose files) |
aspire destroy | Tear down deployment |
aspire run | Start AppHost locally |
aspire doctor | Environment diagnostics (versions, conflicts) |
aspire integration list | List available hosting integrations |
aspire logs --search | Retrieve logs with server-side filtering |
aspire ls | List all AppHosts in the current directory |
aspire doctor is particularly useful in environments where multiple Aspire versions coexist: it checks CLI version, SDK version, installed tools (Helm, Docker), and reports conflicts.
Known gap: aspire doctor currently checks whether Docker is running – but not whether the docker buildx plugin is installed. On fresh Ubuntu installations with docker.io (without docker-buildx), the image build step fails with a misleading error message:
Container runtime 'Docker' is not running or is unhealthy.
The actual cause is the missing buildx plugin. apt-get install -y docker-buildx resolves it. The issue (microsoft/aspire#16118) is known and still open – aspire doctor will cover this check in a future release.
What the Pipeline Model Still Doesn't Solve
The pipeline model makes deployment steps visible and parallelizable – a genuine improvement over the sequential publisher model. One structural gap remains though: values that a service produces only after it starts (Keycloak client secrets, Vault tokens, MinIO access keys) cannot be passed to dependent services within the same pipeline run.
Locally this works seamlessly via ResourceReadyEvent. In deploy mode there is no equivalent yet. The current workaround is a two-deploys pattern: the first deploy starts the service, a custom pipeline step provisions it and saves the values to deployment state. The second deploy reads the state and injects the values. Anyone deploying services of this type should plan for this on the first aspire deploy (microsoft/aspire#18097).
