Polyglot Aspire

Orchestrating Any Language with Aspire

Chris Ayers

Chris Ayers

Principal Software Engineer
Azure CXP AzRel
Microsoft

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

The Polyglot Problem

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

Your orchestration pain

  • Docker Compose: manual port wiring, no built-in telemetry, no health-aware startup ordering
  • Each language has its own config format (.env, YAML, application.properties...)
  • No unified observability — good luck tracing a request across 4 services
  • 15-step README to run locally — "just docker-compose up" never works first time

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

Aspire: The Polyglot Answer

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

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 AppHost — Your Stack in Code

Write your AppHost in the language your team knows:

C# AppHost

var builder = DistributedApplication
    .CreateBuilder(args);

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

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

builder.Build().Run();

TypeScript AppHost

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 — Redis, Azure, Kafka, MongoDB, PostgreSQL — available in both C# and TypeScript.

AppHost Languages

Pick the language your team already knows:

🟦 TypeScriptapphost.ts

await builder.addNodeApp("api", "./src", "server.js")
  .withHttpEndpoint({ env: "PORT" });

💜 C# (.NET)AppHost.cs

builder.AddProject<Projects.Api>("api")
       .WithHttpEndpoint(env: "PORT");

All produce the same result:
Dashboard, service discovery, and telemetry work identically. C# has the richest typed integrations; other SDKs use addDockerfile/addContainer as universal building blocks.

🐍 Pythonapphost.py FUTURE STATE

api = builder.add_dockerfile("api", "./src")
api.with_http_endpoint(target_port=8080, env="PORT")

☕ JavaAppHost.java FUTURE STATE

builder.addDockerfile("api", "./src", null, null)
  .withExternalHttpEndpoints();

🟢 Goapphost.go FUTURE STATE

api, _ := builder.AddDockerfile("api", "./src", nil, nil)
api.WithHttpEndpoint(nil, float64Ptr(8080), stringPtr("http"), nil, nil)

aspire.config.json — One Config for Every Language

This file tells the CLI which language your AppHost uses:

{
  "appHost": { "path": "apphost.py", "language": "python" },
  "sdk": { 
  "version": "13.2.0" },
   "packages": {
    "Aspire.Hosting.Redis": "13.2.0",
    "Aspire.Hosting.JavaScript": "13.2.0",
    "Aspire.Hosting.Python": "13.2.0",
    "Aspire.Hosting.OpenAI": "13.2.0"
  }
}

What it does:

  • appHost.path + appHost.language — declares your stack
  • sdk.version — pins the Aspire SDK version (e.g. 13.2.0)
  • Feature flags use boolean true (not string "true")
  • Replaces old .aspire/settings.json split config

Manage from CLI:

  • aspire config list / get / set
  • aspire secret set/list/get/delete
  • aspire certs clean/trust

Every sample in this talk has one at its root.

How It Works

The three patterns that make polyglot orchestration possible

Service Discovery

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

Read them in any language — same pattern everywhere:

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");

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

Connection Strings

Infrastructure resources get connection strings automatically:

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

Same pattern in every language:

# Python — Redis
client = redis.from_url(f"redis://{os.environ['CONNECTIONSTRINGS__cache']}")
// Node.js — Kafka
const kafka = new Kafka({ brokers: [process.env.CONNECTIONSTRINGS__messaging] });
# Java Spring Boot — via explicit env vars from AppHost
spring.datasource.url=jdbc:postgresql://${PG_HOST:localhost}:${PG_PORT:5432}/${PG_DB:notesdb}
spring.datasource.username=${PG_USER:postgres}

Aspire injects environment variables — your service reads them using its language's standard env var mechanism.

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

Cross-language tracing example:

Trace ID: abc123...
└─ POST /api/process (Python, 245ms)
   ├─ POST /data (.NET, 200ms)
   │  ├─ SQL SELECT (PostgreSQL, 50ms)
   │  └─ POST /notify (Node.js, 100ms)
   │     └─ Redis SET (25ms)
   └─ Render response (40ms)

Aspire sets OTEL_EXPORTER_OTLP_ENDPOINT automatically — add OpenTelemetry to your service and traces flow to the dashboard.

CLI for Every Language

No .NET SDK required — the Aspire CLI is a standalone binary.

http://get.aspire.dev

# Install
irm https://aspire.dev/install.ps1 | iex         # Windows
curl -sSL https://aspire.dev/install.sh | bash  # macOS / Linux

Day-to-day workflow

aspire run              # Start everything
aspire run --detach     # Background mode
aspire ps               # List running apps
aspire stop             # Stop everything
aspire describe --follow  # Watch resources live

Diagnostics & maintenance

aspire doctor           # Check SDK, certs, Docker, WSL2
aspire restore          # Regenerate SDK code
aspire export           # Capture telemetry to zip
aspire update --self    # Upgrade CLI
aspire docs search "redis"  # Docs from terminal

Works identically whether your AppHost is TypeScript or C#.

aspire new — Language-Aware Scaffolding

Start a new project in your team's language:

