← Back to list

Aspire, Top to Bottom: What It Actually Does, and Where It Still Bites

The model, the CLI, the integrations, the dashboard, deployment, and the limitations the marketing page won’t mention. Written by someone…

Krati Varshney in Works On My Machine · 2026-06-27 08:02 · 1 claps · 10.3 min read paywalled
#aspire #dotnet-10 #microservices #devops #software-architecture
Open on Medium ↗
Wiki topics: ECO · Economy · General ☁️ · DevOps & Cloud 🎬 · Film & Television 🏛️ · Architecture

Aspire, Top to Bottom: What It Actually Does, and Where It Still Bites

The model, the CLI, the integrations, the dashboard, deployment, and the limitations the marketing page won’t mention. Written by someone who runs it.

I ran a twelve-service .NET backend for two years before Aspire existed in a form worth using. Local development was a 180-line docker-compose.yml nobody fully owned, four appsettings.Development.json files with the same connection strings copy-pasted between them, and a README whose first instruction was "open three terminals." A new engineer lost most of a day to it. The worst flakiness was an ordering bug: the API booted faster than Postgres, threw on its first migration, and crashed, so the fix everyone learned was "just run it again." We shipped that as tribal knowledge for eighteen months.

I moved the whole thing to Aspire over a weekend. The README is now one line. That is the entire reason I kept it, and most of this article is about whether that tradeoff makes sense for you, because for plenty of teams it does not.

Here is the complete picture, opinions included.

What it is, stated plainly

Aspire is a code-first orchestrator for your inner loop that also emits deployment manifests. You describe your system once, as a graph of resources, in a small project called the AppHost. Aspire stands that graph up locally, injects the connection strings and service URLs between resources, wires every service into one telemetry pipeline, and gives you a dashboard. When you deploy, the same graph generates Docker Compose, Helm charts, or Azure Container Apps definitions.

That is the whole job. The confusion around Aspire comes entirely from expecting more.

It is not a runtime. Your code still runs on .NET, or Python, or Node. It is not a service mesh, it does nothing at the network layer in production. It is not a platform you pay Microsoft to host on. It is not a Kubernetes replacement, it generates the YAML you then run on your own cluster. And it is not a testing framework yet, which is a real gap I will come back to. Strip those misconceptions and what remains is a development-time control plane plus a manifest generator, sharing one model written in code. One source of truth for your topology, used in two places. That part genuinely works.

The rebrand, the version, and the upgrade tax

Update your mental model if it still says “.NET Aspire.” As of the 13.0 release at .NET Conf in November 2025, the project dropped the “.NET” and became Aspire, a multi-language platform. Python and JavaScript are first-class now, and you can author the AppHost itself in TypeScript. The docs moved to aspire.dev and the repo is microsoft/aspire. Current release is 13.4, it requires the .NET 10 SDK regardless of what language your services are in, and the 13.x line has shipped at a punishing cadence: 13.0 in November, 13.1 in January, an enormous 13.2 in March with over 1,100 closed issues, then 13.3 and 13.4.

Now the part the changelog buries and you will feel: every minor release in this line has carried breaking changes. 13.0 obsoleted a batch of APIs and quietly flipped properties between settable and init-only. 13.1 renamed the Azure Redis integration and several connection properties. 13.3 broke startup hooks for Kubernetes, Compose, and AKS, the emulator management endpoints, the dashboard MCP server, and Azure network output names. None of these are catastrophic, all of them cost an afternoon if you skipped the migration notes. Treat Aspire as a fast-moving dependency you pin and upgrade deliberately, not a stable utility you set and forget. Read the breaking-changes section every single bump. I learned that the expensive way on the Redis rename.

The CLI, and the honest read on it

The way in is the Aspire CLI, shipped as a self-extracting bundle that runs even with no global .NET install. That detail tells you the tool stopped being .NET-only.

aspire new          # new app from the interactive template picker
aspire init         # bolt an AppHost (single-file is fine) onto an existing repo
aspire add redis    # add a backing service; the CLI inspects the integration assembly
aspire run          # restore, start the whole graph, open the dashboard

aspire init is the one that matters on a real codebase, because it drops an AppHost into a repo without restructuring your solution. The rest of the surface is built openly for automation and AI coding agents:

aspire start                                # detached, in the background
aspire resource api restart                 # bounce one resource, leave the rest up
aspire wait api --status healthy --timeout 120
aspire run --isolated                       # parallel run: random ports, separate secrets
aspire doctor                               # validate the environment before building
aspire publish                              # emit deployment artifacts

--isolated earns its keep if you work across git worktrees, since it stops two checkouts from fighting over ports and secrets. The candid take on the agent features: roughly half of every recent release is aimed at making Aspire legible to coding agents, the MCP integration, the structured CLI output, the health-check blocking. It is useful and slightly oversold, in the usual proportion. aspire wait makes an agent or a CI script reliable because it can confirm state instead of assuming it. None of it is a reason to adopt Aspire. The AppHost is the reason. The agent tooling is a bonus if you already work that way.

