Let's be honest:
Running a distributed application locally is painful.
You need Docker Compose for databases, 5 terminals for your services, hardcoded connection strings copied across config files, and a prayer that you started everything in the right order.
I've been there. Multiple times. And every time a new developer joins the team, they spend half a day just getting the system running.
Aspire changes that completely.
In today's article, I'll show you what Aspire is, why it exists, and how it orchestrates a real distributed application - with .NET, TypeScript, and Python services - from a single command.
I built a demo (GitHub Copilot built it) called OrderCanvas to demonstrate this in practice. Let's dive in...
First things first - you might have seen older articles calling it ".NET Aspire".
It's just "Aspire" now.
Microsoft dropped the ".NET" prefix because the tool isn't limited to .NET services. The orchestrator happens to be built with .NET (or TypeScript), but the services it manages can be written in any language. The rebranding reflects what it always was: a language-agnostic orchestration tool.
So what is it exactly?
Aspire is a code-first orchestration layer for distributed applications.
Think of it this way: when you're building a system with multiple services, databases, and caches, you face a fundamental problem - how do you run all of this locally?
How do you make sure every service can find every other service? How do you see what's happening across 5 different processes?
Aspire solves this by giving you a single place to describe your entire system: the AppHost.
The AppHost is a small project that defines:
• What infrastructure you need (PostgreSQL, Redis, RabbitMQ, etc.)
• What services exist (APIs, frontends, workers - in any language)
• How they connect to each other
• What should start before what
When you run the AppHost, Aspire:
• Starts everything with one command - containers, APIs, frontends, workers
• Wires connections automatically - database URLs, cache connections, service endpoints are injected as environment variables
• Provides a unified dashboard with logs, traces, metrics, and health for all services
• Manages the full lifecycle - from pulling Docker images to creating Python virtual environments to running npm install
But here's the thing: the AppHost's language is independent of the services' languages. You can orchestrate .NET, TypeScript, Python - or anything else - from a single AppHost.
You might be thinking: "I already have Docker Compose. It works fine."
And yes, Docker Compose can start containers. But let me describe what a typical day looks like without Aspire when you're working on a distributed app:
docker-compose up -d to start PostgreSQL and Redisdotnet run your first APIdotnet run your second APInpm install, then npm run dev for the frontendThat's 5 terminals, 3 config files with connection strings, manual startup order, and zero unified logging.

Now here's what it looks like with Aspire:
dotnet run --project src/OrderCanvas.AppHostThat's not a simplification for the article. That's literally it.
Aspire is useful because it eliminates three categories of pain:
Pain #1: Startup complexity. You shouldn't need a 20-step README to run your own project. Aspire replaces that with one command.
Pain #2: Configuration drift. When connection strings live in 4 different config files, they will get out of sync. Aspire declares them once and injects them everywhere.
Pain #3: Invisible failures. When your order isn't processing, is it the API? The database? The worker? With 5 separate terminal windows, good luck finding the answer. Aspire's dashboard shows logs, traces, and health for everything in one place.
If you're building anything with more than one service and a database, Aspire removes friction you didn't even realize you were living with.
To show what Aspire actually does, I built OrderCanvas - a small order and fulfillment platform with:
Six components. Three languages. One AppHost.

Let me explain how this all fits together.
This is the entire orchestration - Program.cs in the AppHost project:
var builder = DistributedApplication.CreateBuilder(args); // Infrastructurevar postgres = builder.AddPostgres("postgres");var catalogDb = postgres.AddDatabase("catalogdb");var ordersDb = postgres.AddDatabase("ordersdb");var redis = builder.AddRedis("redis"); // .NET APIsvar catalogApi = builder.AddProject<Projects.OrderCanvas_CatalogApi>("catalog-api") .WithReference(catalogDb) .WithReference(redis) .WaitFor(catalogDb) .WaitFor(redis); var ordersApi = builder.AddProject<Projects.OrderCanvas_OrdersApi>("orders-api") .WithReference(ordersDb) .WaitFor(ordersDb); // React Frontendbuilder.AddViteApp("web", "../ordercanvas-web") .WithReference(catalogApi) .WithReference(ordersApi) .WaitFor(catalogApi) .WaitFor(ordersApi); // Python Workerbuilder.AddPythonApp("fulfillment", "../ordercanvas-fulfillment", "main.py") .WithReference(ordersApi) .WaitFor(ordersApi); builder.Build().Run();
That's it. No Docker Compose. No hardcoded ports. No manual connection strings.
A few things to notice:
WithReference()WaitFor()AddPostgres() / AddRedis()AddViteApp() / AddPythonApp()One command:
dotnet run --project src/OrderCanvas.AppHost
What happens behind the scenes:
catalogdb and ordersdb are creatednpm install + npm run dev runs for the React frontendWithin 30-60 seconds, everything is running. No Docker Compose file. No 5 terminals. No setup guide.
The Aspire Dashboard is where the magic becomes visible.
You get a Resources tab that shows every component with live health status:

In addition you can see the graph of dependendies between services, for better understanding of the system topology:

But the dashboard gives you much more:
Structured Logs

Traces

Metrics/Health Checks

Console Logs

No Jaeger. No Zipkin. No Grafana setup. It's all built in.
Let me walk through real request flows - because this is where you see why unified orchestration matters.

