Polyglot Aspire

Orchestrating Any Language with Aspire

Chris Ayers

Chris Ayers

Principal Software Engineer
Azure EngOps AzRel
Microsoft

BlueSky: @chris-ayers.com
LinkedIn: - chris-l-ayers
Blog: https://chris-ayers.com/
GitHub: Codebytes
Mastodon: @Chrisayers@hachyderm.io

The Polyglot Problem

Modern apps are a big city without a map β€” and for polyglot teams,
the streets are in five different languages.

Your team doesn't use one language β€” it uses five.

Your stack today

  • Python ML services
  • Go microservices
  • Java Spring Boot APIs
  • TypeScript/React frontends
  • .NET backend APIs

The question: How do you orchestrate, observe, and wire all of this from one place?

The Orchestration Pain

Five stacks. Five toolchains. Zero shared model.

  • 🚒 Docker Compose β€” manual port wiring, no built-in telemetry, no health-aware startup ordering
  • πŸ“„ Config sprawl β€” .env, YAML, application.properties, appsettings.json β€” each language has its own format
  • πŸ” No unified observability β€” good luck tracing a request across four services in three runtimes
  • πŸ“œ 15-step READMEs β€” "just docker-compose up" never works first time

Aspire: The Polyglot Answer

Aspire is an agent-ready, code-first tool to compose, debug, and deploy any distributed app.

The Four Pillars

πŸ›  Aspire CLI

Your control plane
aspire init Β· aspire run Β· aspire deploy β€” agent-ready, interactive, the same commands for every stack.

πŸ—Ί Aspire AppHost

Your stack in code
One file declares every service and how they connect β€” C#, TypeScript, Python, or aspire.config.json.

πŸ“Š Aspire Dashboard

Your app at a glance
Logs, traces, metrics, and health for every resource β€” powered by OpenTelemetry, surfaced over an MCP server for agents.

🧩 Aspire Integrations

Building blocks, not black boxes
100+ prebuilt packages for databases, caches, queues, AI, and clouds β€” or bring your own container, CLI, or agent.

One Orchestrator for Every Language

Aspire gives you three things, regardless of language:

🎯 Orchestration
Define your entire stack β€” Python, Go, Java, TypeScript, .NET β€” in one AppHost file.

πŸ“‘ Service Discovery
Endpoints and connection strings auto-injected as environment variables.

πŸ“Š Observability
One dashboard for logs, traces, and metrics across all services via OpenTelemetry.

Your Stack in One File

One C# AppHost wires Python, React, and .NET β€” auto-discovery, observability, lifecycle:

var builder = DistributedApplication.CreateBuilder(args);

var redis = builder.AddRedis("cache");
var postgres = builder.AddPostgres("db")
                      .AddDatabase("appdata");

builder.AddUvicornApp("ml-service", "../python", "main:app")
       .WithUv()
       .WithReference(redis);

builder.AddViteApp("frontend", "../react")
       .WithHttpEndpoint(env: "PORT")
       .WithReference(postgres);

builder.AddProject<Projects.Api>("api")
       .WithReference(redis)
       .WithReference(postgres);

builder.Build().Run();

The Dashboard β€” One View for Everything

Same dashboard regardless of what language your services use:

πŸ“‹ Resources β€” All services, containers, status, endpoints
πŸ“œ Console Logs β€” Real-time stdout/stderr from every process
πŸ“Š Structured Logs β€” Parsed JSON logs, filter by level
πŸ” Traces β€” Distributed request tracing across services
πŸ“ˆ Metrics β€” Latency, CPU/memory, custom metrics

Aspire sets OTEL_EXPORTER_OTLP_ENDPOINT automatically β€” add OpenTelemetry to your service and traces flow to the dashboard.

Standalone Dashboard β€” No AppHost Required

Already on OTEL? Get the dashboard with zero rewrites.

