Mittelstand Radar: Buying signals from the German mid-market — secure your first report edition.Join the waitlist
Abstract architectural drawings and blueprints on a light table
Back to blog
dotnetaspirearchitecture

Aspire 13: New Architecture, New Developer Experience

Sven HennessenDevOps

Aspire 13 didn't just get new features – the entire internal architecture was rebuilt. The pipeline model, single-file AppHost, and TypeScript support change how you work with Aspire.

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:

CommandFunction
aspire deployRun deployment
aspire publishGenerate deployment artifacts (Helm charts, Compose files)
aspire destroyTear down deployment
aspire runStart AppHost locally
aspire doctorEnvironment diagnostics (versions, conflicts)
aspire integration listList available hosting integrations
aspire logs --searchRetrieve logs with server-side filtering
aspire lsList 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).

Need support?

Want to adopt the new pipeline model in your existing Aspire projects, or build custom deployment steps via WithPipelineStepFactory, but unsure how to approach the migration? We're happy to help! Just get in touch and we'll work together to get your Aspire architecture ready for version 13.

Get in touch