The AppHost is the only part that matters

Everything good here comes from the AppHost. In C# it is a small program around a DistributedApplication builder.

var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var postgres = builder.AddPostgres("postgres")
                      .WithDataVolume();        // survive restarts
var db = postgres.AddDatabase("appdb");
var broker = builder.AddRabbitMQ("broker");
var api = builder.AddProject<Projects.Store_Api>("api")
                 .WithReference(db)
                 .WithReference(cache)
                 .WithReference(broker)
                 .WaitFor(db);
builder.AddProject<Projects.Store_Web>("web")
       .WithReference(api)
       .WaitFor(api);
builder.Build().Run();

AddRedis and AddPostgres pull and run real containers, no Compose file. WithReference(db) does the work people miss: it injects the appdb connection string into the API's configuration under a name the client integration already knows how to find, so the string lives nowhere in your application code. You wrote the name once. That naming is a contract, and it is the single most important thing to internalize about Aspire.

WaitFor(db) is where my old ordering bug went to die, and it comes with a tradeoff worth saying out loud. Containers report healthy on their own clock, and Postgres with a data volume can take several seconds on a cold start. WaitFor gates your service behind the slowest dependency by design, which is correct, but it means your local cold start is only ever as fast as your slowest container. That is the right call. Know you made it.

Two more things a real run will teach you. First, your topology now lives in C#. That is the lock-in nobody flags at adoption: leaving Aspire means re-expressing your whole graph somewhere else, and the more you lean on it the more that costs. Second, container-to-host networking is the area that bites. 13.0 reworked it specifically because it was fragile, introducing context-aware endpoint resolution so the same resource can resolve to a different URL depending on whether a container or a host process is asking. When a containerized resource cannot reach something running as a host process, this is where you look first.

The single-file apphost.cs is real now thanks to C# 14 file-based apps, which is why aspire init can stay clean. And the same model exists in TypeScript, GA in 13.4, running as a guest process that talks to the .NET orchestrator over JSON-RPC on a local socket. The point is not that anyone enjoys writing infrastructure in TypeScript. The point is the resource model is language-agnostic now, and that is a genuine architectural shift, not a checkbox.

ServiceDefaults, and the trap inside it

The other project in every starter is ServiceDefaults, a shared library your services reference. It is why an aspirified service has telemetry, health checks, and resilience with no manual wiring.

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();   // OpenTelemetry, health checks, service discovery, resilience
builder.AddNpgsqlDbContext<AppDbContext>("appdb");  // name matches the AppHost resource
builder.AddRedisClient("cache");
var app = builder.Build();
app.MapDefaultEndpoints();       // /health and /alive
app.Run();

One line configures OpenTelemetry for traces, metrics, and logs, registers health endpoints, turns on service discovery, and attaches default resilience handlers to your outbound HTTP clients. It is just code in your solution, not a black box, and you should open it and read it on day one.

Two senior caveats the happy path hides. The default resilience handlers add retries to your HTTP calls, and free retries quietly paper over a sick dependency. If you do not watch the metrics, you will ship latency and a degraded downstream you cannot see, because the retry succeeded on attempt three. The second caveat is bigger: the dashboard is not your production observability. ServiceDefaults wires OTEL, but in production you still need somewhere to send it, Azure Monitor, or Grafana with Tempo and Loki, or whatever your shop runs. People stand up Aspire, see beautiful traces locally, and assume observability is solved. It is solved for your laptop.

Integrations, without the catalog

Integrations come in two halves, and the split is where everyone trips. A hosting integration is what you call in the AppHost, AddPostgres, AddRedis, AddRabbitMQ, the Azure family. It runs or provisions the thing and hands out its connection details. A client integration is what you call inside a service, AddNpgsqlDbContext, AddRedisClient, which consumes those details and configures a real client already wired into the telemetry, health, and resilience ServiceDefaults set up. Hosting produces, client consumes, names connect them.

The catalog is broad, Postgres, Redis, RabbitMQ, MongoDB including its EF Core flavor, SQL Server, and a deep Azure set spanning Key Vault, Storage, Service Bus, Cosmos, App Service, Container Apps, Functions, and the AI services, where the older Azure AI Foundry integration was replaced by Microsoft Foundry. The operational note that matters more than the list: these packages version in lockstep with Aspire and have had breaking renames and transitive security bumps, including a StreamJsonRpc and MessagePack CVE patch in the 13.4 line. Pin them with the rest of your Aspire dependencies and review the breaking-changes section on every upgrade. This is not a place to float version ranges.

The dashboard, and its boundary