aspire new aspire-ts-starter -n my-app    # TypeScript AppHost
aspire new aspire-starter -n my-app       # C# AppHost (classic)
cd my-app && aspire run   # Runs immediately — no extra setup

What you get:

  • AppHost file in your chosen language
  • aspire.config.json pre-configured
  • Sample service wired up with service discovery
  • Dashboard ready on first run

Demos

Wrap-Up

Key Takeaways

🌍 Aspire is a polyglot orchestrator — not just for .NET

🗣️ AppHost languages — C#, TypeScript, *Python, *Go, *Java

📡 Universal patternsservices__name__http__0 and CONNECTIONSTRINGS__resource work in every language

📊 One dashboard — Logs, traces, metrics across all services via OpenTelemetry

🛠️ Standalone CLIaspire run / aspire doctor / aspire new — no .NET SDK required

📦 40+ integrations — Redis, Kafka, PostgreSQL, CosmosDB — richest in C#, growing in all SDKs

🚀 Deploy anywhere — Same AppHost model for local dev, staging, and production

🔓 Open sourcegithub.com/dotnet/aspire — MIT license

Resources

Follow Chris Ayers

Questions?

Appendix

Architecture Overview

center

Aspire Polyglot Cheat Sheet

Runtime → Method (C# SDK)

  • Python / ASGI → AddPythonApp()
  • Python / Uvicorn → AddUvicornApp()
  • Node.js → AddNodeApp()
  • Vite / React → AddViteApp()
  • .NET project → AddProject<T>()
  • JavaScript → AddJavaScriptApp()
  • Any Dockerfile → AddDockerfile()
  • Any executable → AddExecutable()

Common Patterns

.WithReference(redis)
.WaitFor(postgres)
.WithHttpEndpoint(env: "PORT")
.WithExternalHttpEndpoints()
.WithUv()
.WithNpm()
.WithBun()
.WithBuildSecret("key", secret)
.WithHttpHealthCheck("/health")
.WithMcpServer("mcp")

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

OpenTelemetry Setup by Language

Python:

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)

Node.js:

const { NodeTracerProvider } =
  require('@opentelemetry/sdk-trace-node');
const { OTLPTraceExporter } =
  require('@opentelemetry/exporter-trace-otlp-grpc');
const { BatchSpanProcessor } =
  require('@opentelemetry/sdk-trace-base');

const provider = new NodeTracerProvider();
provider.addSpanProcessor(
  new BatchSpanProcessor(new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
  }))
);
provider.register();

Aspire sets OTEL_EXPORTER_OTLP_ENDPOINT automatically — add this boilerplate once per service and traces flow to the dashboard.

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

Deploying Polyglot Aspire Apps

aspire run       # Local development
aspire deploy    # Deploy to Azure Container Apps
aspire publish   # Generate deployment artifacts

What Aspire generates:

  • Container images for Python, Node.js, .NET, Go, Java
  • Azure Container Apps / Kubernetes manifests
  • Infrastructure wiring for Redis, Postgres, CosmosDB, Kafka
  • Service connections + environment variables

Same AppHost model for local, staging, and production.

We've all been there: README says "just run docker-compose up" but it never works the first time. Each language has its own logging, its own config format, its own service discovery pattern. You end up with hardcoded URLs everywhere.

This is the Aspire AppHost. It's the central brain that starts everything and wires it together. Python, React, .NET — all visible in one dashboard, auto-wired, observable. No .NET SDK required for non-.NET devs if you use a polyglot AppHost.

The TypeScript AppHost uses the same integration packages as C#, auto-generated via [AspireExport] attributes. A JS/TS team never needs to touch .NET.

Each language has its own idioms — Python uses snake_case, TypeScript uses camelCase, Go returns errors — but the Aspire model is the same everywhere. The C# SDK has the most typed integrations, but all five languages can orchestrate any service via Dockerfile or container.

This is the key file. Drop aspire.config.json in your project root, point it at your AppHost file, set the language, and aspire run just works. The CLI reads this to know how to build and launch your app.

Now let's look at the three mechanisms that power everything: service discovery, connection strings, and the dashboard. These work the same regardless of AppHost language.

This is the magic. The double underscore __ is used because environment variables can't have colons. Every language can read env vars — that's the universal interface.

Aspire handles the complexity of connection strings so you don't have to 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.

This is the killer feature. 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.

The standalone CLI bundles DCP — the Developer Control Plane. A Python or Java developer uses the exact same commands as a .NET developer. aspire doctor is great for troubleshooting environment issues before a talk or demo.

aspire new is language-aware — it scaffolds the right AppHost structure for your chosen language. This is the fastest way to start a polyglot project.

Time to see Aspire in action! We'll walk through 8 samples — each running live with the Aspire dashboard. Watch for: service startup ordering, cross-language logs, and the unified trace view.

The key message: if your team uses multiple languages, Aspire gives you a single place to define, run, observe, and deploy your entire stack. The AppHost language is a choice, not a constraint.

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

Keep this slide handy as a quick reference!