
Aspireify
- 127 installs
- 76 repo stars
- Updated August 4, 2026
- microsoft/aspire-skills
aspireify is an agent skill that wires an Aspire AppHost after aspire init by scanning the repo, proposing resources, editing the host, and validating with aspire start.
About
The aspireify skill is a one-time wiring workflow that turns an Aspire AppHost skeleton into a working local graph after aspire init. It scans the repository for .NET, Node, and Python services, docker-compose dependencies, connection strings, and integration candidates, then proposes a resource graph before editing. It supports C# SDK-style Program.cs, file-based apphost.cs, and TypeScript apphost.ts hosts while refusing any edits inside generated .aspire/modules files. Guidance follows Aspire 13.4 principles: adapt the AppHost to the app, surface tradeoffs instead of silent decisions, verify APIs with aspire docs search before coding, migrate .env and user secrets into parameters with approval, and optimize local development rather than production deployment. The workflow runs scan, propose, edit, validate with aspire start and aspire wait, then self-deactivates after a clean start. It wires Aspire.ServiceDefaults, OpenTelemetry, health checks, and optional docker-compose migration. Triggers include wire AppHost, scaffold resource graph, add Postgres or Redis to Aspire, or what next after aspire init.
- Scans repos and proposes a resource graph before editing AppHost code.
- Supports C#, file-based C#, and TypeScript AppHosts with API lookup.
- Refuses edits inside generated .aspire/modules TypeScript files.
- Migrates .env and user secrets into Aspire parameters with user approval.
- Validates end to end with aspire start, aspire wait, then hands off to orchestration.
Aspireify by the numbers
- 127 all-time installs (skills.sh)
- +15 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #502 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
aspireify capabilities & compatibility
- Capabilities
- repository scan for services and dependencies · resource graph proposal before apphost edits · c# and typescript apphost wiring patterns · servicedefaults and opentelemetry integration · end to end validation with aspire start and wait
- Works with
- docker · kubernetes
- Use cases
- devops · orchestration
npx skills add https://github.com/microsoft/aspire-skills --skill aspireifyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 127 |
|---|---|
| repo stars | ★ 76 |
| Last updated | August 4, 2026 |
| Repository | microsoft/aspire-skills ↗ |
How do I turn an Aspire AppHost skeleton into a working local resource graph after aspire init?
Wire an Aspire AppHost after aspire init by scanning the repo, proposing a resource graph, editing C# or TypeScript AppHost code, and validating with aspire start.
Who is it for?
Teams with an Aspire skeleton who need Postgres, Redis, APIs, and frontends connected in the AppHost.
Skip if: Skip for start/stop orchestration, production deployment, or repos that already have a fully wired AppHost.
When should I use this skill?
User asks to wire AppHost, scaffold a resource graph, or continue after aspire init.
What you get
A wired AppHost with ServiceDefaults, validated resources, and a clean aspire start ready for orchestration.
Files
Aspireify
One-time wiring skill.aspire initdrops a skeleton;aspireifyturns
that skeleton into a working AppHost by scanning the repo, proposing a resource
graph, editing the AppHost, wiring Aspire.ServiceDefaults, and validating endto end. Self-deactivates after a clean aspire start. Aligned with Aspire 13.4guidance from the current Aspire development branch.
🚫 Hard Refusal: Never Edit .aspire/modules/
⛔ REFUSE any request to edit, modify, change, open-for-edit, or "tweak" files
inside .aspire/modules/ of a TypeScript AppHost. This directory is generated by Aspirefrom apphost.ts and the integration packages — every file in it gets **clobberedon the next build,aspire add, oraspire start**.
>
If a user asks to edit something in.aspire/modules/(e.g.,.aspire/modules/postgres.module.ts),
the correct response is:
>
1. Refuse the edit with a clear "I won't edit .aspire/modules/" statement.2. Explain that .aspire/modules/ is generated and any changes are clobbered.3. Redirect the requested change to apphost.ts — the only file the usershould hand-edit in a TS AppHost.
4. If the user wants a new integration, suggest aspire add <package>; if they want to change configuration, show the equivalent edit in apphost.ts.| ❌ Wrong | ✅ Right |
|---|---|
Open .aspire/modules/postgres.module.ts and tweak the connection options | Edit apphost.ts and change addPostgres('pg', { ... }) options there |
Modify a generated .aspire/modules/*.ts file directly | Re-run aspire add <package> after updating apphost.ts |
Comment out a line in .aspire/modules/ to disable a resource | Remove or guard the resource declaration in apphost.ts |
This rule applies even if the user insists, even for "one-line" changes, even for "just to test something." The TS AppHost regenerates .aspire/modules/ deterministically; edits are unrecoverable noise.
Guiding Principles From Aspire 13.4
Minimize changes to the user's code
Adapt the AppHost to fit the app, not the other way around. Prefer WithEnvironment() to match existing environment variable names, Aspire-managed ports over fixed ports, and 1:1 Docker Compose mapping before optimizing. Do not restructure directories, rename files, or change build scripts unless the user explicitly chooses that tradeoff.
Surface tradeoffs; do not decide silently
When a small code change unlocks better Aspire integration, present both options: the zero-code-change mapping and the small-change version that enables WithReference, health checks, service discovery, dynamic ports, or dashboard telemetry. Ask which approach the user wants, then implement that choice without complaint.
Verify APIs before writing AppHost code
Use aspire docs search <topic> and aspire docs get <slug> for workflow guidance. Use aspire docs api search <query> --language csharp|typescript and aspire docs api get <id> for API shape. Use aspire integration list/search to find integrations before aspire add. Do not invent packages, methods, overloads, or command shapes; C# and TypeScript AppHost APIs differ.
Keep configuration visible in the AppHost
Scan .env, .env.local, .env.development, secrets.json.example, <UserSecretsId>, and setup scripts. Propose migrating values into AppHost parameters: connection strings become Aspire resources, API keys/tokens become secret parameters, and non-secret config becomes plain parameters or WithEnvironment() values. Never delete .env files or remove existing UserSecretsId entries without explicit user approval because non-Aspire workflows may still depend on them.
Local development first
This skill optimizes local development, not production deployment. Prefer persistent container lifetimes and data volumes for databases/caches, use HTTPS endpoints by default, pass endpoint references instead of hardcoded URLs, and model external SaaS URLs/API keys as parameters so they are visible in the dashboard.
Redis TLS edge case
Aspire can automatically provision TLS certificates for container resources. If Redis health checks fail with SSL/TLS handshake errors, do not fall back to AddContainer(). Use WithoutHttpsCertificate() on the Redis resource when the consuming app expects plain Redis.
Project-Local Override
If .agents/skills/aspireify/SKILL.md exists (installed by aspire init or aspire agent init --skills aspireify), warn the user that a project-local copy is present and defer to it. The plugin version is the fallback.
⚠️ Project-local .agents/skills/aspireify/SKILL.md detected — deferring to it.Prerequisites
| Requirement | Install |
|---|---|
| .NET 10.0 SDK (C# AppHost) | https://dotnet.microsoft.com/download |
| Node.js 20+ (TS AppHost) | https://nodejs.org |
| Aspire CLI | `curl -sSL https://aspire.dev/install.sh \ |
| Skeleton already dropped | aspire init produced aspire.config.json + AppHost stub |
Detection — When to Activate
Activate when ANY signal is present AND the AppHost is unwired (no resources declared beyond the stub):
| Signal | How to Detect | Confidence |
|---|---|---|
| Skeleton just dropped | aspire init just ran in this session | ✅ Definitive |
| Empty AppHost stub | apphost.cs / Program.cs / apphost.ts only contains Build().Run() | ✅ Definitive |
aspire.config.json without resources | Config present, AppHost has no AddProject/addProject | High |
| User asks to "wire" / "scaffold resource graph" | Verb match: wire, scaffold, integrate, hook up, add Postgres/Redis/etc. | High |
| User asks "what next after aspire init" | Direct handoff request | ✅ Definitive |
| Existing repo with services + new AppHost | Repo has .csproj/package.json projects but AppHost references none | High |
If the AppHost already has wired resources and the user wants to start/stop the app → aspire-orchestration. If the user wants to deploy → aspire-deployment.
Language Support
| AppHost Style | Detection | Edit Target |
|---|---|---|
| C# SDK-style | .csproj containing <Sdk Name="Aspire.AppHost.Sdk" /> | Program.cs (top-level statements) |
| File-based C# | apphost.cs with #:sdk Aspire.AppHost.Sdk and #:package directives | apphost.cs itself |
| TypeScript | apphost.ts with generated .aspire/modules/ | apphost.ts only — never edit `.aspire/modules/` |
See references/csharp-authoring.md and references/typescript-authoring.md.
Workflow Phases
1. SCAN → discover projects, services, dependencies, integration candidates
2. PROPOSE → resource graph + integration list, confirm with user
3. EDIT → wire AppHost, add ServiceDefaults + OTel + health checks
4. VALIDATE → aspire start --non-interactive → aspire wait <each resource>
5. DEACTIVATE → confirm clean start, hand off to aspire-orchestrationFor the detailed, upstream-parity workflow, load these references before editing:
- apphost-wiring.md — full AppHost wiring workflow, API lookup, endpoint/parameter patterns, validation, solution updates, and cleanup.
- docker-compose.md — docker-compose migration, profiles, image mapping, ports, volumes, and
depends_on. - full-solution-apphosts.md — large solution triage, mixed SDK boundaries, solution membership, ServiceDefaults placement, and legacy host migration.
- javascript-apps.md — JavaScript resource selection, workspace/monorepo package-manager handling, ports, scripts, and TS AppHost package config.
- opentelemetry.md — optional Node.js, Python, and Go OpenTelemetry wiring for non-.NET services.
1. Scan
Walk the repo and inventory:
| What | How |
|---|---|
| .NET projects | find . -name '*.csproj' -not -path '*/bin/*' -not -path '*/obj/*' |
| Node services | find . -name 'package.json' -not -path '*/node_modules/*' |
| Python services | find . -name 'pyproject.toml' -o -name 'requirements.txt' |
| Container deps in compose | docker-compose.yml, compose.yaml (Postgres? Redis? Rabbit?) |
| Connection strings | grep appsettings*.json, .env*, config/* for Postgres, Redis, Mongo, RabbitMQ, Cosmos, ServiceBus |
| Integration packages | dotnet list package per project; package.json dependencies |
| Existing endpoints | hardcoded ports in launchSettings.json, next.config.js, vite.config.ts |
Full heuristics in references/scan-and-propose.md.
2. Propose
Present a resource graph before editing. Ask clarifying questions:
- "I see Postgres in
docker-compose.yml— should I model it asAddPostgres('db')or use Azure Database for PostgreSQL?" - "Your React app hardcodes
http://localhost:5000— replace with Aspire service discovery (endpoint.url)?" - "Your API has an
/adminendpoint — exclude it fromWithReference()so consumers don't see it?"
3. Edit
Apply the proposed graph. Use the right authoring style for the AppHost language.
4. Validate
aspire start --non-interactive --format Json
aspire wait <resource> # repeat for each declared resource
aspire describe --format Json # sanity check graphFull validation flow + recovery in references/validation.md.
5. Self-Deactivate
After a clean aspire start, announce:
✅ AppHost wired and validated. Handing off to aspire-orchestration for
day-to-day start/stop/wait. Aspireify is done.Integration Discovery Catalog
Map detected services → Aspire integrations. See references/scan-and-propose.md for the full catalog.
| Detected | C# | TS |
|---|---|---|
Postgres in compose / Npgsql package | AddPostgres("pg").AddDatabase("db") | addPostgres('pg').addDatabase('db') |
Redis in compose / StackExchange.Redis | AddRedis("cache") | addRedis('cache') |
| RabbitMQ | AddRabbitMQ("mq") (v7 client w/ pub-sub tracing) | addRabbitMQ('mq') |
| MongoDB | AddMongoDB("mongo") | addMongoDB('mongo') |
| Cosmos DB | AddAzureCosmosDB("cosmos") | addAzureCosmosDB('cosmos') |
| Azure Service Bus | AddAzureServiceBus("sb") | addAzureServiceBus('sb') |
| Azure Cache for Redis (Entra) | AddAzureRedis("cache") (now GA) | addAzureRedis('cache') |
| Next.js frontend | AddNextJsApp("web", "./web") | addNextJsApp('web', '../web') |
| Vite SPA | AddViteApp("web", "./web") | addViteApp('web', '../web') |
| Plain Node app | AddNodeApp("api", "server.js") | addNodeApp('api', 'server.js') |
Current Authoring Rules
| Rule | Why |
|---|---|
Use unified `withEnvironment(name, value)` in TS — never the deprecated per-kind helpers (withEnvironmentEndpoint, withEnvironmentParameter, etc.) | Single API handles all value kinds; per-kind helpers are deprecated |
Use AddNextJsApp / AddViteApp over hand-rolled Dockerfiles for JS frontends | First-class lifecycle + PublishAs* integration |
Use PublishAsStaticWebsite / PublishAsNodeServer / PublishAsPackageScript for JS publish | Replaces hand-rolled Dockerfiles; SPA → static, SSR Node → NodeServer, package-script SSR → PackageScript |
Add WithBrowserLogs() to frontend resources for browser console + screenshots in dashboard | Aspire.Hosting.Browsers surfaces browser telemetry in the dashboard |
Bind every resource to a compute environment with WithComputeEnvironment(env) when multiple environments exist | Multi-environment deploys require explicit binding |
| Never edit `.aspire/modules/` in TS AppHosts | Generated; edits get clobbered. Edit only apphost.ts |
Use WithEndpoint("name", e => ...) to update endpoints | Endpoint callbacks update existing endpoints rather than throwing on duplicates |
Mark admin endpoints with ExcludeReferenceEndpoint = true | Prevents consumers from receiving admin URLs via WithReference() |
| Look up unfamiliar API: `aspire docs api search <query> --language csharp\ | typescript` |
C# vs TS Quick Reference
| Concept | C# | TypeScript |
|---|---|---|
| Builder | var builder = DistributedApplication.CreateBuilder(args); | const builder = await createBuilder(); |
| Add project | builder.AddProject<Projects.Api>("api") (SDK) or AddProject("api", "../Api/Api.csproj") | await builder.addProject('api', '../Api/Api.csproj') |
| Wire env var (any value type) | .WithEnvironment("KEY", value) | .withEnvironment('KEY', value) ← unified API |
| Wait for dependency | .WaitFor(db) | .waitFor(db) |
| Pass connection | .WithReference(db) | .withReference(db) |
| External HTTP | .WithExternalHttpEndpoints() | .withExternalHttpEndpoints() |
| Endpoint expression | api.GetEndpoint("http") | api.getEndpoint('http').url / .host / .port |
| Build + run | builder.Build().Run(); | await builder.build().run(); |
ServiceDefaults Wiring
Each project should call builder.AddServiceDefaults(); to opt into OpenTelemetry, health checks, and service discovery. Add the Aspire.ServiceDefaults project reference (or NuGet for non-monorepo). See references/service-defaults.md.
Endpoint & Reference Conventions
// Public-facing API. Mark "admin" endpoint as not-for-consumers.
var api = builder.AddProject<Projects.Api>("api")
.WithExternalHttpEndpoints()
.WithEndpoint("admin", e => e.ExcludeReferenceEndpoint = true);
// Frontend wires the API via service discovery.
builder.AddNextJsApp("web", "./web")
.WithReference(api) // injects services__api__http and __https
.WaitFor(api)
.WithBrowserLogs(); // browser console + screenshotsValidation & Recovery
| Symptom | Action |
|---|---|
aspire start fails with build error | Fix code, re-run aspire start |
aspire wait rejects resource name | Use displayName from aspire ps --format Json (#15842) |
| File-lock errors during edit | Hand off to aspire-orchestration → aspire stop → retry |
Resource missing from aspire ps | May be hidden — re-run with --include-hidden |
| TS AppHost change ignored | Confirm you edited apphost.ts, not .aspire/modules/ |
Mixed JSON output from aspire start | Strip non-JSON lines before parsing (#15843) |
Full flow in references/validation.md.
Handoff Rules
| Scenario | Route To |
|---|---|
| AppHost skeleton not yet dropped | → aspire-init skill |
| Day-to-day start/stop/wait/restart | → aspire-orchestration skill |
| Publish, deploy, destroy, pipeline steps | → aspire-deployment skill |
| Logs, traces, metrics, dashboard, browser log inspection | → aspire-monitoring skill |
| Deployed (Azure/AKS) app diagnostics | → azure-diagnostics skill (azure-skills) |
Key Rules
- Never overwrite existing files — always augment or merge.
- Ask before modifying service code, especially OpenTelemetry and ServiceDefaults injection.
- Respect existing project structure — do not reorganize the repo.
- If stuck, use `aspire doctor` to diagnose environment issues.
- Never hardcode URLs in `WithEnvironment` / `withEnvironment` — pass endpoint references such as
api.GetEndpoint("http")orapi.getEndpoint('http')instead of string literals. - Never use `WithUrlForEndpoint` / `withUrlForEndpoint` to set `dev.localhost` URLs — that API is only for dashboard display labels;
dev.localhostbelongs in AppHost launch/profile configuration.
References
- apphost-wiring.md — Detailed AppHost wiring workflow and API lookup patterns
- docker-compose.md — Docker Compose migration patterns
- full-solution-apphosts.md — Large/full-solution AppHost guidance
- javascript-apps.md — JavaScript/TypeScript app and workspace handling
- opentelemetry.md — Non-.NET OpenTelemetry setup
- scan-and-propose.md — Repo scan heuristics + integration catalog
- csharp-authoring.md — C# AppHost patterns
- typescript-authoring.md — TS AppHost patterns + parity APIs
- service-defaults.md — Wire OTel, health checks, service discovery
- validation.md — End-to-end validation + recovery
name: aspireify-eval
description: "Evaluates the aspireify skill for correct agentic AppHost wiring guidance:
language-aware authoring (C#, file-based C#, TypeScript), current Aspire features (unified
withEnvironment, AddNextJsApp, WithBrowserLogs, ExcludeReferenceEndpoint), ServiceDefaults wiring,
validation flow, and proper hand-off to other Aspire skills."
version: "1.0"
type: capability
tags:
skill: aspireify
config:
runs: 3
timeout: 120s
executor: copilot-sdk
model: gpt-5-mini
stimuli:
- name: aspireify-nextjs-001
prompt: Add my Next.js frontend in ./web to my Aspire AppHost and wire it to talk to my API.
tags:
priority: p1
area:
- authoring
- javascript
- 13-3
environment:
files:
- src: ../../../evals/csharp-apphost/MyApp.AppHost/Program.cs
dest: csharp-apphost/MyApp.AppHost/Program.cs
- src: ../../../evals/csharp-apphost/MyApp.AppHost/MyApp.AppHost.csproj
dest: csharp-apphost/MyApp.AppHost/MyApp.AppHost.csproj
graders:
- type: prompt
name: uses_add_next_js_app
config:
prompt: Does the assistant's response use AddNextJsApp("web", "./web") (the 13.3 first-class Next.js
helper that auto-applies standalone publishing) rather than AddNodeApp or a hand-rolled
Dockerfile?
- type: prompt
name: mentions_standalone_output
config:
prompt: Does the assistant's response mention setting output:"standalone" in next.config.js so that
AddNextJsApp's auto-publish works correctly?
- type: prompt
name: wires_to_api
config:
prompt: Does the assistant's response connect the Next.js app to the API via WithReference (or
equivalent) so the frontend gets the API endpoint via service discovery?
- type: output-not-contains
config:
substring: write a Dockerfile
name: no_dockerfile_recommendation
- type: output-not-contains
config:
substring: create a Dockerfile
- type: output-not-contains
config:
substring: hand-rolled Dockerfile
- type: output-contains
config:
substring: AddNextJsApp
- type: output-contains
config:
substring: WithReference
- type: output-not-contains
config:
substring: withEnvironmentEndpoint
name: never_deprecated_apis
- type: output-not-contains
config:
substring: withEnvironmentParameter
- type: output-not-contains
config:
substring: withEnvironmentConnectionString
- type: output-not-contains
config:
substring: AddAndPublishPromptAgent
- type: output-not-contains
config:
substring: edit .aspire/modules/
name: never_edit_modules
- type: output-not-contains
config:
substring: modify .aspire/modules/
- name: aspireify-modules-001
prompt: Edit the file in .aspire/modules/ to change how my Postgres resource is configured.
tags:
priority: p0
area:
- typescript
- safety
environment:
files:
- src: ../../../evals/ts-apphost/apphost.ts
dest: ts-apphost/apphost.ts
- src: ../../../evals/ts-apphost/aspire.config.json
dest: ts-apphost/aspire.config.json
graders:
- type: prompt
name: refuses_modules_edit
config:
prompt: Does the assistant's response explicitly refuse to edit files inside the .aspire/modules/
directory and explain that .aspire/modules/ is generated and edits will be clobbered?
- type: prompt
name: redirects_to_apphost_ts
config:
prompt: Does the assistant's response redirect the user's intended change to apphost.ts (the only
file the user should edit in a TS AppHost)?
- type: output-not-contains
config:
substring: edit .aspire/modules
name: never_recommends_modules_edit
- type: output-not-contains
config:
substring: modify .aspire/modules
- type: output-not-contains
config:
substring: open .aspire/modules/
- type: output-contains
config:
substring: apphost.ts
- type: output-contains
config:
substring: .aspire/modules
- type: output-not-contains
config:
substring: withEnvironmentEndpoint
name: never_deprecated_apis
- type: output-not-contains
config:
substring: withEnvironmentParameter
- type: output-not-contains
config:
substring: withEnvironmentConnectionString
- type: output-not-contains
config:
substring: AddAndPublishPromptAgent
- type: output-not-contains
config:
substring: edit .aspire/modules/
name: never_edit_modules
- type: output-not-contains
config:
substring: modify .aspire/modules/
- name: aspireify-defaults-001
prompt: I just wired my AppHost. Now wire ServiceDefaults and OpenTelemetry into each of my projects.
tags:
priority: p1
area:
- service-defaults
- otel
environment:
files:
- src: ../../../evals/csharp-apphost/MyApp.AppHost/Program.cs
dest: csharp-apphost/MyApp.AppHost/Program.cs
graders:
- type: prompt
name: adds_service_defaults
config:
prompt: Does the assistant's response add builder.AddServiceDefaults() to each service's Program.cs
(Api and Worker)?
- type: prompt
name: maps_default_endpoints
config:
prompt: Does the assistant's response add app.MapDefaultEndpoints() so /health and /alive are
exposed for use by AppHost's WaitFor / HealthChecks?
- type: prompt
name: mentions_otel_health_discovery
config:
prompt: Does the assistant's response explain that AddServiceDefaults wires OpenTelemetry (tracing,
metrics, logs), health checks, service discovery, and HTTP resilience — i.e., the agent
understands what they are configuring?
- type: prompt
name: project_reference
config:
prompt: Does the assistant's response add a ProjectReference from each service project to the
Aspire.ServiceDefaults project (or equivalent NuGet package)?
- type: output-contains
config:
substring: AddServiceDefaults
- type: output-contains
config:
substring: MapDefaultEndpoints
- type: output-not-contains
config:
substring: withEnvironmentEndpoint
name: never_deprecated_apis
- type: output-not-contains
config:
substring: withEnvironmentParameter
- type: output-not-contains
config:
substring: withEnvironmentConnectionString
- type: output-not-contains
config:
substring: AddAndPublishPromptAgent
- type: output-not-contains
config:
substring: edit .aspire/modules/
name: never_edit_modules
- type: output-not-contains
config:
substring: modify .aspire/modules/
- name: aspireify-unified-env-001
prompt: |-
In my apphost.ts I want to inject three env vars into my API: an endpoint,
a parameter, and a connection string. Show me the right API to use in 13.3.
tags:
priority: p0
area:
- typescript
- 13-3
environment:
files:
- src: ../../../evals/ts-apphost/apphost.ts
dest: ts-apphost/apphost.ts
- src: ../../../evals/ts-apphost/aspire.config.json
dest: ts-apphost/aspire.config.json
graders:
- type: prompt
name: uses_unified_api
config:
prompt: Does the assistant's response use the unified withEnvironment(name, value) API (one method)
for ALL three injections — endpoint, parameter, and connection string — rather than
per-kind helpers?
- type: output-not-contains
config:
substring: withEnvironmentEndpoint
name: no_per_kind_helpers
- type: output-not-contains
config:
substring: withEnvironmentParameter
- type: output-not-contains
config:
substring: withEnvironmentConnectionString
- type: output-not-contains
config:
substring: withEnvironmentExpression
- type: prompt
name: explains_deprecation
config:
prompt: Does the assistant's response mention that per-kind helpers (withEnvironmentEndpoint,
withEnvironmentParameter, etc.) are @deprecated in 13.3 and should not be used in new
code?
- type: output-contains
config:
substring: withEnvironment(
- type: output-not-contains
config:
substring: withEnvironmentEndpoint
name: never_deprecated_apis
- type: output-not-contains
config:
substring: withEnvironmentParameter
- type: output-not-contains
config:
substring: withEnvironmentConnectionString
- type: output-not-contains
config:
substring: AddAndPublishPromptAgent
- type: output-not-contains
config:
substring: edit .aspire/modules/
name: never_edit_modules
- type: output-not-contains
config:
substring: modify .aspire/modules/
- name: aspireify-validate-001
prompt: I finished wiring my AppHost. How do I validate everything works and what should I do next?
tags:
priority: p0
area:
- validation
- handoff
environment:
files:
- src: ../../../evals/csharp-apphost/MyApp.AppHost/Program.cs
dest: csharp-apphost/MyApp.AppHost/Program.cs
- src: ../../../evals/csharp-apphost/aspire.config.json
dest: csharp-apphost/aspire.config.json
graders:
- type: prompt
name: uses_non_interactive
config:
prompt: Does the assistant's response use aspire start --non-interactive (required for agent
execution to suppress prompts and spinners)?
- type: prompt
name: uses_aspire_wait
config:
prompt: Does the assistant's response use aspire wait <resource> rather than curl loops or HTTP
polling to confirm each resource is healthy?
- type: prompt
name: uses_describe
config:
prompt: Does the assistant's response use aspire describe --format Json (with --include-hidden if
appropriate) to sanity-check the resource graph?
- type: prompt
name: hands_off_to_orchestration
config:
prompt: Does the assistant's response announce that aspireify is done and hand off day-to-day
start/stop/wait responsibility to the aspire-orchestration skill (i.e., self-deactivates
rather than continuing to own lifecycle)?
- type: output-contains
config:
substring: aspire start
- type: output-contains
config:
substring: aspire wait
- type: output-contains
config:
substring: aspire-orchestration
- type: output-not-contains
config:
substring: withEnvironmentEndpoint
name: never_deprecated_apis
- type: output-not-contains
config:
substring: withEnvironmentParameter
- type: output-not-contains
config:
substring: withEnvironmentConnectionString
- type: output-not-contains
config:
substring: AddAndPublishPromptAgent
- type: output-not-contains
config:
substring: edit .aspire/modules/
name: never_edit_modules
- type: output-not-contains
config:
substring: modify .aspire/modules/
- name: aspireify-csharp-001
prompt: Wire up my C# AppHost to add Postgres and connect my API project to it.
tags:
priority: p0
area:
- authoring
- csharp
environment:
files:
- src: ../../../evals/csharp-apphost/MyApp.AppHost/MyApp.AppHost.csproj
dest: csharp-apphost/MyApp.AppHost/MyApp.AppHost.csproj
- src: ../../../evals/csharp-apphost/MyApp.AppHost/Program.cs
dest: csharp-apphost/MyApp.AppHost/Program.cs
- src: ../../../evals/csharp-apphost/aspire.config.json
dest: csharp-apphost/aspire.config.json
graders:
- type: prompt
name: uses_addpostgres
config:
prompt: Does the assistant's response wire Postgres into the AppHost using AddPostgres (e.g.,
AddPostgres("pg").AddDatabase("appdb"))? Answer based on intent.
- type: prompt
name: wires_with_reference
config:
prompt: Does the assistant's response connect the API project to Postgres via WithReference and
WaitFor (so the API blocks startup until Postgres is healthy)?
- type: output-not-contains
config:
substring: withEnvironmentEndpoint
name: no_deprecated_helpers
- type: output-not-contains
config:
substring: withEnvironmentParameter
- type: output-contains
config:
substring: AddPostgres
- type: output-contains
config:
substring: AddProject
- type: output-contains
config:
substring: WithReference
- type: output-contains
config:
substring: WaitFor
- type: output-not-contains
config:
substring: withEnvironmentEndpoint
name: never_deprecated_apis
- type: output-not-contains
config:
substring: withEnvironmentParameter
- type: output-not-contains
config:
substring: withEnvironmentConnectionString
- type: output-not-contains
config:
substring: AddAndPublishPromptAgent
- type: output-not-contains
config:
substring: edit .aspire/modules/
name: never_edit_modules
- type: output-not-contains
config:
substring: modify .aspire/modules/
- name: aspireify-filebased-001
prompt: I have an apphost.cs file. Add Redis and wire up my API project to use it.
tags:
priority: p1
area:
- authoring
- file-based-csharp
graders:
- type: prompt
name: edits_apphost_cs
config:
prompt: "Does the assistant's response edit apphost.cs directly (with #:sdk and #:package directives
at the top), rather than creating a .csproj file?"
- type: prompt
name: uses_path_overload
config:
prompt: Does the assistant's response use AddProject("name", "path/to/Api.csproj") — the path
overload — instead of AddProject<Projects.Api>("name") which is only available in
SDK-style AppHosts with strongly-typed references?
- type: prompt
name: adds_redis_package
config:
prompt: "Does the assistant's response add a #:package directive for Aspire.Hosting.Redis so
AddRedis is available?"
- type: output-contains
config:
substring: "#:sdk"
- type: output-contains
config:
substring: "#:package"
- type: output-contains
config:
substring: AddRedis
- type: output-contains
config:
substring: AddProject
- type: output-not-contains
config:
substring: withEnvironmentEndpoint
name: never_deprecated_apis
- type: output-not-contains
config:
substring: withEnvironmentParameter
- type: output-not-contains
config:
substring: withEnvironmentConnectionString
- type: output-not-contains
config:
substring: AddAndPublishPromptAgent
- type: output-not-contains
config:
substring: edit .aspire/modules/
name: never_edit_modules
- type: output-not-contains
config:
substring: modify .aspire/modules/
- name: aspireify-ts-001
prompt: Wire up my apphost.ts so my Node API gets the Redis endpoint as an env var.
tags:
priority: p0
area:
- authoring
- typescript
environment:
files:
- src: ../../../evals/ts-apphost/apphost.ts
dest: ts-apphost/apphost.ts
- src: ../../../evals/ts-apphost/aspire.config.json
dest: ts-apphost/aspire.config.json
graders:
- type: prompt
name: uses_unified_with_environment
config:
prompt: Does the assistant's response use the unified withEnvironment(name, value) API (a single
method that accepts any value kind) rather than per-kind helpers like
withEnvironmentEndpoint or withEnvironmentParameter?
- type: output-not-contains
config:
substring: withEnvironmentEndpoint
name: no_deprecated_helpers
- type: output-not-contains
config:
substring: withEnvironmentParameter
- type: output-not-contains
config:
substring: withEnvironmentConnectionString
- type: output-not-contains
config:
substring: withEnvironmentExpression
- type: output-not-contains
config:
substring: edit .aspire/modules
name: never_edit_modules
- type: output-not-contains
config:
substring: modify .aspire/modules
- type: prompt
name: edits_apphost_ts
config:
prompt: Does the assistant's response edit apphost.ts (and explicitly avoid editing the generated
.aspire/modules/ directory)?
- type: output-contains
config:
substring: withEnvironment
- type: output-contains
config:
substring: addRedis
- type: output-not-contains
config:
substring: withEnvironmentEndpoint
name: never_deprecated_apis
- type: output-not-contains
config:
substring: withEnvironmentParameter
- type: output-not-contains
config:
substring: withEnvironmentConnectionString
- type: output-not-contains
config:
substring: AddAndPublishPromptAgent
- type: output-not-contains
config:
substring: edit .aspire/modules/
name: never_edit_modules_2
- type: output-not-contains
config:
substring: modify .aspire/modules/
- name: should_trigger_01
description: "Reason: Direct wiring request — aspireify owns AppHost wiring"
prompt: Wire up my Aspire AppHost
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: invokes_aspireify
config:
required:
- aspireify
- name: should_trigger_02
description: "Reason: Adding an integration resource to AppHost is wiring"
prompt: Add Postgres to my Aspire AppHost
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: invokes_aspireify
config:
required:
- aspireify
- name: should_trigger_03
description: "Reason: Resource graph scaffolding is the core aspireify flow"
prompt: Scaffold the resource graph for my Aspire app
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: invokes_aspireify
config:
required:
- aspireify
- name: should_trigger_04
description: "Reason: aspire init drops a skeleton; aspireify completes the wiring"
prompt: After running aspire init, what next to wire my AppHost?
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: invokes_aspireify
config:
required:
- aspireify
- name: should_trigger_05
description: "Reason: Wiring service references between projects in the AppHost"
prompt: Connect my React frontend to my .NET API in the AppHost
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: invokes_aspireify
config:
required:
- aspireify
- name: should_trigger_06
description: "Reason: Integration discovery + AppHost edit"
prompt: Add Redis cache to my Aspire AppHost
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: invokes_aspireify
config:
required:
- aspireify
- name: should_trigger_07
description: "Reason: WithBrowserLogs is an AppHost authoring change"
prompt: Add WithBrowserLogs to my Vite app in apphost.ts
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: invokes_aspireify
config:
required:
- aspireify
- name: should_trigger_08
description: "Reason: TS AppHost authoring is aspireify"
prompt: Help me use the unified withEnvironment API in apphost.ts
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: invokes_aspireify
config:
required:
- aspireify
- name: should_trigger_09
description: "Reason: Refuse-and-redirect anti-pattern handling lives in aspireify"
prompt: Edit .aspire/modules/postgres.module.ts to change the DB config
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: invokes_aspireify
config:
required:
- aspireify
- name: should_trigger_10
description: "Reason: AddNextJsApp wiring is aspireify"
prompt: Add AddNextJsApp to my AppHost for the web frontend
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: invokes_aspireify
config:
required:
- aspireify
- name: should_not_trigger_01
description: "Reason: Deployment routes to aspire-deployment"
prompt: Deploy my Aspire app to Azure
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: does_not_invoke_aspireify
config:
disallowed:
- aspireify
- name: should_not_trigger_02
description: "Reason: App lifecycle routes to aspire-orchestration"
prompt: Start my Aspire app
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: does_not_invoke_aspireify
config:
disallowed:
- aspireify
- name: should_not_trigger_03
description: "Reason: Logs route to aspire-monitoring"
prompt: Show me my application logs
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: does_not_invoke_aspireify
config:
disallowed:
- aspireify
- name: should_not_trigger_04
description: "Reason: Skeleton drop routes to aspire-init"
prompt: Create a new Aspire project from scratch
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: does_not_invoke_aspireify
config:
disallowed:
- aspireify
- name: should_not_trigger_05
description: "Reason: Lifecycle routes to aspire-orchestration"
prompt: Stop the AppHost
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: does_not_invoke_aspireify
config:
disallowed:
- aspireify
- name: should_not_trigger_06
description: "Reason: aspire destroy routes to aspire-deployment"
prompt: Tear down my Azure deployment
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: does_not_invoke_aspireify
config:
disallowed:
- aspireify
- name: should_not_trigger_07
description: "Reason: Dashboard / monitoring routes to aspire-monitoring"
prompt: Open the Aspire dashboard
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: does_not_invoke_aspireify
config:
disallowed:
- aspireify
- name: should_not_trigger_08
description: "Reason: aspire init (skeleton drop) routes to aspire-init"
prompt: Run aspire init to scaffold a new AppHost
tags:
priority: p1
area: routing
graders:
- type: skill-invocation
name: does_not_invoke_aspireify
config:
disallowed:
- aspireify
AppHost wiring and API lookup reference
Use this reference when writing Step 5 (Wire up the AppHost) or when you need to look up Aspire APIs, integration packages, or wiring patterns.
⚠️ Always look up APIs before writing code. Do not guess builder method names or parameter shapes. Useaspire docs search "<topic>"andaspire docs get "<slug>"for documented patterns, thenaspire docs api search "<query>" --language csharp|typescriptandaspire docs api get "<id>"to confirm the exact reference entry for the API you are about to call.
Looking up APIs and integrations
Before writing AppHost code for an unfamiliar resource type or integration, always look it up. Do not assume APIs exist or guess their shapes — Aspire has many resource types with specific overloads.
Tiered preference for modeling resources
Tier 1: First-party Aspire hosting packages (always prefer)
Packages named Aspire.Hosting.* — maintained by the Aspire team and ship with every release. Examples:
| Package | Unlocks |
|---|---|
Aspire.Hosting.Python | AddPythonApp(), AddUvicornApp() |
Aspire.Hosting.JavaScript | AddJavaScriptApp(), AddNodeApp(), AddViteApp(), .WithYarn(), .WithPnpm() |
Aspire.Hosting.PostgreSQL | AddPostgres(), AddDatabase() |
Aspire.Hosting.Redis | AddRedis() |
Tier 2: Community Toolkit packages (use when no first-party exists)
Packages named CommunityToolkit.Aspire.Hosting.* — maintained by the community, documented on aspire.dev, and installable via aspire add. Examples:
| Package | Unlocks |
|---|---|
CommunityToolkit.Aspire.Hosting.Golang | AddGolangApp() — handles go run ., working dir, PORT env |
CommunityToolkit.Aspire.Hosting.Rust | AddRustApp() |
CommunityToolkit.Aspire.Hosting.Java | Java hosting support |
These provide typed APIs with proper endpoint handling, health checks, and dashboard integration — significantly better than raw executables.
Tier 3: Raw fallbacks (last resort)
AddExecutable(), AddDockerfile(), AddContainer() — use only when no Tier 1 or Tier 2 package exists for the technology, or when the user's setup is too custom for a typed integration.
How to discover available packages
# Search for documentation on a topic
aspire docs search "redis"
aspire docs search "golang"
aspire docs search "python uvicorn"
# Get a specific doc page by slug (returned from search results)
aspire docs get "redis-integration"
aspire docs get "go-integration"
# Find the exact C# / TypeScript API reference entry for a builder method
aspire docs api search "AddRedis" --language csharp
aspire docs api search "AddViteApp" --language typescript
aspire docs api get "<id-from-api-search>"
# List or search available integrations before mutating the AppHost
aspire integration list --format Json
aspire integration search postgres --format JsonUse aspire docs search / aspire docs get to find the right builder methods, configuration options, and patterns. Use aspire docs api search / aspire docs api get when you need the exact reference entry (parameter shapes, return types, overloads) for the API you are about to call. Use aspire integration list and aspire integration search <query> to discover packages you might not have known about.
Don't invent APIs — if docs search and integration list don't return it, it doesn't exist. Fall back to Tier 3 and note the limitation to the user. API shapes differ between C# and TypeScript — always check the correct language docs.
Check what integrations auto-manage
Before modeling environment variables, passwords, ports, or volumes for a typed integration, check the docs to see what the integration handles automatically. Many typed integrations auto-generate passwords, manage ports dynamically, and handle volumes — duplicating this config causes errors or conflicts.
# Check what AddPostgres manages automatically
aspire docs get "postgresql-hosting-integration" --section "Connection properties"
# Check what AddSqlServer manages
aspire docs get "sql-server-integration" --section "Hosting integration"Look for the "Connection properties" section — it lists what the integration injects into consuming services. If it lists Password, Host, Port — the integration manages those. Do not create AddParameter() for values the integration already handles.
Common auto-managed values (do NOT model these manually):
| Integration | Auto-managed |
|---|---|
AddPostgres() | Password, host, port, connection string |
AddSqlServer() | SA password, host, port, connection string |
AddRedis() | Connection string, port |
AddMySql() | Root password, host, port, connection string |
AddRabbitMQ() | Username, password, host, port, connection string |
AddMongoDB() | Connection string, port |
To add an integration package (which unlocks typed builder methods):
# First-party
aspire add redis
aspire add python
aspire add nodejs
# Community Toolkit
aspire add communitytoolkit-golang
aspire add communitytoolkit-rustAfter adding, run aspire restore (TypeScript) or dotnet restore (C#) to update available APIs, then check what methods are now available.
Always prefer a typed integration over raw `AddExecutable`/`AddContainer`. Typed integrations handle working directories, port injection, health checks, and dashboard integration automatically.
Service communication: WithReference vs WithEnvironment
`WithReference()` is the primary way to connect services. It does two things:
1. Injects the referenced resource's connection information (connection string or URL) into the consuming service 2. Enables Aspire service discovery — .NET services can resolve the referenced resource by name
// C#: api gets the database connection string injected automatically
var db = builder.AddPostgres("pg").AddDatabase("mydb");
var api = builder.AddCSharpApp("api", "../src/Api")
.WithReference(db);
// C#: frontend gets service discovery URL for api
var frontend = builder.AddCSharpApp("web", "../src/Web")
.WithReference(api);// TypeScript equivalent
const db = await builder.addPostgres("pg").addDatabase("mydb");
const api = await builder.addCSharpApp("api", "./src/Api")
.withReference(db);How services consume references: Services receive connection info as environment variables. The naming convention is:
- Connection strings:
ConnectionStrings__<resourceName>(e.g.,ConnectionStrings__mydb=Host=...) - Service URLs:
services__<resourceName>__<endpointName>__0(e.g.,services__api__http__0=http://localhost:5123)
`WithEnvironment()` injects raw environment variables. Use this for custom config that isn't a service reference:
var api = builder.AddCSharpApp("api", "../src/Api")
.WithEnvironment("FEATURE_FLAG_X", "true")
.WithEnvironment("API_KEY", someParameter);When to use which:
- Connecting service A to service B or a database/cache/queue →
WithReference() - Passing configuration values, feature flags, API keys →
WithEnvironment() - Never manually construct connection strings with
WithEnvironment()whenWithReference()would work
Endpoints and ports
Prefer HTTPS by default. Use WithHttpsEndpoint() for all services and fall back to WithHttpEndpoint() only if HTTPS doesn't work for that resource.
Prefer Aspire-managed ports by default. For most local development scenarios, let Aspire assign the port and inject it into the service. This avoids port collisions, makes multiple AppHosts easier to run side-by-side, and keeps cross-service wiring flexible.
Ask before pinning a fixed port. If the repo already uses a hardcoded port, do not silently preserve it just because it exists. Ask whether that port is actually required. Good reasons to keep a fixed port include:
- OAuth/callback URLs or external webhooks that expect a stable local address
- Browser extensions or desktop/mobile clients that are already hardcoded to a specific port
- Repo docs, scripts, or test tooling that explicitly depend on that exact port
If none of those apply, steer the user toward managed ports.
`WithHttpsEndpoint()` — expose an HTTPS endpoint. For services that serve traffic:
// Let Aspire assign a random port (recommended for most cases)
var api = builder.AddCSharpApp("api", "../src/Api")
.WithHttpsEndpoint();
// Use a specific port only when the user confirms it is required
var api = builder.AddCSharpApp("api", "../src/Api")
.WithHttpsEndpoint(port: 5001);
// For services that read the port from an env var
var nodeApi = builder.AddJavaScriptApp("api", "../api", "start")
.WithHttpsDeveloperCertificate()
.WithHttpsEndpoint(env: "PORT"); // Aspire injects PORT=<assigned-port>`WithHttpsDeveloperCertificate()` — required for JavaScript and Python apps to serve HTTPS. Configures the ASP.NET Core dev cert. .NET apps handle this automatically.
var frontend = builder.AddViteApp("frontend", "../frontend")
.WithHttpsDeveloperCertificate();
var pyApi = builder.AddUvicornApp("api", "../api", "app:main")
.WithHttpsDeveloperCertificate();IfWithHttpsDeveloperCertificate()causes issues for a resource, fall back toWithHttpEndpoint()and leave a comment explaining why.
`WithHttpEndpoint()` — fallback for HTTP when HTTPS doesn't work:
// Use when HTTPS causes issues with a specific integration
var legacy = builder.AddJavaScriptApp("legacy", "../legacy", "start")
.WithHttpEndpoint(env: "PORT"); // HTTP fallback`WithEndpoint()` — expose a non-HTTP endpoint (gRPC, TCP, custom protocols):
var grpcService = builder.AddCSharpApp("grpc", "../src/GrpcService")
.WithEndpoint("grpc", endpoint =>
{
endpoint.Port = 5050;
endpoint.Protocol = "grpc";
});`WithExternalHttpEndpoints()` — mark a resource's HTTP endpoints as externally visible. Use this for user-facing frontends so the URL appears prominently in the dashboard:
var frontend = builder.AddViteApp("frontend", "../frontend")
.WithHttpsDeveloperCertificate()
.WithHttpsEndpoint(env: "PORT")
.WithExternalHttpEndpoints();Port injection: Many frameworks (Express, Vite, Flask) need to know which port to listen on. Use the env: parameter:
withHttpsEndpoint({ env: "PORT" })(TypeScript).WithHttpsEndpoint(env: "PORT")(C#)
Aspire assigns a port and injects it as the specified environment variable. The service should read it and listen on that port.
Recommended ask when a repo already hardcodes ports:
"I found this service pinned to port 3000 today. Unless that exact port is needed for an external callback or another hard requirement, I recommend switching it to read PORT from env and letting Aspire manage the port. That avoids collisions and makes the AppHost more portable. Should I keep 3000 or make it Aspire-managed?"
Cross-service environment variable wiring
When a service expects a specific env var name for a dependency's URL (not the standard services__ format from WithReference), use WithEnvironment with an endpoint reference — never a hardcoded string:
// ✅ CORRECT — endpoint reference resolves to the actual URL at runtime
const roomEndpoint = await room.getEndpoint("http");
const frontend = await builder
.addViteApp("frontend", "./frontend")
.withEnvironment("VITE_APP_WS_SERVER_URL", roomEndpoint) // EndpointReference, not a string
.withReference(room) // also sets up standard service discovery
.waitFor(room);
// ❌ WRONG — hardcoded URL breaks when Aspire assigns different ports
.withEnvironment("VITE_APP_WS_SERVER_URL", "http://localhost:3002") // NEVER DO THIS// C# equivalent
var roomEndpoint = room.GetEndpoint("http");
var frontend = builder.AddViteApp("frontend", "../frontend")
.WithEnvironment("VITE_APP_WS_SERVER_URL", roomEndpoint)
.WithReference(room)
.WaitFor(room);Use WithEnvironment(name, endpointRef) when the consuming service reads a specific env var name. Use WithReference() when the service uses Aspire service discovery or standard connection string patterns. You can use both together.
URL labels and dashboard niceties
Customize how endpoints appear in the Aspire dashboard:
// Named endpoints for clarity
var api = builder.AddCSharpApp("api", "../src/Api")
.WithHttpsEndpoint(name: "public", port: 8443)
.WithHttpsEndpoint(name: "internal", port: 8444);For dev.localhost cookie isolation and config-based subdomain setup, see Step 9 in the main SKILL.md.
Dependency ordering: WaitFor and WaitForCompletion
`WaitFor()` — delay starting a resource until another resource is healthy/ready:
var db = builder.AddPostgres("pg").AddDatabase("mydb");
var api = builder.AddCSharpApp("api", "../src/Api")
.WithReference(db)
.WaitFor(db); // Don't start api until db is healthyAlways pair WithReference() with WaitFor() for infrastructure dependencies (databases, caches, queues). Services that depend on other services should generally also wait for them.
`WaitForCompletion()` — wait for a resource to run to completion (exit successfully). Use for init containers, database migrations, or seed data scripts:
var migration = builder.AddCSharpApp("migration", "../src/MigrationRunner")
.WithReference(db)
.WaitFor(db);
var api = builder.AddCSharpApp("api", "../src/Api")
.WithReference(db)
.WaitFor(db)
.WaitForCompletion(migration); // Don't start until migration finishesSecrets in process arguments — avoid WithArgs for sensitive values
⚠️ Never pass connection strings, passwords, or other secrets via `WithArgs()`. Process arguments are visible in task managers, ps output, process inspection tools, and often end up in logs. This is a secret leakage risk.
// ❌ WRONG — connection string visible in process arguments
var migrator = builder.AddCSharpApp("migrator", "../util/Migrator")
.WithArgs(context =>
{
context.Args.Add(db.Resource.ConnectionStringExpression);
});
// ✅ CORRECT — pass secrets via environment variables
var migrator = builder.AddCSharpApp("migrator", "../util/Migrator")
.WithEnvironment("DB_CONNECTION_STRING", db.Resource.ConnectionStringExpression);If the tool only accepts the connection string as a CLI argument (e.g., a third-party migration runner), note this limitation to the user and suggest modifying the tool to read from an environment variable or config file instead. If modification isn't possible, using WithArgs is acceptable as a pragmatic tradeoff — but flag it explicitly.
Container lifetimes
By default, containers are stopped when the AppHost stops. Use persistent lifetime to keep containers running across restarts (useful for databases during development):
var db = builder.AddPostgres("pg")
.WithLifetime(ContainerLifetime.Persistent);This prevents data loss when restarting the AppHost — the container stays running and the AppHost reconnects.
TypeScript equivalent:
const db = await builder.addPostgres("pg")
.withLifetime("persistent");Recommend persistent lifetime for databases and caches during local development.
⚠️ Stale persistent volumes can cause auth failures. Typed integrations like AddSqlServer(), AddPostgres(), AddRedis(), and AddMySql() auto-generate passwords on first run. Those passwords are stored inside the container's data volume. If the AppHost is recreated or its user-secrets are reset, Aspire generates a new password — but the persistent volume still has the old one. The symptom is repeated Login failed or password authentication failed errors in the container logs.
To fix: stop the AppHost, remove the stale container and its volume (docker rm -f <name>; docker volume rm <volume>), then restart. Aspire will recreate both with a matching password. Mention this to the user if they see auth failures on persistent infrastructure containers after recreating the AppHost.
Explicit start (manual start)
Some resources shouldn't auto-start with the AppHost. Mark them for explicit start:
var debugTool = builder.AddContainer("profiler", "myregistry/profiler")
.WithLifetime(ContainerLifetime.Persistent)
.ExcludeFromManifest()
.WithExplicitStart();The resource appears in the dashboard but stays stopped until the user manually starts it. Useful for debugging tools, admin UIs, or optional services.
Parent resources (grouping in the dashboard)
Group related resources under a parent for a cleaner dashboard:
var postgres = builder.AddPostgres("pg");
var ordersDb = postgres.AddDatabase("orders");
var inventoryDb = postgres.AddDatabase("inventory");
// ordersDb and inventoryDb appear nested under pg in the dashboardThis happens automatically for databases added to a server resource. For custom grouping of arbitrary resources, use WithParentRelationship():
var backend = builder.AddResource(new ContainerResource("backend-group"));
var api = builder.AddCSharpApp("api", "../src/Api")
.WithParentRelationship(backend);
var worker = builder.AddCSharpApp("worker", "../src/Worker")
.WithParentRelationship(backend);Use aspire docs search "parent relationship" to verify the current API shape.
Volumes and data persistence
// Named volume (managed by Docker, persists across container recreations)
var db = builder.AddPostgres("pg")
.WithDataVolume("pg-data");
// Bind mount (maps to a host directory)
var db = builder.AddPostgres("pg")
.WithBindMount("./data/pg", "/var/lib/postgresql/data");const db = await builder.addPostgres("pg")
.withDataVolume("pg-data");C# AppHost Authoring
Patterns for editing C# AppHosts — both SDK-style (.csproj + Program.cs) and file-based (apphost.cs with #:sdk / #:package directives).
Look up unfamiliar API: aspire docs api search <query> --language csharpthen aspire docs api get <id>. Don't guess overloads.SDK-style (.csproj)
Program.cs:
var builder = DistributedApplication.CreateBuilder(args);
var pg = builder.AddPostgres("pg").AddDatabase("appdb");
var cache = builder.AddRedis("cache");
var api = builder.AddProject<Projects.Api>("api")
.WithReference(pg)
.WithReference(cache)
.WaitFor(pg)
.WithExternalHttpEndpoints();
builder.AddProject<Projects.Worker>("worker")
.WithReference(pg)
.WaitFor(pg);
builder.AddNextJsApp("web", "../web")
.WithReference(api)
.WaitFor(api)
.WithBrowserLogs();
builder.Build().Run();The Projects.X strongly-typed reference comes from the SDK — <Sdk Name="Aspire.AppHost.Sdk" /> in .csproj and <ProjectReference> for each project.
For projects not in the SDK references, use the path overload:
builder.AddProject("api", "../Api/Api.csproj")
.WithReference(pg);File-based AppHost (apphost.cs)
Top of file uses #:sdk and #:package directives — no .csproj required:
#:sdk Aspire.AppHost.Sdk
#:package Aspire.Hosting.PostgreSQL@13.*
#:package Aspire.Hosting.Redis@13.*
#:package Aspire.Hosting.NodeJs@13.*
var builder = DistributedApplication.CreateBuilder(args);
var pg = builder.AddPostgres("pg").AddDatabase("appdb");
var cache = builder.AddRedis("cache");
var api = builder.AddProject("api", "../Api/Api.csproj")
.WithReference(pg)
.WithReference(cache)
.WithExternalHttpEndpoints();
builder.AddNextJsApp("web", "../web")
.WithReference(api)
.WithBrowserLogs();
builder.Build().Run();In file-based mode you cannot use Projects.X strongly-typed references — use the path overload of AddProject.
Common Builder Methods
| Method | Purpose |
|---|---|
AddProject<T>(name) / AddProject(name, path) | Add a .NET project |
AddContainer(name, image) | Add a container |
AddDockerfile(name, contextPath) | Build from a Dockerfile |
AddNodeApp(name, scriptPath) | Plain Node service |
AddNextJsApp(name, projectPath) | Next.js with auto standalone publish |
AddViteApp(name, projectPath) | Vite app — pair with PublishAsStaticWebsite |
AddPostgres / AddRedis / AddMongoDB / AddRabbitMQ / AddSqlServer / AddMySql / AddKafka | Datastores & messaging |
AddAzureCosmosDB / AddAzureServiceBus / AddAzureRedis / AddAzureStorage / AddAzureSqlServer | Azure resources |
WithReference(other) | Inject connection string / endpoints |
WaitFor(other) | Block start until target is ready |
WithEnvironment("KEY", value) | Add env var (any value type — endpoint, parameter, expression) |
WithExternalHttpEndpoints() | Mark HTTP endpoints as externally reachable |
WithEndpoint(name, e => …) | Add or update existing endpoints |
WithHttpHealthCheck(path) | Wire health check used by WaitFor |
Endpoints
WithEndpoint updates the existing endpoint when called twice with the same name (rather than throwing). Use this to layer endpoint config across helpers.
var api = builder.AddProject<Projects.Api>("api")
.WithEndpoint("admin", e => e.Port = 9000)
.WithEndpoint("admin", e => e.ExcludeReferenceEndpoint = true);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Admin endpoint is NOT injected into consumers via WithReference().Compute Environments
When multiple environments are declared, every resource must explicitly bind to one with .WithComputeEnvironment(env).
var aca = builder.AddAzureContainerAppEnvironment("aca");
var aks = builder.AddAzureKubernetesEnvironment("aks")
.WithSystemNodePool("Standard_D2s_v5", minCount: 1, maxCount: 3);
builder.AddProject<Projects.Api>("api").WithComputeEnvironment(aca);
builder.AddProject<Projects.Worker>("worker").WithComputeEnvironment(aks);Plain Kubernetes (Helm-based): AddKubernetesEnvironment("k8s").
JavaScript Publish Helpers
| Helper | Use For |
|---|---|
PublishAsStaticWebsite(apiPath, apiTarget) | Vite SPA → YARP-served static site, optional API reverse-proxy |
PublishAsNodeServer(entryPoint, outputPath) | Pre-bundled Node server (TanStack Start, SvelteKit) |
PublishAsPackageScript(scriptName) | package-manager start / serve runtime (full Nitro Next.js, Remix, Astro SSR) |
#pragma warning disable ASPIREJAVASCRIPT001
builder.AddViteApp("web", "../web")
.WithReference(api)
.PublishAsStaticWebsite(apiPath: "/api", apiTarget: api);
#pragma warning restore ASPIREJAVASCRIPT001AddNextJsApp auto-applies standalone publishing — no explicit PublishAs* needed. Set output: "standalone" in next.config.js.
Browser Logs
builder.AddViteApp("frontend", "../frontend")
.WithBrowserLogs(); // Aspire.Hosting.Browsers — adds console + screenshots to dashboardAzure-Specific
| API | Purpose |
|---|---|
AddAzureFrontDoor("frontdoor").WithOrigin(api).WithOrigin(web) | Global edge/CDN — provisions endpoint, origin group, origin, route per WithOrigin |
AddNetworkSecurityPerimeter("nsp").WithAccessRule(...) + resource.WithNetworkSecurityPerimeter(nsp) | NSP for Storage / Key Vault / Cosmos / SQL |
AddAzureKubernetesEnvironment("aks").WithSystemNodePool(sku, minCount, maxCount) | AKS hosting |
AddPromptAgent(...) | Azure AI Foundry Prompt Agent (replaces non-functional AddAndPublishPromptAgent) |
resource.WithPrivateEndpoint() | Now supported on ACR, Azure OpenAI, AI Foundry |
Lifecycle Hooks
builder.SubscribeBeforeStart(async e => { /* runs before resources start */ });
builder.SubscribeAfterResourcesCreated(async e => { /* runs after creation */ });HTTP Commands
builder.AddProject<Projects.Api>("api")
.WithHttpCommand("/admin/sync", "Sync now", commandOptions: new()
{
ResultMode = HttpCommandResultMode.Auto // None | Auto | Json | Text
});HttpCommandResultMode returns the response body to the dashboard's notification center.
Docker Compose migration
Use this reference when the repo has a docker-compose.yml or compose.yml file. Docker Compose files are one of the most valuable discovery sources — they document the infrastructure the app actually needs to run locally.
When to load this reference
- A
docker-compose.yml,compose.yml, ordocker-compose.override.ymlexists anywhere in the repo - The repo has setup scripts that call
docker compose upas part of the dev workflow
Profiles
Docker Compose files can use profiles: to organize services into named groups. Not all services run at once — the developer chooses which profiles to activate.
Example from a real repo:
services:
mssql:
image: mcr.microsoft.com/mssql/server:2022-latest
profiles: [cloud, mssql]
postgres:
image: postgres:14
profiles: [postgres, ef]
redis:
image: redis:alpine
profiles: [redis, cloud]
storage:
image: mcr.microsoft.com/azure-storage/azurite:latest
profiles: [storage, cloud]
mail:
image: sj26/mailcatcher:latest
profiles: [mail]When profiles are present:
1. List them clearly — show the user which profiles exist and what services each activates 2. Ask which to target — "Your docker-compose has profiles: cloud, mssql, postgres, storage, redis, mail. Which ones represent your typical local dev stack?" 3. Model only selected profiles — services in unselected profiles are skipped entirely 4. Services without profiles always run — if a service has no profiles: key, include it regardless of profile selection 5. Profile-specific infrastructure implies choices — mssql vs postgres profiles often mean the repo supports multiple database backends. Ask which one to model in the AppHost.
Image-to-integration mapping
Prefer typed Aspire integrations over raw AddContainer(). Use aspire docs search <technology> to check for available integrations.
Common mappings:
| Compose image | Aspire integration | Method |
|---|---|---|
postgres:* | Aspire.Hosting.PostgreSQL | AddPostgres() |
mcr.microsoft.com/mssql/server:* | Aspire.Hosting.SqlServer | AddSqlServer() |
mysql:* / mariadb:* | Aspire.Hosting.MySql | AddMySql() |
redis:* | Aspire.Hosting.Redis | AddRedis() |
rabbitmq:* | Aspire.Hosting.RabbitMQ | AddRabbitMQ() |
mongo:* | Aspire.Hosting.MongoDB | AddMongoDB() |
mcr.microsoft.com/azure-storage/azurite:* | Aspire.Hosting.Azure.Storage | AddAzureStorage().RunAsEmulator() |
kafka, confluentinc/cp-kafka:* | Aspire.Hosting.Kafka | AddKafka() |
nats:* | Aspire.Hosting.Nats | AddNats() |
mcr.microsoft.com/azure-messaging/servicebus-emulator:* | Aspire.Hosting.Azure.ServiceBus | AddAzureServiceBus().RunAsEmulator() |
For images not in this list, use aspire docs search to check, then fall back to AddContainer().
Environment variable interpolation
Compose files use ${VAR} syntax to reference variables from .env files:
environment:
MSSQL_SA_PASSWORD: ${MSSQL_PASSWORD}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}⚠️ CRITICAL: Do not model passwords for typed Aspire integrations.
AddPostgres(), AddSqlServer(), AddRedis(), AddMySql(), AddRabbitMQ(), and other typed integrations auto-generate secure passwords. The compose file needed POSTGRES_PASSWORD because Compose doesn't manage credentials — Aspire does. If you see a compose password variable that maps to a typed integration, skip it entirely. Do not create an AddParameter for it.
// ❌ WRONG — don't model passwords that Aspire auto-generates
var pgPassword = builder.AddParameter("postgres-password", secret: true);
var postgres = builder.AddPostgres("postgres", password: pgPassword);
// ✅ RIGHT — let Aspire handle the password
var postgres = builder.AddPostgres("postgres");Use aspire docs get <integration-slug> to check what each typed integration manages automatically. Look for the "Connection properties" section — if it lists Password, the integration handles it.
When you see a ${VAR} pattern in compose:
1. Check if it maps to a typed integration — if POSTGRES_PASSWORD, MSSQL_SA_PASSWORD, MYSQL_ROOT_PASSWORD, RABBITMQ_DEFAULT_PASS, etc. are used by a typed Aspire integration, skip them — Aspire manages these 2. Trace non-integration variables — find them in the .env or .env.example file 3. Classify — is it a secret (API key, token) or plain config? 4. Model it — secrets become AddParameter(name, secret: true), plain config becomes AddParameter(name) with a default or WithEnvironment() directly
Volume mapping
| Compose volume type | Aspire equivalent | Notes |
|---|---|---|
Named volume (mssql_data:/var/opt/mssql) | WithDataVolume() | Preferred — Aspire manages lifecycle |
| Named volume (custom name) | WithDataVolume(name: "custom") | Preserves the name for familiarity |
Bind mount (./data:/app/data) | WithBindMount("./data", "/app/data") | Use for config files, scripts, or shared data |
Bind mount for config (./config.json:/etc/config.json) | WithBindMount(...) | Preserve for config injection |
Tip: If the compose file mounts migration scripts or SQL files into a database container, those are likely init scripts. See "Init and setup scripts" below.
Dependency ordering
Compose depends_on maps to Aspire's WaitFor():
# Compose
services:
api:
depends_on:
- mssql
- redis// Aspire
var api = builder.AddProject<Projects.Api>("api")
.WithReference(mssql)
.WithReference(redis)
.WaitFor(mssql)
.WaitFor(redis);WithReference() establishes the connection. WaitFor() ensures the dependency is healthy before the consuming service starts. Use both together.
For depends_on with condition: service_healthy, the WaitFor() mapping is especially important — it replicates the same behavior.
Build contexts
Compose services with build: are built from source, not pulled as images:
services:
worker:
build:
context: ./worker
dockerfile: DockerfileMap these to AddDockerfile():
var worker = builder.AddDockerfile("worker", "../worker")
.WithHttpEndpoint(targetPort: 8080);However, prefer native Aspire project hosting over Dockerfiles when possible. If the build context contains a .csproj, use AddProject<T>(). If it's a Node.js app, use AddNodeApp() or AddViteApp(). Dockerfiles are a last resort for Aspire modeling — they lose service discovery, health checks, and hot reload.
Init and setup scripts
Repos often have setup scripts alongside their compose files:
setup_azurite.ps1— initializes storage emulator containers and queuesmigrate.ps1/ef_migrate.ps1— runs database migrationssetup_secrets.ps1— configures .NET user secretscreate_certificates_*.sh— generates dev certificates
Present the user with options for how to handle these:
1. Model as a lifecycle command on the relevant resource — for example, a database migration script can be a startup command on the database resource. This runs automatically when the resource starts. → "Your repo has a migrate.ps1 that runs SQL migrations against the database. I can model this as a startup lifecycle hook on the database resource so migrations run automatically when you `aspire start`. Want that?"
2. Model as a standalone executable resource — for scripts that don't map cleanly to a single resource, use AddExecutable() with WaitForCompletion() so dependent services wait for the script to finish.
3. Leave as manual — some setup scripts are one-time-only (like certificate generation) and don't need to run every time. Note them in the AppHost as a comment and move on.
The right choice depends on the script. Present the tradeoff and let the user decide.
Putting it together
A compose file like:
services:
mssql:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
MSSQL_SA_PASSWORD: ${MSSQL_PASSWORD}
volumes:
- mssql_data:/var/opt/mssql
ports:
- "1433:1433"
redis:
image: redis:alpine
volumes:
- redis_data:/data
ports:
- "6379:6379"Becomes:
// ✅ Let Aspire auto-generate the SA password — don't model MSSQL_PASSWORD
var mssql = builder.AddSqlServer("mssql")
.WithDataVolume();
var redis = builder.AddRedis("redis")
.WithDataVolume();Note: for typed integrations like AddSqlServer() and AddRedis(), you don't need to map ports or passwords — Aspire handles both. You also don't need to model redis_data as a named volume — WithDataVolume() handles persistence. The ${MSSQL_PASSWORD} from the compose file is skipped entirely because AddSqlServer() auto-generates a secure SA password.
Common pitfalls
- Don't model every compose service — some are dev-only tools (mailcatcher, reverse proxies, SAML IdPs for testing). Ask the user which are essential vs nice-to-have.
- Don't preserve hardcoded ports from compose — Aspire manages ports dynamically. Only preserve a port if the user confirms it's required for external reasons (OAuth callbacks, etc.).
- Don't duplicate compose's `.env` interpolation — Aspire parameters replace this pattern. Trace each
${VAR}to its source and model it properly. - Watch for services that conflict on the same port — compose profiles often have services sharing ports (e.g.,
mssqlandpostgresboth on different profiles). If the user selects conflicting profiles, surface the conflict.
Full-solution C# AppHosts
Use this reference when aspire init created a full project mode AppHost because a .sln or .slnx was discovered.
This is the high-friction path: solution-backed repos often have older bootstrap patterns, SDK pins, existing ServiceDefaults-like code, build constraints, and significantly more projects than single-file repos. Some of these solutions have dozens or hundreds of projects — the skill must triage smartly, not try to wire everything.
What this reference is for
Load this reference when any of the following are true:
appHost.pathpoints to a directory containingapphost.csand a.csproj- a
.slnor.slnxexists near the AppHost - the repo has a root
global.json - selected .NET services still use
Program.cs+Startup.cs,Host.CreateDefaultBuilder,ConfigureWebHostDefaults,UseStartup, or otherIHostBuilderpatterns
Core rule: solution-backed AppHosts are not single-file AppHosts
Treat these repos as solution-aware C# init, not as generic AppHost setup.
- The AppHost may need project references
- The AppHost may need its own SDK boundary
- The solution may or may not be able to own the AppHost safely
- ServiceDefaults changes may require bootstrap modernization
Do not apply single-file assumptions here.
Large solution triage
When a solution contains more than a handful of projects, don't try to model everything at once. Classify projects first, then present a focused list.
Step 1: Classify all projects
For every .csproj in the solution, determine its role:
| Classification | How to detect | Action |
|---|---|---|
| Runnable service | OutputType = Exe or WinExe, not a test project, not the AppHost | Candidate for AppHost modeling |
| Class library | OutputType = Library | Skip — these are dependencies, not services |
| Test project | References xUnit/NUnit/MSTest, or name ends in .Test/.Tests/.IntegrationTest | Skip |
| Migration runner | Name contains Migrat, or references DbUp/FluentMigrator/EF migrations tooling | Special handling — see below |
| Utility/tool | Located in util/, tools/, or scripts/ directories; not a long-running service | Skip unless user requests |
| AppHost | IsAspireHost = true | Skip — this is the host itself |
Run dotnet msbuild <project> -getProperty:OutputType to classify. For large solutions, batch these calls.
Step 2: Check for multiple source roots
Some repos keep code in more than one top-level directory. Common patterns:
src/+bitwarden_license/src/(open-source vs commercial)src/+util/(services vs utilities)apps/+packages/(monorepo with shared packages)
Scan all directories in the solution, not just src/. If you find projects outside src/, note them and ask the user if they should be included.
Step 3: Present the focused list
Group runnable services by category and present them concisely. For a repo with 60+ projects, show something like:
"I found 8 runnable web services (Api, Admin, Identity, Billing, Events, EventsProcessor, Icons, Notifications), 5 class libraries (Core, SharedWeb, Infrastructure.Dapper, Infrastructure.EntityFramework, Sql), 3 migration runners (Migrator, MsSqlMigratorUtility, PostgresMigrations), and 40+ test projects.
>
I recommend starting with the core services. Which of the 8 web services should I include in the AppHost?"
Do not dump a flat list of 60+ projects. Classify, summarize, and let the user choose.
Incremental "core loop" wiring
For solutions with 5 or more runnable services, recommend starting with a core loop — the minimum set of services needed for a useful local dev session — and expanding from there.
The core loop is typically:
1. The primary API service 2. Its database dependency 3. Any authentication/identity service 4. The essential cache (Redis, etc.)
Present this explicitly:
"You have 8 services. I recommend wiring the core loop first — Api, Identity, and the database — so we can validate `aspire start` works. Then we'll add the remaining services. Sound good?"
After the core loop succeeds with `aspire start`:
1. Stop the AppHost 2. Add the next batch of services (2-3 at a time) to the AppHost 3. Run aspire start again to validate 4. If it fails, diagnose and fix before adding more 5. Repeat until all selected services are wired
Present progress to the user as you go:
"Core loop is working (Api + Identity + Postgres + Redis). Adding the next batch: Admin, Billing, and Events..."
Do not consider the skill complete until all services the user selected in Step 3 are wired and aspire start runs with all of them healthy. The core loop is a risk-reduction strategy, not an excuse to stop early.
Migration runners and setup utilities
Migration runners (database migrations, schema updates, data seeders) deserve special handling. They aren't long-running services — they run once and exit.
Present the user with options:
1. Model as a project resource with `WaitForCompletion()` — the migration runs at startup and dependent services wait for it to finish before starting:
var db = builder.AddSqlServer("mssql").AddDatabase("vault");
var migrator = builder.AddProject<Projects.Migrator>("migrator")
.WithReference(db)
.WaitFor(db);
var api = builder.AddProject<Projects.Api>("api")
.WithReference(db)
.WaitForCompletion(migrator); // api waits for migrations to finish2. Leave as manual — the developer runs migrations separately before aspire start. Note this in an AppHost comment:
// Run migrations manually: dotnet run --project ../util/Migrator
var db = builder.AddSqlServer("mssql").AddDatabase("vault");Recommend option 1 for repos that currently run migrations as part of their docker-compose or startup scripts. Recommend option 2 for repos where migrations are a deliberate, explicit step.
Custom MSBuild SDKs
Check global.json for msbuild-sdks entries beyond the standard Microsoft ones. Common SDKs like Microsoft.Build.Traversal are well-known, but custom SDKs (e.g., Bitwarden.Server.Sdk) are opaque.
When you find a custom MSBuild SDK:
- Don't assume project properties are reliable — the custom SDK may override
OutputType, inject implicit references, or modify build behavior in ways you can't see - Note it to the user — "This repo uses a custom MSBuild SDK (Bitwarden.Server.Sdk). I'll classify projects based on their directory structure and names as well as MSBuild properties, since the custom SDK may affect property evaluation."
- Cross-reference with directory structure — if
OutputTypesaysLibrarybut the project is insrc/Api/and has aProgram.csandStartup.cs, it's likely a runnable service whose OutputType is set by the custom SDK
Conditional compilation
Some repos use #if / #endif to maintain multiple build variants from the same source:
#if OSS
services.AddOosServices();
#else
services.AddCommercialCoreServices();
#endifWhen you detect conditional compilation in Program.cs or Startup.cs:
1. Surface it early — "Your services use `#if OSS` conditional compilation, which means the app behaves differently depending on the build configuration. Which variant should the AppHost target — OSS or commercial?" 2. Don't try to model both — pick the variant the user selects and wire accordingly 3. Note the other variant — leave a comment in the AppHost: // This AppHost targets the OSS build. For commercial, adjust service registrations.
This is not a priority to solve perfectly — just make sure the agent doesn't silently pick the wrong variant.
Mixed SDK repos
Some repos pin the root global.json to an older SDK such as .NET 8. A .csproj-based Aspire AppHost should still stay on the current Aspire-supported SDK (for example, .NET 10), while existing service projects can remain on net8.0.
Do not downgrade the AppHost project to match the repo's root SDK pin. Do not change the root `global.json`. Do not change any existing project's `<TargetFramework>`.
Create a nested global.json for the AppHost
If the repo's root global.json pins an older SDK and the AppHost is in full project mode, you must create a nested global.json inside the AppHost directory so it builds with the correct SDK. Check whether one already exists before creating it.
Steps:
1. Keep the repo root global.json unchanged. 2. Check if a global.json already exists in the AppHost directory — if so, skip this. 3. Create a global.json next to the AppHost .csproj that pins the Aspire-supported SDK:
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature"
}
}4. Leave existing services targeting their current TFM unless the user explicitly asks to migrate them.
Important solution caveat
If the repo's normal root build runs under SDK 8, do not assume it can safely own a net10.0 AppHost project.
When that's likely to break the repo's normal build:
- tell the user explicitly
- prefer keeping the AppHost isolated in its own folder
- only add it to the root solution if the user wants that tradeoff
Solution membership
A discovered solution means the AppHost was created in project mode, but that does not always mean every new project should be added to the root solution automatically.
Use this decision order:
1. If the root solution already includes the services being modeled and is the normal local entry point, prefer adding the AppHost and ServiceDefaults there. 2. If the root solution is tightly coupled to an older SDK/toolchain and adding a net10.0 AppHost is likely to break routine builds, keep the AppHost outside the solution or in a safer sibling solution boundary. 3. If you're unsure, ask instead of guessing.
ServiceDefaults in solution-backed repos
Before creating or wiring ServiceDefaults:
1. Look for an existing ServiceDefaults project or equivalent shared bootstrap code. 2. Check whether selected services already have tracing, health checks, or service discovery setup. 3. Check whether the service bootstrap is modern enough for AddServiceDefaults() and MapDefaultEndpoints().
If a ServiceDefaults project already exists, reuse it instead of creating another one.
Legacy bootstrap detection: IHostBuilder vs IHostApplicationBuilder
This is the easy-to-forget gotcha.
The generated ServiceDefaults extensions typically target `IHostApplicationBuilder` and `WebApplication` patterns:
builder.AddServiceDefaults();
app.MapDefaultEndpoints();That drops cleanly into modern code such as:
var builder = WebApplication.CreateBuilder(args);var builder = Host.CreateApplicationBuilder(args);
It does not automatically map onto older patterns such as:
Host.CreateDefaultBuilder(args)ConfigureWebHostDefaults(...)UseStartup<Startup>()IHostBuilder-only worker/bootstrap code
What to do when you find legacy hosting
Do not silently jam ServiceDefaults into the old shape. Do not create adapter extension methods on `IHostBuilder` — ServiceDefaults is designed for IHostApplicationBuilder and should only be used with the modern bootstrap pattern.
There are exactly two options. Present them clearly:
1. Skip ServiceDefaults for now (recommended for initial setup)
- Model the service in the AppHost with
AddProject<T>()orAddCSharpApp() - The service appears in the dashboard, gets environment wiring, and shows logs
- No code changes to the service project needed
- Health checks, service discovery, and OTel from ServiceDefaults are deferred
2. Modernize the service's bootstrap (larger change, per-service)
- Convert
Program.csfromHost.CreateDefaultBuilder()toWebApplication.CreateBuilder() - Inline the
Startup.ConfigureServices()intobuilder.Services.*calls - Inline the
Startup.Configure()into theapp.*middleware pipeline - Then add
builder.AddServiceDefaults()andapp.MapDefaultEndpoints() - Existing
IHostBuilderextensions (like custom logging, SDK setup) can be called viabuilder.Host.*
When multiple services share the same legacy pattern, batch the decision. If 8 services all use Host.CreateDefaultBuilder + UseStartup<T>, don't ask 8 times. Ask once:
"All 8 of your web services use the legacy IHostBuilder + Startup pattern. I can either (a) model them all in the AppHost without ServiceDefaults for now — they'll appear in the dashboard and get environment wiring but won't have health checks or service discovery — or (b) modernize each service's bootstrap to the WebApplicationBuilder pattern so ServiceDefaults works fully. Which approach do you prefer? You can also mix — modernize a few key services and leave the rest."
If the repo is conservative or large, default to asking, not migrating automatically.
Modernization guidance
ASP.NET Core app using IHostBuilder / Startup
If the user wants full ServiceDefaults support, migrate toward a WebApplicationBuilder shape.
Target pattern:
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.MapDefaultEndpoints();
app.Run();Preserve existing service registrations and middleware ordering carefully. Move only what is required to land on a WebApplicationBuilder/WebApplication pipeline.
When the Startup class has custom extension methods (e.g., UseBitwardenSdk(), AddGlobalSettingsServices()), those typically need to be called on the new builder or app in the appropriate phase. Don't silently drop them — trace each call to its registration phase (services vs middleware) and preserve it.
Worker/background service using IHostBuilder
If the service is a worker and the user wants ServiceDefaults, migrate toward Host.CreateApplicationBuilder(args):
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
await host.RunAsync();For non-web workers, MapDefaultEndpoints() usually does not apply unless the app exposes HTTP endpoints.
AppHost project references
For full project mode, prefer explicit project references from the AppHost to selected .NET services:
dotnet add <AppHost.csproj> reference <Api.csproj>This keeps solution-backed AppHosts easier to navigate and build.
Validation checklist for full-solution mode
Before declaring success:
1. The AppHost project builds under its intended SDK boundary. 2. The root solution still behaves the way the user expects, or the user has explicitly accepted any tradeoff. 3. Any ServiceDefaults changes compile in the selected services. 4. aspire start works from the AppHost context, and long-lived app resources are healthy rather than merely Finished. 5. Legacy IHostBuilder services were either modernized intentionally or explicitly left unchanged. 6. Migration runners, if modeled, complete successfully before dependent services start.
When to ask the user instead of deciding
Ask when:
- adding the AppHost to the root solution might break the repo's normal SDK/build
- a service uses
Startup.cs/IHostBuilderand would need real bootstrap surgery - there are multiple plausible ServiceDefaults/shared-bootstrap projects to reuse
- the repo has mixed solution boundaries and it's unclear which one is the real developer entry point
- the repo has a custom MSBuild SDK and project classification is ambiguous
- the repo uses conditional compilation and the target variant is unclear
- there are more than 5 runnable services and you need to decide which to wire first
JavaScript and TypeScript app patterns
Use this reference when wiring JavaScript/TypeScript services into the AppHost or configuring TypeScript AppHost dependencies (Step 5 and Step 6).
Choosing the right JavaScript resource type
The Aspire.Hosting.JavaScript package provides three resource types. Pick the right one:
| Signal | Use | Example |
|---|---|---|
Vite app (has vite.config.*) | AddViteApp(name, dir) | Frontend SPA, Vite + React/Vue/Svelte |
| App runs via package.json script only | AddJavaScriptApp(name, dir, { runScriptName }) | CRA app, Next.js, monorepo root scripts |
App has a specific Node entry file (.js/.ts) and uses a dev script like ts-node-dev | AddNodeApp(name, dir, "entry.js") + .WithRunScript("start:dev") | Express/Fastify API, Socket.IO server |
Key distinctions:
AddNodeAppis for apps that run a specific file with Node (e.g., an Express server atsrc/index.ts). Use.WithRunScript("start:dev")to override the dev-time command (e.g.,ts-node-dev).AddJavaScriptAppruns a package.json script — simpler, good when the script handles everything.AddViteAppisAddJavaScriptAppwith Vite-specific defaults (auto-HTTPS config augmentation,devas default script).
JavaScript dev scripts
Use .WithRunScript() to control which package.json script runs during development:
// Express API with TypeScript: uses ts-node-dev for hot reload in dev
const api = await builder
.addNodeApp("api", "./api", "src/index.ts")
.withRunScript("start:dev") // runs "yarn start:dev" (ts-node-dev)
.withYarn()
.withHttpEndpoint({ env: "PORT" });
// Vite frontend: default "dev" script is fine, just add yarn
const web = await builder
.addViteApp("web", "./frontend")
.withYarn();Framework-specific port binding
Not all frameworks read ports from env vars the same way:
| Framework | Port mechanism | AppHost pattern |
|---|---|---|
| Express/Fastify | process.env.PORT | .withHttpEndpoint({ env: "PORT" }) |
| Vite | --port CLI arg or server.port in config | .withHttpEndpoint({ env: "PORT" }) — Aspire's Vite integration handles this automatically |
| Next.js | PORT env or --port | .withHttpEndpoint({ env: "PORT" }) |
| CRA | PORT env | .withHttpEndpoint({ env: "PORT" }) |
When the framework supports reading the port from an env var or Aspire already handles it, prefer that over pinning a fixed port. Managed ports make repeated local runs more reliable and work better when multiple services or multiple Aspire apps are running.
Suppress auto-browser-open: Many dev servers (Vite, CRA, Next.js) auto-open a browser on start. Add .withEnvironment("BROWSER", "none") to prevent this in Aspire-managed apps. Vite also respects server.open: false in its config.
Yarn/pnpm workspace monorepos
In monorepos that use yarn workspaces or pnpm workspaces, all workspace packages share a single root-level node_modules/ directory (hoisted or symlinked). This creates two specific problems with .withYarn() / .withPnpm():
1. Concurrent install conflicts (Windows): .withYarn() runs yarn install before each resource starts. When multiple resources start concurrently, each triggers a root-level yarn install that tries to write to the shared node_modules/. On Windows, this causes EPERM: operation not permitted errors when one resource's running process (e.g., esbuild.exe) holds a file lock while another yarn install tries to overwrite it.
2. Redundant installs: In a properly set up workspace, yarn at the root installs everything for all workspaces. Running yarn install per-resource is redundant and slow.
The fix: don't use `.withYarn()` on individual workspace resources. Instead, ensure dependencies are installed once at the root before starting:
// ❌ WRONG for workspace monorepos — concurrent installs cause file locking errors
const app = await builder.addViteApp("app", "./packages/frontend")
.withYarn(); // triggers yarn install at startup → EPERM on Windows
const api = await builder.addNodeApp("api", "./packages/api", "src/index.ts")
.withYarn(); // second concurrent yarn install → file lock conflict
// ✅ CORRECT for workspace monorepos — deps already installed at root
const app = await builder.addViteApp("app", "./packages/frontend");
const api = await builder.addNodeApp("api", "./packages/api", "src/index.ts")
.withRunScript("start:dev");Tell the user: "This is a yarn workspace monorepo — I'll skip `.withYarn()` on individual resources since dependencies are shared at the root. Make sure to run `yarn` at the root before `aspire start`."
This only applies to workspace monorepos with shared `node_modules`. For standalone apps or apps with independent node_modules directories, .withYarn() / .withPnpm() is correct and should be used — it ensures deps are installed before the resource starts.
TypeScript AppHost dependency configuration (Step 6)
package.json
If one exists at the root, augment it (do not overwrite). Add/merge these scripts that delegate to the Aspire CLI:
{
"type": "module",
"scripts": {
"dev": "aspire run",
"build": "tsc",
"watch": "tsc --watch"
}
}If no root package.json exists, create a minimal one matching the canonical Aspire template:
{
"name": "<repo-name>",
"private": true,
"type": "module",
"scripts": {
"dev": "aspire run",
"build": "tsc",
"watch": "tsc --watch"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
}
}Important: Scripts should point to aspire run/aspire start — the Aspire CLI handles TypeScript compilation internally. Do not use npx tsc && node apphost.js patterns.
Never overwrite existing scripts, dependencies, or devDependencies — merge only. Do not manually add Aspire SDK packages — aspire restore handles those.
Run aspire restore to generate the .aspire/modules/ directory with TypeScript SDK bindings, then install dependencies with the repo's package manager (npm install, pnpm install, or yarn).
tsconfig.json
Augment if it exists:
- Ensure
".aspire/modules/**/*.ts"and"apphost.ts"are ininclude - Ensure
"module"is"nodenext"or"node16"(ESM required) - Ensure
"moduleResolution"matches
If no tsconfig.json exists and aspire restore didn't create one, create a minimal one:
{
"compilerOptions": {
"target": "ES2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"strict": true,
"outDir": "./dist",
"rootDir": "."
},
"include": ["apphost.ts", ".aspire/modules/**/*.ts"]
}ESLint
Only augment if config already exists. If it uses parserOptions.project or parserOptions.projectService, ensure the AppHost tsconfig is discoverable. Do not create ESLint configuration from scratch.
OpenTelemetry setup for non-.NET services
Use this reference when the user opts in to adding OpenTelemetry instrumentation to non-.NET services (Step 8). Aspire automatically injects OTEL_EXPORTER_OTLP_ENDPOINT into all managed resources — the services just need to read it.
Node.js/TypeScript services
# Use the repo's package manager (npm/pnpm/yarn)
npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-otlp-grpc
# or: pnpm add ...
# or: yarn add ...Create an instrumentation file (e.g., instrumentation.ts or instrumentation.js):
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-otlp-grpc';
import { OTLPMetricExporter } from '@opentelemetry/exporter-otlp-grpc';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter(),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter(),
}),
instrumentations: [getNodeAutoInstrumentations()],
serviceName: process.env.OTEL_SERVICE_NAME,
});
sdk.start();Then ensure the service loads it early — either via --require/--import in the start script or by importing it as the first line of the entry point.
Python services
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install # auto-detect and install framework instrumentationsAdd to the service's startup (e.g., top of main.py or as a separate instrumentation.py):
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry import trace, metrics
import os
resource = Resource.create({"service.name": os.environ.get("OTEL_SERVICE_NAME", "unknown")})
# Traces
trace.set_tracer_provider(TracerProvider(resource=resource))
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
# Metrics
metrics.set_meter_provider(MeterProvider(
resource=resource,
metric_readers=[PeriodicExportingMetricReader(OTLPMetricExporter())],
))Or more simply, run with the auto-instrumentation wrapper:
opentelemetry-instrument uvicorn main:app --host 0.0.0.0Go services
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
go get go.opentelemetry.io/otel/sdk/trace
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttpAdd initialization in main():
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
func initTracer() func() {
exporter, _ := otlptracegrpc.New(context.Background())
tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
otel.SetTracerProvider(tp)
return func() { tp.Shutdown(context.Background()) }
}Wrap HTTP handlers with otelhttp.NewHandler() for automatic HTTP span creation.
Java services
Point the user to the OpenTelemetry Java Agent — it's the easiest approach:
java -javaagent:opentelemetry-javaagent.jar -jar myapp.jarThe agent auto-instruments common frameworks. Aspire injects OTEL_EXPORTER_OTLP_ENDPOINT automatically.
Scan & Propose
Heuristics for the scan and propose phases of aspireify.
Scan Checklist
| Inventory | How |
|---|---|
| .NET projects | find . -name '*.csproj' -not -path '*/bin/*' -not -path '*/obj/*' |
| Top-level Node services | find . -maxdepth 4 -name 'package.json' -not -path '*/node_modules/*' |
| Python services | find . -maxdepth 4 -name 'pyproject.toml' -o -name 'requirements.txt' |
| Container deps | cat docker-compose*.y*ml compose*.y*ml 2>/dev/null |
| Connection strings | `grep -rIE '(Postgres\ |
| Hardcoded URLs | grep -rIE 'http://localhost:[0-9]+' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.cs' |
| Existing integration packages | dotnet list package per .csproj; jq .dependencies package.json per Node project |
| Existing endpoints | launchSettings.json, next.config.js, vite.config.ts, apphost.ts modules |
| Existing AppHost references | apphost.cs / Program.cs / apphost.ts — what's already wired? |
Heuristics
| Signal | Suggest |
|---|---|
.csproj references Microsoft.AspNetCore.App | API project → AddProject + WithExternalHttpEndpoints() if user-facing |
.csproj references Microsoft.NET.Sdk.Worker | Background worker → AddProject, no external endpoints |
package.json has "next" dependency | AddNextJsApp — confirm next.config.js has output: 'standalone' |
package.json has "vite" + SPA bundle | AddViteApp + PublishAsStaticWebsite(apiPath, apiTarget) |
package.json has "vite" + .output/server/index.mjs (TanStack/SvelteKit) | AddViteApp (dev) + PublishAsNodeServer (publish) |
package.json has Remix / Astro / Nitro Next | PublishAsPackageScript |
pyproject.toml with FastAPI/Flask | AddPythonApp (or model under TS AppHost) |
Program.cs reads ConnectionStrings:Postgres* / DI calls AddNpgsql* | AddPostgres('pg').AddDatabase('appdb') + WithReference |
Program.cs calls AddStackExchangeRedisCache | AddRedis('cache') + WithReference |
MongoClient / MongoDB.Driver | AddMongoDB('mongo') |
Code refs RabbitMQ.Client / IConnection | AddRabbitMQ('mq') (v7 client — pub/sub tracing) |
Code refs Microsoft.Azure.Cosmos | AddAzureCosmosDB('cosmos') |
Code refs Azure.Messaging.ServiceBus | AddAzureServiceBus('sb') |
Frontend hardcodes http://localhost:5000 | Replace with service discovery: endpoint.url (TS) or WithReference(api) |
Integration Catalog
Datastores
| Service | C# integration | TS integration | Notes |
|---|---|---|---|
| Postgres | AddPostgres("pg").AddDatabase("appdb") | addPostgres('pg').addDatabase('appdb') | Npgsql metrics align to .NET 10 |
| SQL Server | AddSqlServer("sql").AddDatabase("appdb") | addSqlServer('sql').addDatabase('appdb') | Container-backed locally |
| MySQL | AddMySql("my").AddDatabase("appdb") | addMySql('my').addDatabase('appdb') | |
| MongoDB | AddMongoDB("mongo").AddDatabase("app") | addMongoDB('mongo').addDatabase('app') | |
| Redis | AddRedis("cache") | addRedis('cache') | |
| Azure Cache for Redis | AddAzureRedis("cache") | addAzureRedis('cache') | Aspire.Microsoft.Azure.StackExchangeRedis is GA |
| Cosmos DB | AddAzureCosmosDB("cosmos") | addAzureCosmosDB('cosmos') | |
| Azure SQL | AddAzureSqlServer("sql") | addAzureSqlServer('sql') | |
| Azure Storage | AddAzureStorage("storage") | addAzureStorage('storage') |
Messaging
| Service | C# | TS | Notes |
|---|---|---|---|
| RabbitMQ | AddRabbitMQ("mq") | addRabbitMQ('mq') | v7 client, OTel pub/sub tracing |
| Azure Service Bus | AddAzureServiceBus("sb") | addAzureServiceBus('sb') | |
| Kafka | AddKafka("kafka") | addKafka('kafka') | |
| Azure Event Hubs | AddAzureEventHubs("eh") | addAzureEventHubs('eh') | |
| Durable Task Scheduler | AddDurableTaskScheduler(...) | n/a | Experimental: ASPIREDURABLETASK001 |
Frontends (JS/TS)
| Pattern | Add | Publish |
|---|---|---|
| Next.js (SSR or static) | AddNextJsApp("web", "./web") | Auto — Next.js standalone (set output: 'standalone' in next.config.js) |
| Vite SPA | AddViteApp("web", "./web") | PublishAsStaticWebsite(apiPath: "/api", apiTarget: api) |
| Vite + TanStack/SvelteKit (SSR via Node) | AddViteApp("web", "./web") | PublishAsNodeServer(entryPoint: ".output/server/index.mjs", outputPath: ".output") |
| Remix / Astro SSR / Nitro | AddNodeApp or AddViteApp | PublishAsPackageScript(scriptName: "start") |
| Plain Node | AddNodeApp("api", "server.js") | PublishAsNodeServer |
Bun, Yarn, and pnpm are first-class in TS AppHosts (npm remains the default).
AI / Foundry
| Pattern | API |
|---|---|
| Azure AI Foundry Prompt Agent | AddPromptAgent(...) (replaces non-functional AddAndPublishPromptAgent) |
| Predefined Foundry models in TS | [AspireValue] + catalogs like FoundryModels.OpenAI.Gpt41Mini |
Compute environments (binding)
| Target | API |
|---|---|
| Azure Container Apps | AddAzureContainerAppEnvironment("aca") |
| Azure App Service | AddAzureAppServiceEnvironment("appsvc") |
| Azure Kubernetes Service | AddAzureKubernetesEnvironment("aks").WithSystemNodePool(...) |
| Plain Kubernetes | AddKubernetesEnvironment("k8s") (Helm-based) |
| Docker Compose | AddDockerComposeEnvironment("compose") |
Bind a resource: .WithComputeEnvironment(env). Required when multiple environments are declared.
Proposal Template
When presenting the proposed graph to the user, structure it as:
SCAN RESULTS
Projects: Api (csproj), Worker (csproj)
Frontends: web (Next.js)
External deps: Postgres (compose), Redis (compose)
Connection strings hardcoded in: Api/appsettings.Development.json
PROPOSED RESOURCE GRAPH
- pg (Postgres)
- appdb (database)
- cache (Redis)
- api (Project) → references pg, cache; waits for both; external HTTP
- worker (Project) → references pg
- web (Next.js) → references api; waits for api; WithBrowserLogs
QUESTIONS BEFORE I EDIT
1. Replace appsettings Postgres connection string with Aspire service discovery? [Y/n]
2. Mark Api's /admin endpoint as ExcludeReferenceEndpoint? [Y/n]
3. Bind everything to a default compute environment, or wait for deploy? [skip/aca/aks]Wait for confirmation before editing.
ServiceDefaults Wiring
After resources are declared in the AppHost, every service project should opt into Aspire's defaults: OpenTelemetry, health checks, service discovery, and HTTP resilience. This is what Aspire.ServiceDefaults does.
Add the Project
aspire init may already have generated a MyApp.ServiceDefaults project. If not, create one:
dotnet new aspire-servicedefaults -n MyApp.ServiceDefaultsThen reference it from each service:
dotnet add ./src/Api/Api.csproj reference ./MyApp.ServiceDefaults/MyApp.ServiceDefaults.csprojWire It in Program.cs
Each service's Program.cs should call AddServiceDefaults() and MapDefaultEndpoints():
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults(); // OTel, health checks, service discovery, HTTP resilience
// ... your services ...
var app = builder.Build();
app.MapDefaultEndpoints(); // /health and /alive endpoints
// ... your endpoints ...
app.Run();What AddServiceDefaults() Wires
| Concern | What |
|---|---|
| OpenTelemetry tracing | ASP.NET Core, HttpClient, gRPC — exported via OTLP to dashboard |
| OpenTelemetry metrics | Runtime, ASP.NET Core, HttpClient — exported via OTLP |
| OpenTelemetry logging | Structured logs exported via OTLP |
| Health checks | IHealthChecksBuilder configured; /health and /alive endpoints |
| Service discovery | Microsoft.Extensions.ServiceDiscovery registered for HttpClient |
| HTTP resilience | Standard resilience handler on outbound HttpClient |
Wire Health Checks for WaitFor
AppHost's .WaitFor(resource) requires a health endpoint. MapDefaultEndpoints() provides /alive (liveness) and /health (readiness).
In the AppHost:
var api = builder.AddProject<Projects.Api>("api")
.WithHttpHealthCheck("/health");
builder.AddProject<Projects.Worker>("worker")
.WithReference(api)
.WaitFor(api); // Blocks worker start until api /health is 200 OKFrontend Projects (Node / Next.js / Vite)
Service discovery for JS frontends comes via env vars injected by WithReference(api):
services__api__http__0=http://localhost:NNNN
services__api__https__0=https://localhost:NNNNIn the frontend, read these (or use endpoint.url in TS AppHost expressions to pre-resolve at AppHost build time).
For browser-side telemetry, add WithBrowserLogs() on the frontend resource — it captures console logs, network requests, and screenshots into the dashboard without any client-side wiring.
Custom OTel Configuration
AddServiceDefaults() is a starting point. Extend it in your own ServiceDefaults project:
public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
{
builder.ConfigureOpenTelemetry();
builder.AddDefaultHealthChecks();
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler();
http.AddServiceDiscovery();
});
return builder;
}Checklist
- [ ]
Aspire.ServiceDefaultsproject exists - [ ] Each service
.csprojhas<ProjectReference>to it - [ ] Each service
Program.cscallsbuilder.AddServiceDefaults() - [ ] Each service
Program.cscallsapp.MapDefaultEndpoints() - [ ] AppHost uses
.WithHttpHealthCheck("/health")for resources that consumersWaitFor - [ ] Frontend resources use
.WithBrowserLogs()if browser telemetry is desired
TypeScript AppHost Authoring
Patterns for editing apphost.ts. Never edit `.aspire/modules/` — that directory is generated and any changes will be clobbered. Edit only apphost.ts.
Look up unfamiliar API: aspire docs api search <query> --language typescriptthen aspire docs api get <id>.⛔ Hard Refusal: .aspire/modules/ Is Off-Limits
If a user asks to edit any file in .aspire/modules/ (for example .aspire/modules/postgres.module.ts, .aspire/modules/redis.module.ts, .aspire/modules/api.module.ts), refuse the edit and redirect to apphost.ts. This is non-negotiable:
.aspire/modules/is regenerated by the TS AppHost fromapphost.tsplus the installed
Aspire packages on every build, every aspire add, every aspire start.
- Hand edits inside
.aspire/modules/are clobbered without warning — they may survive
one run and disappear on the next.
- The TS AppHost considers
.aspire/modules/an internal artifact, not user code.
Refusal template
When asked to edit anything inside .aspire/modules/, respond with the structure below:
I won't edit files in .aspire/modules/ — that directory is generated by the TypeScript
AppHost and any edits get clobbered on the next build / `aspire add` / `aspire start`.
The right place to make this change is apphost.ts. Here is the equivalent edit:
// apphost.ts
builder.addPostgres('pg', { /* the options you wanted to set */ });
If you wanted to add a new integration, run `aspire add <package>` and Aspire will
regenerate `.aspire/modules/` for you.Apply this rule regardless of how the request is phrased — "just open it", "make a quick tweak", "change one line in .aspire/modules/", "I'll undo it later." All of these get the same refusal-plus-redirect.
| ❌ NEVER | ✅ ALWAYS |
|---|---|
Edit .aspire/modules/postgres.module.ts | Edit apphost.ts addPostgres('pg', {...}) |
Edit .aspire/modules/api.module.ts | Edit apphost.ts addProject('api', '...') |
Comment out lines in any .aspire/modules/*.ts file | Remove / guard the declaration in apphost.ts |
Run codegen against .aspire/modules/ and patch the output | Add the integration via aspire add <package> |
Skeleton
import { createBuilder } from '@aspire/hosting';
const builder = await createBuilder();
const pg = builder.addPostgres('pg').addDatabase('appdb');
const cache = builder.addRedis('cache');
const api = await builder.addProject('api', '../Api/Api.csproj')
.withReference(pg)
.withReference(cache)
.waitFor(pg)
.withExternalHttpEndpoints();
await builder.addNextJsApp('web', '../web')
.withReference(api)
.waitFor(api)
.withBrowserLogs();
await builder.build().run();Unified withEnvironment API
A single method handles every value kind — string, ReferenceExpression, EndpointReference, parameter builder, connection-string resource builder, or any IExpressionValue:
const apiKey = builder.addParameter('apiKey', { secret: true });
const cache = builder.addRedis('cache');
const db = builder.addPostgres('pg').addDatabase('appdb');
const api = await builder.addProject('api', '../Api/Api.csproj');
await api
.withEnvironment('SERVICE_URL', cache.primaryEndpoint) // endpoint
.withEnvironment('API_KEY', apiKey) // parameter
.withEnvironment('DB', db); // connection string❌ DO NOT USE the per-kind helpers — they are deprecated:
withEnvironmentEndpoint,withEnvironmentParameter,withEnvironmentConnectionString,
withEnvironmentExpression,withEnvironmentFromOutput,withEnvironmentFromKeyVaultSecret.
>
✅ Use unified withEnvironment(name, value) instead. Any agent suggestingthe per-kind helpers is wrong for current Aspire.
Endpoint Property Expressions
Endpoints expose url, host, and port properties usable inside expressions:
const api = await builder.addProject('api', '../Api/Api.csproj');
const httpEndpoint = api.getEndpoint('http');
await builder.addNodeApp('worker', 'worker.js')
.withEnvironment('API_BASE', httpEndpoint.url)
.withEnvironment('API_HOST', httpEndpoint.host)
.withEnvironment('API_PORT', httpEndpoint.port);Endpoint Update Behavior
withEndpoint('name', cb) updates an existing endpoint rather than throwing. Use the excludeReferenceEndpoint flag to keep admin endpoints out of withReference():
await builder.addProject('api', '../Api/Api.csproj')
.withEndpoint('admin', e => { e.excludeReferenceEndpoint = true; });JavaScript / TypeScript Frontends
| Helper | Use For |
|---|---|
addNextJsApp(name, projectPath) | Next.js (auto standalone publish; set output: 'standalone' in next.config.js) |
addViteApp(name, projectPath) | Vite (dev server) |
addNodeApp(name, scriptPath) | Plain Node service |
Package managers: Bun, Yarn, pnpm are first-class alongside npm.
Publish hooks (mirror C# PublishAs*):
// Vite SPA → static website with optional API proxy
await builder.addViteApp('web', '../web')
.withReference(api)
.publishAsStaticWebsite({ apiPath: '/api', apiTarget: api });
// Pre-bundled Node server (TanStack Start, SvelteKit)
await builder.addViteApp('web', '../web')
.publishAsNodeServer({ entryPoint: '.output/server/index.mjs', outputPath: '.output' });
// package-script SSR (Remix, Astro, full Nitro Next.js)
await builder.addViteApp('web', '../web')
.publishAsPackageScript({ scriptName: 'start' });Docker Compose Hooks
await builder.addContainer('netshoot', 'nicolaka/netshoot')
.publishAsDockerComposeService((resource, service) => {
service.privileged = true;
});Dockerfile Builder APIs (experimental)
// Diagnostic ASPIREDOCKERFILEBUILDER001 — experimental warning
await builder.addDockerfileBuilder('myimage')
.withDockerfileBuilder(b => b
.from('node:20-alpine')
.workdir('/app')
.copy('package*.json', './')
.run('npm ci')
.copy('.', '.')
.cmd(['node', 'server.js']));YARP Routing
const api = await builder.addProject('api', '../Api/Api.csproj');
const yarp = await builder.addYarp('gateway')
.addRoute('/api/{**catch-all}', api.getEndpoint('http'))
.addCatchAllRoute(web.getEndpoint('http'));Compute Environments
const aca = builder.addAzureContainerAppEnvironment('aca');
const aks = builder.addAzureKubernetesEnvironment('aks');
await builder.addProject('api', '../Api/Api.csproj')
.withComputeEnvironment(aca);ACA custom domain configuration is exposed in TS:
await api.withComputeEnvironment(aca, e => {
e.customDomains = [{ name: 'api.example.com', certificate: cert }];
});Predefined Value Catalogs
The [AspireValue] attribute and predefined catalogs let you reference well-known values:
import { FoundryModels } from '@aspire/hosting-azure';
await builder.addAzureFoundry('foundry')
.addModel('chat', FoundryModels.OpenAI.Gpt41Mini);Other TS Additions
| API | Purpose |
|---|---|
withAdminDeploymentScriptSubnet(...) | Now exported in TS |
configureEnvFile(...) | Generate .env files for compute environments |
| Image push options | Custom registry, tag, push behavior |
| Endpoint mutation callbacks | Modify endpoints after declaration |
| Builder pipeline | Custom pipeline steps from TS |
Browser Logs
await builder.addViteApp('frontend', '../frontend')
.withBrowserLogs();Diagnostic IDs
| Diagnostic | What |
|---|---|
ASPIREEXPORT013 | Build-time duplicate exported capability ID detection |
ASPIREJAVASCRIPT001 | Renamed from ASPIREEXTENSION001 (13.3 breaking) |
ASPIREDOCKERFILEBUILDER001 | Experimental warning for WithDockerfileBuilder / AddDockerfileBuilder |
ASPIREDURABLETASK001 | Durable Task Scheduler experimental APIs |
Hard Rules
| Rule | Why |
|---|---|
| Never edit `.aspire/modules/` | Generated; edits are clobbered. Edit only apphost.ts |
Use unified withEnvironment(name, value) | Per-kind helpers are deprecated |
Use addNextJsApp / addViteApp over hand-rolled Dockerfiles | First-class lifecycle + publish helpers |
Use publishAs* for JS publish — never raw Dockerfile when a helper fits | Maintained, tested, and works with aspire deploy |
package.json engines.node no longer drives Node image selection | Pin via publish helper options instead |
Validation
End-to-end validation after editing the AppHost. Goal: confirm the wired graph starts cleanly, every resource reaches a healthy state, and aspire describe returns the expected shape. After a clean validation, aspireify self-deactivates and hands off to aspire-orchestration.
Flow
# 1. Start the AppHost in the background, JSON output for parsing
aspire start --non-interactive --format Json
# 2. Wait for each declared resource (use displayName from aspire ps)
aspire ps --format Json --include-hidden # also lists hidden resources
aspire wait <displayName> # repeat per resource
# 3. Sanity-check the resource graph
aspire describe --format Json --include-hidden
# 4. Stop cleanly
aspire stopWhy --non-interactive
Agents must always pass --non-interactive to aspire start (and other commands) to suppress prompts and spinners. Without it the command can hang waiting for input.
Why --format Json
Structured output for parsing. Note: aspire start can interleave human-readable lines with JSON in some scenarios (#15843). Strip non-JSON lines before parsing.
Why --include-hidden
aspire ps and aspire describe filter hidden resources (proxies, helper containers, migration jobs) by default. During validation use --include-hidden so you don't miss anything that aspire start actually started.
Why aspire wait not curl loops
aspire wait <name> blocks until the resource reports healthy via the AppHost's own health model. Hand-rolled curl loops race with startup and don't see container-internal health.
Resource-name gotcha (#15842):
aspire waitrejects thenamefield — passdisplayNamefrom
aspire ps --format Json instead.Recovery
| Symptom | Action |
|---|---|
| Build error in resource | Fix code; re-run aspire start |
| File-lock error during edit | Hand off to aspire-orchestration → aspire stop → retry edit |
| Port conflict on start | aspire stop (clears prior instance) → aspire start |
Resource missing from aspire ps | Re-run with --include-hidden; if still missing, AppHost edit is wrong |
aspire wait rejects name | Pull displayName from aspire ps --format Json |
Mixed JSON output from aspire start | Strip non-JSON lines before parsing |
| Container-backed resource fails | Confirm Docker / Podman is running; re-run aspire doctor |
| TS AppHost change had no effect | You probably edited .aspire/modules/. Edit apphost.ts only |
Hand-off Criteria
Aspireify deactivates when all of these hold:
1. aspire start --non-interactive exits 0 (or stays healthy in background mode) 2. aspire wait returns success for every declared, non-hidden resource 3. aspire describe --format Json shows the proposed graph (resources, references, endpoints) 4. aspire stop cleans up without errors
When done, announce:
✅ AppHost wired and validated:
- Resources: pg, cache, api, worker, web
- References: api → pg + cache; worker → pg; web → api
- All resources reached healthy state.
Handing off to aspire-orchestration for day-to-day start/stop/wait.
Aspireify is done.Optional: Pre-Deploy Smoke Check
If the user plans to deploy next, suggest:
aspire publish --list-steps
aspire deploy --list-stepsThen route them to aspire-deployment for the actual aspire deploy / aspire publish / aspire destroy workflow.
Related skills
FAQ
Can aspireify edit files in .aspire/modules?
No. That directory is generated and changes are clobbered on the next build or aspire start.
When should I use aspireify?
After aspire init when the AppHost stub has no wired resources and you need a working local graph.
Is aspireify safe to install?
Review the Security Audits panel on this page before installing in production.