The Aspire Dashboard ships as a standalone container. Point any OTLP-emitting app at it and you get logs, traces, and metrics β€” no AppHost, no .NET, no commitment.

  • βœ… Node.js / Python / Java / Go / Rust β€” anything with an OTEL SDK works
  • βœ… Same UI as the AppHost-managed dashboard
  • βœ… Local-only by default β€” OTLP endpoint and dashboard auth keys printed at startup
  • βœ… Use it in CI, in a Dockerfile, in docker-compose.yml, or attached to a Kubernetes pod
docker run --rm -it -p 18888:18888 -p 4317:18889 \
  -d --name aspire-dashboard \
  mcr.microsoft.com/dotnet/aspire-dashboard:latest

Open http://localhost:18888 β€” done.

Wire Any OTEL App in 30 Seconds

Point your Node.js app at the standalone dashboard:

// otel.js
import { NodeSDK } from "@opentelemetry/sdk-node";
import {
  OTLPTraceExporter
} from "@opentelemetry/exporter-trace-otlp-grpc";

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: "http://localhost:4317"
  })
});
sdk.start();

Migration path: start with the standalone dashboard for observability today; adopt the AppHost later when you want service discovery, integrations, and aspire deploy.

OpenTelemetry β€” Python

Wire OTEL once. Aspire injects OTEL_EXPORTER_OTLP_ENDPOINT automatically.

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc \
  .trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export \
  import BatchSpanProcessor

provider = TracerProvider()
provider.add_span_processor(
  BatchSpanProcessor(OTLPSpanExporter(
    endpoint=os.environ.get(
      'OTEL_EXPORTER_OTLP_ENDPOINT')
  ))
)
trace.set_tracer_provider(provider)

One block of boilerplate β€” and your Python service is in the dashboard.

OpenTelemetry β€” Node.js

Same idea, different runtime β€” and you get auto-instrumentation for free.

const { NodeSDK } =
  require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } =
  require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } =
  require('@opentelemetry/exporter-trace-otlp-grpc');

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
  }),
  instrumentations: [getNodeAutoInstrumentations()]
});
sdk.start();

Auto-instrumentations capture Express, Fastify, fetch, pg, redis β€” without code changes.

Architecture Overview

AppHost orchestrates everything β€” DCP manages processes, Dashboard collects telemetry

center

How It Works

The patterns that make polyglot orchestration possible

Service Discovery β€” The Pattern

Aspire injects service endpoints as environment variables:

# Pattern: services__<name>__<protocol>__<index>
services__api__http__0=http://localhost:5000
services__frontend__http__0=http://localhost:3000

Why double underscore? Env vars can't have colons β€” so __ stands in for the path separator. Every language can read env vars; that's the universal interface.

No hardcoded URLs. No .env files. Aspire wires it.

Service Discovery β€” Read It Anywhere

Same env-var pattern, every language:

Python:

api_url = os.environ['services__api__http__0']
requests.get(f'{api_url}/data')

Go:

apiURL := os.Getenv("services__api__http__0")

Node.js:

const apiUrl = process.env['services__api__http__0'];
await fetch(`${apiUrl}/data`);

Java:

String apiUrl = System.getenv("services__api__http__0");

Connection Strings β€” The Pattern

Infrastructure resources get connection strings as environment variables:

# Pattern: CONNECTIONSTRINGS__<resource>
CONNECTIONSTRINGS__cache=localhost:6379
CONNECTIONSTRINGS__db=Host=localhost;Port=5432;Username=postgres;Password=...
CONNECTIONSTRINGS__messaging=localhost:9092

Python β€” Redis:

client = redis.from_url(
  f"redis://{os.environ['CONNECTIONSTRINGS__cache']}"
)

Aspire injects the env var β€” your service reads it using its language's standard mechanism.

Same Pattern, Every Language

Node.js β€” Kafka:

const kafka = new Kafka({
  brokers: [process.env.CONNECTIONSTRINGS__messaging]
});

Java β€” PostgreSQL:

String url = "jdbc:postgresql://"
  + System.getenv("PG_HOST") + ":"
  + System.getenv("PG_PORT") + "/"
  + System.getenv("PG_DB");

Go, Rust, .NET, PowerShell β€” same pattern. Aspire sets the env var; your code reads it.

Resource Lifecycle Management

center

Dependency Order: Infrastructure β†’ Backend Services β†’ Frontend
Health Monitoring: WithHttpHealthCheck("/health") β€” automatic restarts on failure
Graceful Shutdown: Clean termination of all processes
Backed by 100+ integrations β€” Postgres, Redis, Kafka, Cosmos, OpenAI, Ollama, and your own containers all participate in the same lifecycle.

The AppHost β€” C#

Write your AppHost in the language your team knows. Here's C#:

var builder = DistributedApplication
    .CreateBuilder(args);

var redis = builder.AddRedis("cache");

builder.AddPythonApp("api", "../api", "app.py")
       .WithReference(redis)
       .WithHttpEndpoint(env: "PORT");

builder.Build().Run();

40+ integrations β€” Redis, Azure, Kafka, MongoDB, PostgreSQL β€” available out of the box.

The AppHost β€” TypeScript

Same model, different syntax. Best fit for Node.js / TS workspaces:

const builder = await createBuilder();

const redis = await builder.addRedis("cache");

await builder
  .addPythonApp("api", "../api", "app.py")
  .withReference(redis)
  .withHttpEndpoint({ env: "PORT" });

await builder.build().run();

Same 40+ integrations as C# β€” auto-generated via [AspireExport] attributes. A JS/TS team never needs to touch .NET.

Two AppHost Languages

Author your AppHost in C# or TypeScript today β€” both officially supported.

πŸ’œ C# (.NET) β€” AppHost.cs β€” best fit for teams already on .NET tooling.

🟦 TypeScript β€” apphost.ts β€” best fit for Node.js / TypeScript workspaces.

Same model, different syntax. Both produce the same dashboard, service discovery, health checks, and deployment artifacts.

Same integration surface β€” the TypeScript SDK is auto-generated from the same .NET hosting integrations via the Aspire Type System (ATS). No separate integration code to maintain.

Workloads Can Be Anything

Pick the AppHost language that fits your repo. Your services don't need to match.

The TypeScript SDK is auto-generated from the same .NET hosting integrations via the Aspire Type System (ATS) β€” there's no separate integration surface to maintain.

Workloads inside the AppHost can be written in:

  • C#, JavaScript, TypeScript, Python, Go, Java, Rust, PowerShell, and more

via AddProject, AddJavaScriptApp, AddPythonApp, AddDockerfile, AddContainer, or AddExecutable.

Write Once with ATS

Author the integration in C#. Use it from any AppHost.

Aspire Type System (ATS) β€” the contract that bridges .NET and guest languages.

  • Author your hosting integration in C# as you always have
  • Annotate exported APIs with ATS attributes β€” [AspireExport], [AspireExportType], [AspireExportMethod]
  • Aspire's analyzer validates the export shape at build time
[AspireExport]
public static class MyIntegrationExtensions
{
    [AspireExportMethod]
    public static IResourceBuilder<MyResource>
        AddMyService(
            this IDistributedApplicationBuilder builder,
            string name) { ... }
}

Use From TypeScript β€” Zero Bindings

The CLI auto-generates a TypeScript SDK into .modules/ when a TS AppHost runs aspire add <your-package>. TypeScript callers get fluent, typed methods.

import { createBuilder } from "./.modules/aspire.js";
import { addMyService } from
    "./.modules/my-integration.js";

const builder = await createBuilder();
const svc = await addMyService(builder, "svc");

The trade-off: the guest process talks to the .NET host over a local JSON-RPC socket (Unix socket / named pipe), authenticated with a per-session token. One IPC hop, no port exposure, no duplicated integration code per language.

Status: preview feature in 13.x.

Cheat Sheet β€” Runtimes