aspire run opens the dashboard, an OpenTelemetry viewer welded to a live resource map. Distributed traces across services, structured logs you can filter, metrics, and a resource graph. You can export traces, logs, and config as JSON or .env and import a teammate's captured bundle, which turns "works on my machine" into something you can actually hand off. That last feature is underrated for debugging across a team.

The newer pieces are worth a clear-eyed note. The dashboard runs an MCP server, so Copilot or Claude Code can query live application data, list resources, stream logs, pull traces, and answer “why is this service failing” from real telemetry instead of a pasted screenshot. There is also a GenAI visualizer that renders the prompts, schemas, and tool calls through your model integrations, which beats scattering Console.WriteLine through prompt-building code if you ship anything that talks to an LLM. Both are genuinely useful. Both are dev-time only. The dashboard is ephemeral and local, and conflating it with production monitoring is the most common mistake I see new Aspire teams make.

Deployment, and who owns the YAML

Manage expectations here or you will be disappointed. aspire publish turns your AppHost graph into deployment artifacts. It does not run a pipeline and it hosts nothing. It emits files, and you or your CD system apply them.

The targets matured across the 13.x line. The Docker Compose publisher went stable in 13.2. First-class Kubernetes and AKS via Helm landed in 13.3 and is called mature in 13.4, and “mature” is Microsoft’s word, not mine, so pressure-test the generated charts before you bet a production cluster on a feature that is barely a quarter old. The Azure Developer CLI path takes the graph to Azure Container Apps and provisions the Azure resources you declared along the way, which is the smoothest road if you are already an Azure shop.

aspire publish     # emit artifacts for your target

#   Docker Compose     -> stable, fine for a single box
#   Kubernetes / AKS    -> Helm charts, new, verify before prod
#   Azure Container Apps -> via azd, provisions Azure resources too

The honest framing: Aspire gets you from a working local graph to a credible first manifest without hand-writing YAML from zero. It does not absolve you of understanding that YAML. The moment it is generated, you own it. Treat the output as a strong first draft you review and then maintain, because that is what it is.

Where it is still weak

This is the section a tutorial skips and the one that should decide your adoption. After a year on it, here is where Aspire still hurts.

Testing is the largest gap, and the team admits it. aspire wait is a useful CI building block, but there is no first-class testing experience: no live dashboard during tests, no capture and replay, no partial AppHost execution, no request mocking. If your value proposition to leadership is "better integration testing," Aspire is not there yet.

You cannot cleanly run a subset of resources. On a twelve-service graph you rarely want all twelve up to work on one. Detached mode plus aspire resource start/stop help, but a real "run only these three" experience does not exist, and on a large graph it is a daily papercut and a slow startup you pay every time.

It assumes one repo. The single-AppHost model fights any org that spreads services across repositories, and proper multi-repo support is still a roadmap item, not a shipped feature. If your microservices live in fifteen repos, Aspire’s mental model and your codebase are in open disagreement.

And the two costs I already named: a breaking change roughly every minor release, and topology lock-in into Aspire’s model. Neither is disqualifying. Both are real, and a senior evaluating this for a team should price them in rather than discover them in month three.

When to use it, and when to walk away

Aspire earns its place when you run more than two or three services that must come up together, especially with backing infrastructure, and most of all when you deploy to Azure. The inner loop and the deployment story both get dramatically simpler and the observability is close to free for local work. That is a strong fit and I would reach for it again on that shape of system.

Walk away from it for a single API with one database, where the AppHost and ServiceDefaults are pure overhead and a plain launchSettings.json wins. Think hard before adopting it across a many-repo organization, or on a team that wants Aspire to solve integration testing today, or anywhere a fast-moving dependency with quarterly breaking changes is a non-starter. The polyglot rebrand is real, but if your team is allergic to Microsoft tooling, the lock-in of expressing your topology in Aspire's model is a fair reason to pass.

For exhaustive API detail, aspire.dev is the reference and it keeps pace with the release cadence. What I have given you is the shape of the thing and the judgment around it, which is the part the docs cannot.

That twelve-service backend still has an API, a worker, a database, a cache, and a broker. The README is aspire run. Nobody loses a day onboarding anymore, and the run-it-twice ordering bug has been gone for a year. I went in knowing the costs, and on this system they were worth paying. Yours might be a different system. Now you know what to weigh.


메타데이터
post_id
e6804718f892
slug
aspire-top-to-bottom-what-it-actually-does-and-where-it-still-bites-e6804718f892
url
https://medium.com/works-on-my-machine/aspire-top-to-bottom-what-it-actually-does-and-where-it-still-bites-e6804718f892
canonical_url
https://medium.com/works-on-my-machine/aspire-top-to-bottom-what-it-actually-does-and-where-it-still-bites-e6804718f892
author_url
https://medium.com/@krativarshney7
status
ok
fetched_at
2026-07-09 16:18:44