.NET Aspire as a compile-time alternative to Docker Compose
.NET Aspire earns its keep on multi-service .NET projects with mixed dependencies and junior-heavy teams. It is a compile-time, type-safe alternative to Docker Compose, where the IDE owns service discovery and connection strings instead of copy-pasted YAML and appsettings.
The daily tax of a microservice topology
Most multi-service .NET projects I see in the wild start the same way. A dashboard, a backend-for-frontend, one or two ingestion workers, a database, a cache, some blob storage. Someone writes a docker-compose.yml to wire it all up locally, and a runbook in the README that explains the bits Compose cannot.
That setup works. I have shipped plenty of systems on it. The issue is what it costs the team on day-to-day developer experience, and that cost is paid disproportionately by junior engineers.
A typical morning on a Compose-and-runbook project, for a junior dev, looks like this. Pull main. Re-read the README to remember which docker compose invocation is current. Check whether the connection strings in appsettings.Development.json still match the ports in the Compose file. Find out the cache password rotated last sprint and the Compose file is out of sync with three of the service appsettings files. Spend forty minutes getting back to a green local environment before writing a single line of code.
Multiply that across nine developers, most of them junior, and the daily tax on the team is significant. It is also invisible on a Jira board. Nobody logs a ticket called "spent an hour reconciling appsettings before standup".
What I changed on a recent engagement
I will be deliberately vague about the client, but the shape of the engagement matters for the argument.
Greenfield UK platform. Vue.js dashboard, a C# backend-for-frontend, a progress engine, two Azure Functions for ingestion, SQL, Blob storage, Redis. Roughly nine developers, predominantly junior. The original plan was Docker Compose plus runbooks for local development.
I made one early call. I introduced .NET Aspire as the local development orchestrator before the team got into the rhythm of docker compose up. By the end of week one, the entire ecosystem (the dashboard, the backend-for-frontend, the progress engine, both Azure Functions, SQL, Blob, Redis) came up on F5 from a single Visual Studio solution. No runbook. No README ritual. Open the solution, hit F5, and the whole thing is running with a dashboard that shows you which services are healthy.
That was a meaningful change in the texture of the team's day. The morning tax dropped to zero. New starters cloned the repo, opened it, hit F5, and were productive that afternoon.
This was also the first commercial production-team Aspire deployment for me and for the wider team I was working with. So I am not writing as someone who has done this twenty times. I am writing as someone who picked it up on a real project, pushed it through to production, and formed an opinion.
Aspire as a typed C# app host
The mental model that helped me sell Aspire to the team was this. Docker Compose is YAML that describes your local topology. Aspire is C# that describes your local topology, and the C# is part of your solution.
That sounds like a small distinction. It is not.
In an Aspire app host, services are first-class C# objects. You declare a SQL database resource, a Redis cache, a Blob storage emulator, your API projects and your worker projects, and then you wire them together with method calls. When the API needs the cache, you pass the cache resource in as a reference. When the worker needs the database, same idea.
Two things follow from that, and they are the whole argument for Aspire on a junior-heavy team.
First, service discovery and connection strings stop being copy-paste artefacts. The app host owns them. The API does not have a connection string for Redis hardcoded in appsettings.Development.json that drifts out of sync with the Compose file. It receives the connection string from Aspire at startup, generated from the resource graph the app host described in C#.
Second, the topology is refactor-aware and IDE-aware. Rename a service in C# and the references update. Remove a dependency and the compiler tells you what broke. Add a new worker that needs the cache, and you wire it up with a method call that the IDE autocompletes. The "is my environment in a sensible state" question gets answered by the C# compiler before you press F5, not by trial and error after.
That is the framing I have ended up using. Aspire is a compile-time, type-safe alternative to Docker Compose. YAML cannot tell you on Tuesday morning that the change someone merged on Monday afternoon broke the topology. C# can.
What week one actually looked like
Concretely, here is what I had stood up by the Friday of week one, in front of the team.
The Vue.js dashboard, served via the backend-for-frontend. The backend-for-frontend itself, talking to SQL and Redis. The progress engine, talking to SQL and to Blob storage. Both Azure Functions, configured against the local Functions emulator and pointed at the same SQL and Blob resources. SQL Server in a container, with a developer-grade migration step running on app host startup. Azurite for Blob. Redis in a container.
All of that came up on F5. The Aspire dashboard gave each developer a live view of which services were running, the structured logs from each one in a single pane, and the distributed traces stitched together across service boundaries.
For the juniors on the team, the dashboard was the quiet star of the show. The mental load of "where do I find the logs for the thing I just called" went to zero. They could click through a request from the Vue.js client into the backend-for-frontend, into the progress engine, into the database call, and back, in one place.
I want to be careful about how I frame the win here. The team would have got the system working with Compose. They are good engineers. The point is not that Aspire made the impossible possible. It removed a category of friction that was going to be paid every single day, by every developer, for the lifetime of the project.
The honest limitation: sub-repository pointer drift
I want to talk about the sharp edge I hit, because writing about a tool only in glowing terms is how you end up with juniors quietly hating you six months later.
The system was split across three repositories. The progress engine and the ingestion services lived in their own repos, and were pulled into the dashboard repo as sub-repositories. The Aspire app host sat in the dashboard repo and referenced the others through those sub-repository checkouts.
Multi-repo Aspire works. It is genuinely fine. The sharp edge is sub-repository commit pointer drift on short-lived feature branches.
Here is the failure mode. A developer starts a feature that touches both the dashboard and the progress engine. They make a commit on a short-lived feature branch in the progress engine repo. They update the sub-repository pointer in the dashboard repo to match. They open a pull request on the dashboard repo. Code review happens. The progress engine pull request gets merged with a squash, which rewrites the commit. The feature branch on the progress engine repo gets deleted.
Now the sub-repository pointer in the dashboard repo points at a commit that no longer exists. Anyone who clones the dashboard repo at that point gets a broken checkout. The Aspire app host cannot start because one of its referenced projects is dangling.
The fix in our case was not a tooling fix. It was a discipline rule. Sub-repos always point at long-lived branches, never at short-lived feature branches. Cross-repo work means landing the sub-repo change first, on its long-lived branch, and only then bumping the pointer in the parent repo.
That rule worked. It is also the kind of rule that needs writing down, repeating, and gently enforcing in code review for the first month. Aspire did not cause the problem. Multi-repo workflows have always had this shape. Aspire makes the failure visible immediately, because the app host stops starting, where a Compose setup might have failed more quietly later.
If you adopt Aspire across multiple repositories, plan for that conversation on day one rather than day thirty.
When I would not reach for Aspire
I have become an Aspire advocate, but I want to be honest about where it does not earn its keep.
Small systems are the obvious one. If your local topology is a single API and a single database, Aspire is overkill. You will spend more time wiring up the app host than you ever spent on appsettings.Development.json. A docker compose file with two services in it, or just a connection string pointing at a local SQL instance, is the right answer.
Senior teams who are already happy with Compose are the second case. The whole argument for Aspire I have made above is about the daily tax paid by junior developers reconciling environments. A team of senior engineers who already have a Compose workflow in muscle memory, with a tidy README, are not paying that tax. Asking them to learn a new orchestrator to solve a problem they do not have is not a kindness.
The third case is projects where the local development workflow has already calcified around something else. If a team has six months of Compose plus shell scripts plus a Tilt or Skaffold setup that everyone understands, the cost of swapping to Aspire mid-flight is real. I would not do it without a strong reason. New projects, or projects early enough that the workflow has not set, are where the switch is cheap.
There is also a quieter case. Polyglot systems where most of the services are not .NET. Aspire has hosting integrations for non-.NET workloads, and they are improving, but the C# app host is most expressive when most of the services it is orchestrating are themselves .NET projects. If your topology is two Go services, a Python worker and a single .NET API, Compose is probably still the right answer.
Closing thought
Aspire is one of the few tools I have adopted as a default since first using it commercially. On any new multi-service .NET project, with a mixed bag of dependencies and a team that includes junior engineers, my starting position now is "Aspire, unless someone gives me a reason not to".
That should not be read as Aspire being right for every project. It is right for a specific shape of project, which happens to be a shape I see often. On small systems, on senior teams happy with what they have, or on projects whose local workflow has already settled, Compose plus a good README is still a perfectly good answer.
If you are starting a multi-service .NET build and want a second opinion on whether Aspire is the right call for your team, I am happy to have a short conversation about it. No pitch attached.