Pick the right Add* for your service:

  • Python / ASGI β†’ AddPythonApp()
  • Python / Uvicorn β†’ AddUvicornApp()
  • Node.js β†’ AddNodeApp()
  • Vite / React β†’ AddViteApp()
  • JavaScript β†’ AddJavaScriptApp()
  • .NET project β†’ AddProject<T>()
  • Go β†’ AddGolangApp() (Community Toolkit)
  • Java / Spring Boot β†’ AddSpringApp() (Community Toolkit)
  • Any Dockerfile β†’ AddDockerfile()
  • Any executable β†’ AddExecutable()

Infrastructure: AddRedis("name") Β· AddPostgres("name").AddDatabase("db") Β· AddKafka("name") Β· AddAzureCosmosDB("name").RunAsEmulator()

Cheat Sheet β€” Common Patterns

Chainable methods you'll use everywhere:

.WithReference(redis)              // wire endpoints
.WaitFor(postgres)                 // start ordering
.WithHttpEndpoint(env: "PORT")     // expose http
.WithExternalHttpEndpoints()       // public ingress
.WithUv()                          // python: uv
.WithNpm() / .WithBun()            // js: package mgr
.WithBuildSecret("key", secret)    // build-time secret
.WithRunScript("dev")              // npm script
.WithHttpHealthCheck("/health")    // probe
.WithMcpServer("mcp")              // expose MCP

aspire.config.json β€” The File

This file tells the CLI which language your AppHost uses.

{
  "appHost": {
    "path": "apphost.py",
    "language": "python"
  },
  "sdk": { "version": "13.2.0" },
  "channel": "stable",
  "features": {
    "polyglotSupportEnabled": true
  },
  "profiles": {
    "default": {
      "applicationUrl": "https://localhost:17000"
    }
  }
}

aspire.config.json β€” What Each Field Does

  • appHost.path + appHost.language β€” declares your stack (csharp, typescript, python...)
  • sdk.version β€” pins the Aspire SDK version
  • channel β€” release channel (stable, preview)
  • profiles β€” dashboard URLs (replaces apphost.run.json)
  • Feature flags use boolean true β€” not string "true"

Every sample in this talk has one at its root β€” peek inside.

Manage Config From the CLI

No manual JSON editing required:

# Read & write config values
aspire config list
aspire config get <key>
aspire config set <key> <value>

# Manage secrets (encrypted at rest)
aspire secret set <key> <value>
aspire secret list / get / delete

# Local dev certs
aspire certs clean
aspire certs trust

Every sample in this talk has an aspire.config.json at its root β€” peek inside.

Getting Started

No .NET SDK required β€” get.aspire.dev

Install & scaffold

curl -sSL https://aspire.dev/install.sh | bash
aspire new aspire-py-starter -n my-app
aspire new aspire-ts-starter -n my-app
aspire new aspire-starter -n my-app
cd my-app && aspire run

Day-to-day commands

aspire run / aspire start    # Run / background
aspire ps / aspire stop      # List / stop
aspire describe --follow     # Watch resources
aspire doctor                # Environment check
aspire otel / aspire logs    # Telemetry & logs
aspire export                # Capture to zip

Agent-Ready CLI

Two MCP servers. One model. Any agent.

MCP support out of the box β€” no plugins, no glue.

  • πŸ›  CLI MCP β€” stdio. Agent spawns aspire agent mcp as a subprocess. Set up by aspire agent init.
  • πŸ“Š Dashboard MCP β€” streamable HTTP + API key. Click the MCP button in the dashboard top-right.
  • 🧠 Tools agents get: list_resources, list_console_logs, list_traces, execute_resource_command, search_docs, doctor.
  • πŸ”Œ Clients: VS Code, Claude Code, Copilot CLI, OpenCode β€” any MCP-aware client works.

Polyglot bonus: the agent sees Python tracebacks, Go panics, Java stack traces, and Node errors through the same OTEL pipeline.

Wire It Up in 30 Seconds

One-time setup, then any MCP-aware agent works:

# 1. One-time setup in your AppHost dir
$ aspire agent init
  β—» Aspire skill file       (recommended)
  β—» Aspire MCP server
  β—» Playwright CLI

# 2. Start your stack, open your agent
$ aspire run
$ claude    # reads .mcp.json
$ code .    # reads .vscode/mcp.json
// .vscode/mcp.json (auto-generated)
{ "servers": { "aspire": {
    "type": "stdio",
    "command": "aspire",
    "args": ["agent", "mcp"]
} } }

Demos

8 samples β€” live with the Aspire dashboard

8 Live Demos

Simple β†’ Full-stack

  1. ts-starter β€” Express + React (TS AppHost)
  2. flask-markdown-wiki β€” Flask + Redis (Python AppHost)
  3. vite-react-api β€” FastAPI + React + Redis (TS AppHost)
  4. django-htmx-polls β€” Django + HTMX + PostgreSQL (Python AppHost)

Multi-runtime β†’ Polyglot
5. spring-boot-postgres β€” Spring Boot + PostgreSQL (Java AppHost)
6. svelte-go-bookmarks β€” Go API + Svelte + PostgreSQL (Go AppHost)
7. dotnet-angular-cosmos β€” Angular + .NET + CosmosDB (C# AppHost)
8. polyglot-event-stream β€” .NET + Python + Node.js + Kafka (C# AppHost)

Demo: TypeScript Starter

TypeScript AppHost β†’ Express API + React Frontend

center

Demo: Flask Markdown Wiki

Python AppHost β†’ Flask Wiki App + Redis Cache

center

Demo: Vite React + FastAPI

TypeScript AppHost β†’ React Frontend + Python FastAPI + Redis

center

Demo: Django HTMX Polls

Python AppHost β†’ Django + HTMX + PostgreSQL

center

Demo: Spring Boot Notes

Java AppHost β†’ Spring Boot API + PostgreSQL

center

Demo: Svelte + Go Bookmarks

Go AppHost β†’ Go REST API + Svelte Frontend + PostgreSQL

center

Demo: Angular + .NET + CosmosDB

C# AppHost β†’ Angular Frontend + .NET API + Azure CosmosDB

center

Demo: Polyglot Event Stream

C# AppHost β†’ .NET Producer + Python Consumer + Node.js Dashboard + Kafka

center

Same Model, Two Commands

One AppHost. Local, staging, production.

aspire run       # Local development
aspire deploy    # Deploy to target  (Preview)
aspire publish   # Generate artifacts (Preview)
aspire do        # Pipeline step      (Preview)

What Aspire generates from your AppHost:

  • 🐳 Container images for all languages
  • ☸️ Azure Container Apps / Kubernetes manifests
  • πŸ”Œ Infrastructure wiring (Redis, Postgres, Kafka…)
  • πŸ”— Service connections + environment variables

No separate deploy config. The AppHost is the contract.

What That Looks Like

aspire run (local)

βœ… redis (cache)         healthy   :6379
βœ… postgres (db)         healthy   :5432
βœ… ml-service (python)   running   :8000
βœ… frontend (vite)       running   :5173
βœ… api (.net)            running   :8080

Dashboard: http://localhost:15888

aspire deploy (target)

β†’ Building images for python, node, .net
β†’ Pushing to acrcdbytes.azurecr.io
β†’ Provisioning Container Apps environment
β†’ Wiring Postgres + Redis connection strings
βœ… Deployed to dev environment in 4m 12s

Same code-first AppHost drives both.

Wrap-Up

Key Takeaways


🎯 One orchestrator for every language β€” Define your entire stack in one AppHost file, regardless of runtime


πŸ“Š Unified observability out of the box β€” One dashboard for logs, traces, and metrics across all services via OpenTelemetry


πŸš€ From local dev to production β€” Same model, same CLI, same config β€” aspire run to aspire deploy

Resources

Follow Chris Ayers

Questions?

We've all been there. The README says "just run docker-compose up" but it never works the first time. Five languages, one app, no map.

Each language has its own logging, its own config format, its own service discovery pattern. You end up with hardcoded URLs everywhere.

Use the official positioning sentence verbatim β€” it sets up everything that follows. Then transition to the four pillars.

These three promises map onto the rest of the talk. Each one shows up everywhere.

This is the Aspire AppHost β€” the central brain that starts everything and wires it together. Python, React, .NET, all visible in one dashboard.

This is the payoff β€” show it early. One dashboard for everything, regardless of language. Click a trace to see the full waterfall across Python, .NET, and Node.js. Export as .env for local debugging.

This closes a real adoption gap for polyglot teams: "I'm not on .NET, can I still use any of this?" Yes β€” start here.

Once they like the UI, the AppHost story becomes a much easier sell.

Add the OTel SDK, point at the env var Aspire injects, and you're done.

Node gets even better β€” auto-instrumentations grab HTTP, DB, redis, etc. for free.

The AppHost is the central orchestrator. All services report their telemetry to the unified dashboard.

Now let's look at the mechanisms under the hood.

This is the magic. The double underscore __ is used because environment variables can't have colons.

Each runtime has its own env-var idiom β€” but the pattern is identical.

Aspire handles connection-string complexity so you don't manage .env files.

For C# AppHosts using AddPostgres, connection strings are auto-generated. For polyglot AppHosts, you wire env vars explicitly β€” same result, more control.

Aspire manages startup ordering automatically based on WithReference and WaitFor dependencies. Infrastructure comes up first, then backends, then frontends. Over 100 integrations are wired to the same lifecycle so health/dependency rules apply to everything in the AppHost.

C# is the original AppHost language. Most existing samples use this.

The TypeScript AppHost uses the same integration packages as C#.

Per aspire.dev/languages-and-runtimes: only C# and TypeScript are documented AppHost authoring languages today.

AppHost authoring is C#/TS today; workload language support is separate and much broader.

Integration authors don't write a TS binding by hand β€” the analyzer + ATS scanner generates it.

That's how 100+ .NET integrations show up automatically in TypeScript AppHosts.

Quick reference for the runtime side.

Keep this slide handy β€” these are the building blocks for everything we covered.

Drop aspire.config.json at the project root β€” the CLI reads it on every command.

Common gotcha: feature flags are JSON booleans, not strings.

The CLI is the friendlier surface β€” most teams never edit aspire.config.json directly.

aspire doctor is great before a talk. aspire start runs in detached mode. aspire new is language-aware β€” it scaffolds the right AppHost structure for your chosen language.

Per aspire.dev/get-started/aspire-mcp-server/. CLI MCP = stdio + `aspire agent init`. Dashboard MCP = streamable HTTP + API key from the dashboard UI.

Earlier drafts invented an `http://localhost:15889/mcp` URL β€” doesn't exist for the CLI server.

Time to see Aspire in action!

Ordered by escalating complexity β€” same orchestration model, increasing sophistication.

The simplest polyglot demo. TypeScript AppHost with auto-wired API and frontend.

Python orchestrating Python β€” the AppHost and the service are both Python.

Full-stack TypeScript-orchestrated app with Python backend and Redis caching.

Real-time voting with HTMX partial updates, Django backend, PostgreSQL persistence.

Java orchestrating Java β€” experimental Java AppHost with Spring Boot and PostgreSQL.

Go orchestrating a full-stack app β€” Go API backend with Svelte frontend.

Classic .NET AppHost orchestrating Angular frontend with CosmosDB emulator.

The ultimate polyglot demo β€” three languages, one event pipeline, full distributed tracing.

This works whether your services are Python, Go, Java, TypeScript, .NET β€” you don't need per-language deploy plumbing.

The pivot: aspire run β†’ aspire deploy. Same model, no extra plumbing.

If your team uses multiple languages, Aspire gives you a single place to define, run, observe, and deploy your entire stack.