First request (cache MISS):
GET /api/catalog/productsSecond request within 30 seconds (cache HIT):
In the Aspire Dashboard you'll see the full trace: web → catalog-api → Redis (GET) → PostgreSQL (SELECT) → Redis (SET). On cache hit, the trace is shorter - it stops at Redis with no PostgreSQL step. You can see this difference visually, in real time.
This is where the multi-language orchestration really shines.

Step 1 - User places an order:
POST /api/orders/orders with cart itemsStep 2 - Python worker fulfills automatically:
GET /orders?status=Pending every 10 secondsPUT /orders/{id}/fulfill on the Orders APIStep 3 - User sees the update:
The distributed trace in the Aspire Dashboard shows the complete flow from Python → .NET → PostgreSQL. Three languages, one trace view. You don't need to tab between terminals to figure out what happened - it's all correlated automatically.
This is what convinced me. Let me be concrete about what changes.
| Concern | Without Aspire | With Aspire |
|---|---|---|
| Starting the system | 5+ terminals, manual order | dotnet run (one command) |
| Connection strings | Copy-paste into each config | Declared once, injected automatically |
| Service URLs | Hardcode localhost:PORT | Aspire injects via environment variables |
| Port conflicts | Debug manually | Aspire assigns ports automatically |
| Database creation | Manual SQL or scripts | AddDatabase() handles it |
| See all logs | Tab between terminals | Dashboard - unified, filterable |
| Distributed traces | Install Jaeger + configure | Built-in, zero config |
| Add a new service | Edit docker-compose, add env vars | One line in AppHost |
| Onboard new developer | Long README, multiple steps | dotnet run and you're done |
Every single row in this table is a real problem I've hit on real projects. The connection strings one alone has cost me hours of debugging across multiple teams.
If you've ever set up OpenTelemetry manually, you know it takes work. You need to install NuGet packages, configure exporters, decide where to send data, set up a collector, run Jaeger or Zipkin...
With Aspire, each .NET service simply calls:
builder.AddServiceDefaults();
This single line configures:
/health and /aliveAll of that is automatically pointed at the Aspire Dashboard. No separate collector. No Jaeger container. No Grafana stack.
For non-.NET services, Aspire is smart about it too. The Python worker in OrderCanvas gets OTEL_EXPORTER_OTLP_ENDPOINT injected automatically as an environment variable - no Python-side configuration needed. Aspire captures its structured stdout logs and shows them in the same dashboard alongside the .NET services.
The result: you get full observability (logs + traces + metrics + health) across all services without writing a single line of observability configuration beyond AddServiceDefaults().
This is important: Aspire doesn't require your services to be .NET.
In OrderCanvas:
All three appear in the same dashboard. All get environment variables injected. All have health monitoring. All can reference each other with WithReference().
No Dockerfiles needed for local development. No manual port management. One graph, one dashboard, any language.
OrderCanvas/├── src/│ ├── OrderCanvas.AppHost/ # THE orchestrator│ │ └── Program.cs # Defines the whole system│ ├── OrderCanvas.ServiceDefaults/ # Shared telemetry/health config│ ├── OrderCanvas.CatalogApi/ # Product catalog API (.NET)│ ├── OrderCanvas.OrdersApi/ # Order management API (.NET)│ ├── ordercanvas-web/ # React + Vite frontend│ └── ordercanvas-fulfillment/ # Python worker└── README.md
The AppHost's Program.cs is the single source of truth. Every resource, every dependency, every relationship is declared there. A new developer opens that file and immediately understands the entire system topology.
Let me show you what OrderCanvas actually looks like when you run it.
When you open the Web Frontend, you land on the Product Catalog page - a clean grid of products pulled from the Catalog API. Each product card shows the name, price, category, and an "Add to Cart" button.

You add a few items to the cart, enter your name and email, and click "Place Order". The order is saved as "Pending" and within seconds the Python fulfillment worker picks it up, processes it, and marks it as "Fulfilled". You can watch this happen in real time on the Orders page - the status badge flips from purple "Pending" to green "Fulfilled" without you doing anything.
The key point: this entire flow involves 3 different languages and 4 different services (React → .NET Catalog API → .NET Orders API → Python Worker), all started from one command, all visible in one dashboard. That's the promise of Aspire - and OrderCanvas is the proof that it delivers.
Aspire solves a real problem that every distributed application developer faces: the pain of running, wiring, and observing multiple services locally.
Instead of Docker Compose + 5 terminals + hardcoded connection strings + manual observability setup, you get:
The OrderCanvas demo proves this with a real multi-language distributed app. Check it out, run dotnet run, and see for yourself.
That's all from me today.
P.S. Follow me on YouTube.
Stop arguing about code style. In this course you get a production-proven setup with analyzers, CI quality gates, and architecture tests — the exact system I use in real projects. Join here.
Not sure yet? Grab the free Starter Kit — a drop-in setup with the essentials from Module 01.
Design Patterns that Deliver — Solve real problems with 5 battle-tested patterns (Builder, Decorator, Strategy, Adapter, Mediator) using practical, real-world examples. Trusted by 650+ developers.
Just getting started? Design Patterns Simplified covers 10 essential patterns in a beginner-friendly, 30-page guide for just $9.95.
Every Monday morning, I share 1 actionable tip on C#, .NET & Architecture that you can use right away. Join here.
Join 20,000+ subscribers who mass-improve their .NET skills with actionable tips on C#, Software Architecture & Best Practices.
Subscribe to the TheCodeMan.net and be among the 20,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.