This is the full developer documentation for Aspire # Compose distributed apps in code. > Model, run, observe, and deploy distributed applications from one code-first control plane. 01 Polyglot by design ## Bring every service into one model. Use the language and framework that fit each service. Aspire brings the whole stack into one model and one workflow. * ![](/_astro/csharp.bEYMnWDV_Z1ibTvo.svg)C# * ![](/_astro/typescript.C9-blvjE_Z1ibTvo.svg)TypeScript * ![](/_astro/javascript.BtpOR5m6_Z1ibTvo.svg)JavaScript * ![](/_astro/python.HT_z5JOp_Z1ibTvo.svg)Python * ![](/_astro/java.B9Whfg8T_Z1ibTvo.svg)Java * ![](/_astro/go-light-icon.CrPrQ3fH_1BhLB3.webp)Go * ![](/_astro/rust-icon.Dbc7QCHB_ZEgN4g.webp)Rust * ![](/_astro/docker.CnL-arqo_Z1ibTvo.svg)Containers [Explore application resources](/get-started/resources/) 02 Agent-ready ## Give AI the full application context. Aspire supplies live context from your application model, running resources, and telemetry. * ### AppHost Services + dependencies * ### Runtime Resources + health * ### Telemetry Logs + traces + errors [Set up Aspire for your agent](/get-started/ai-coding-agents/) Aspire**AppHost** Live application context * GitHub Copilot * ![](/_astro/agent-claude.dJjxgD3o_Z1M1tdi.svg)Claude * ![](/_astro/agent-openai.L0iksW7i_Z1M1tdi.svg)OpenAI One connection. Full application context. Works with any AI tool. 03 One application model ## From repository to running system. The AppHost is the source of truth for how the application fits together across development, diagnostics, and deployment. Built for your AI agent **Context from Aspire** Aspire gives connected AI tools the resource graph, references, configuration, and commands represented here. ********terminal**aspire-shop ``` $ aspire run AppHost:apphost.mts Dashboard:https://localhost:17178/login?t=b3f9c1e0 Logs:~/.aspire/cli/logs/apphost-2f8c.log Press CTRL+C to stop the AppHost and exit. ``` Running aspire run turns the AppHost application model into a live resource topology, then exposes every healthy resource in the Aspire dashboard. ![](/_astro/typescript.C9-blvjE_gR4qT.svg)![](/_astro/csharp.bEYMnWDV_gR4qT.svg)AppHostTopologyDashboard ```typescript import { createBuilder } from "./.aspire/modules/aspire.mjs"; const builder = await createBuilder(); const postgres = await builder .addPostgres("postgres") .addDatabase("catalogdb"); const cache = await builder.addRedis("basketcache"); const catalog = await builder .addNodeApp("catalogservice", "./catalog", "src/index.ts") .withReference(postgres); await builder.addViteApp("frontend", "./frontend") .withReference(catalog) .withReference(cache); await builder.build().run(); ``` ```csharp var builder = DistributedApplication.CreateBuilder(args); var postgres = builder.AddPostgres("postgres") .AddDatabase("catalogdb"); var cache = builder.AddRedis("basketcache"); var catalog = builder .AddProject("catalogservice") .WithReference(postgres); builder.AddProject("frontend") .WithReference(catalog) .WithReference(cache); builder.Build().Run(); ``` ![](/_astro/react-icon.BghDNlos_Zf9fxc.svg)![](/_astro/typescript.C9-blvjE_Zf9fxc.svg)![](/_astro/javascript.BtpOR5m6_Zf9fxc.svg)Frontendfrontend ![](/_astro/csharp.bEYMnWDV_Zf9fxc.svg)![](/_astro/java.B9Whfg8T_Zf9fxc.svg)![](/_astro/python.HT_z5JOp_Zf9fxc.svg)![](/_astro/go-light-icon.CrPrQ3fH_Z1vO0lP.webp)Servicecatalogservice ![](/_astro/postgresql-icon.DTORe-rE_Z2uak78.webp)![](/_astro/mongodb-icon.KisFuM9l_1axqzM.webp)![](/_astro/mysqlconnector-icon.De_epwTX_Z22dkju.webp)![](/_astro/sqlite-icon.5bKv4aj0_at6xm.webp)Databasepostgres ![](/_astro/redis-icon.CxKuuAy2_Z1Lycfd.webp)![](/_astro/valkey-icon.BdkvDMZ6_ZrOgvH.webp)![](/_astro/garnet-icon.C0u592g4_Z2dyxaf.webp)Cachebasketcache service discoveryconnection stringcache client NameStateSourceURLs ![](/_astro/postgresql-icon.DTORe-rE_2i1zsH.webp)postgresdocker.io/library/postgres:18.3[tcp://localhost:5432]() ![](/_astro/redis-icon.CxKuuAy2_ZTVH7g.webp)basketcachedocker.io/library/redis:8.6[tcp://localhost:6379]() ![](/_astro/csharp.bEYMnWDV_P59Hc.svg)catalogserviceAspireShop.CatalogService.csproj[https://localhost:7099]() ![](/_astro/react-icon.BghDNlos_P59Hc.svg)frontendAspireShop.Frontend.csproj[https://localhost:7188]() **0** of 4 resources running 1. 01 ### Compose the app in code Define services, containers, data stores, references, and configuration in a readable AppHost. [Explore the AppHost](/get-started/app-host/) 2. 02 ### Run the whole system Start every dependency together, with service discovery, local endpoints, and dev-time configuration already connected. [Learn about aspire run](/reference/cli/commands/aspire-run/) 3. 03 ### Observe from the first request Use built-in OpenTelemetry to move from a resource to its logs, traces, metrics, and health without extra setup. [Explore the dashboard](/dashboard/) 4. 04 ### Deploy with the same application model Create deployment artifacts for cloud services, containers, Kubernetes, or the infrastructure path your team already uses. [Explore deployment options](/deployment/) 04 Local first, production ready ## One model. Every environment. Development, testing, and deployment stay connected to one application topology without forcing the architecture to drift. Agents see every environment **Context from Aspire** Aspire exposes the selected environment's resources, endpoints, health, and available commands to connected AI tools. 01Local02Test03Production ********app topology**Local Fast feedback ### Run everything locally. Start projects, containers, and data services together. Aspire wires local endpoints, configuration, and service discovery from the AppHost. `aspire run`**4 resources healthy ![](/_astro/aspire-logo-32.DGHSFRgf_j3OI6.svg)AppHost ![](/_astro/react-icon.BghDNlos_F8CNe.svg)![](/_astro/typescript.C9-blvjE_F8CNe.svg)![](/_astro/javascript.BtpOR5m6_F8CNe.svg)**frontend**local process ![](/_astro/csharp.bEYMnWDV_F8CNe.svg)![](/_astro/java.B9Whfg8T_F8CNe.svg)![](/_astro/python.HT_z5JOp_F8CNe.svg)![](/_astro/go-light-icon.CrPrQ3fH_1r8WV5.webp)**api**local process ![](/_astro/postgresql-icon.DTORe-rE_21DjXR.webp)![](/_astro/mongodb-icon.KisFuM9l_2qTOXR.webp)![](/_astro/mysqlconnector-icon.De_epwTX_Z2cclAl.webp)![](/_astro/sqlite-icon.5bKv4aj0_Z2dCX5l.webp)**database**local container ![](/_astro/redis-icon.CxKuuAy2_ZPJNef.webp)![](/_astro/valkey-icon.BdkvDMZ6_2efMEw.webp)![](/_astro/garnet-icon.C0u592g4_svw0Y.webp)**cache**local container Repeatable validation ### Test the topology you ship. Use the application model in automated tests and CI workflows so dependencies and relationships match the system developers run locally. [Testing docs](/testing/overview/)**isolated test resources ![](/_astro/aspire-logo-32.DGHSFRgf_j3OI6.svg)AppHost ![](/_astro/dev-tunnels-icon.Bn_RqgUr_F8CNe.svg)**frontend**dev tunnel ![](/_astro/csharp.bEYMnWDV_F8CNe.svg)![](/_astro/java.B9Whfg8T_F8CNe.svg)![](/_astro/python.HT_z5JOp_F8CNe.svg)![](/_astro/go-light-icon.CrPrQ3fH_1r8WV5.webp)**api**staging environment ![](/_astro/postgresql-icon.DTORe-rE_21DjXR.webp)![](/_astro/mongodb-icon.KisFuM9l_2qTOXR.webp)![](/_astro/mysqlconnector-icon.De_epwTX_Z2cclAl.webp)![](/_astro/sqlite-icon.5bKv4aj0_Z2dCX5l.webp)**database**staged database ![](/_astro/redis-icon.CxKuuAy2_ZPJNef.webp)![](/_astro/valkey-icon.BdkvDMZ6_2efMEw.webp)![](/_astro/garnet-icon.C0u592g4_svw0Y.webp)**cache**short-lived container Deployment ready ### Deploy the model you know. Move the application to cloud targets without changing its architecture. Choose managed services, containers, Kubernetes, or your own infrastructure. `aspire deploy`**release ready ![](/_astro/aspire-logo-32.DGHSFRgf_j3OI6.svg)AppHost ![](/_astro/azure-app-services.B0FFn4fq_F8CNe.svg)![](/_astro/aws.D-HJMglE_F8CNe.svg)**frontend**Azure App ServiceAWS Amplify ![](/_astro/azure-container-apps-environments.Cxfx93EZ_F8CNe.svg)![](/_astro/aws.D-HJMglE_F8CNe.svg)**api**Azure Container AppsAWS App Runner ![](/_astro/azure-postgresql-icon.BMlm1jYK_Z1Ct0bp.webp)![](/_astro/aws.D-HJMglE_F8CNe.svg)**database**Azure DatabaseAWS RDS ![](/_astro/azure-cacheredis-icon.BPM2--yb_ZgCbWf.webp)![](/_astro/aws.D-HJMglE_F8CNe.svg)**cache**Azure Managed RedisAWS ElastiCache 05 Observability included ## See the whole application. View OpenTelemetry logs, traces, and metrics locally. Follow requests across services with your team or coding agent. Agents act on these signals **Context from Aspire** Aspire gives connected AI tools dashboard context, including logs, traces, metrics, health, and resource commands. [Explore the Aspire dashboard](/dashboard/overview/)[Standalone dashboard](/dashboard/standalone/)[OpenTelemetry concepts](/fundamentals/telemetry/)[Debug with coding agents](/dashboard/ai-coding-agents/) ********Aspire dashboard**Resources / Graph ![Aspire dashboard resource graph showing the AspireShop services, databases, and caches connected as healthy nodes.](/_astro/resources-graph-dark.BPwlKInb_1kmTv8.webp) ![Aspire dashboard resources table listing every running project, container, and executable with state, endpoints, and actions.](/_astro/resources-table-dark.BtXqH7Uc_Zlg72Y.webp) ![Aspire dashboard console logs streaming interleaved, color-coded output from every resource in real time.](/_astro/console-dark.DEgDxIhH_2qficx.webp) ![Aspire dashboard structured logs view with a filterable, searchable grid of OpenTelemetry log records.](/_astro/structured-logs-dark.FLMXUvJZ_uNmRt.webp) ![Aspire dashboard traces list showing distributed traces with per-resource span counts and durations.](/_astro/traces-dark.BKkLrtKf_11tSMW.webp) ![Aspire dashboard trace detail waterfall for an add-to-cart request across the frontend, basket service, and cache, with span attributes.](/_astro/trace-detail-dark.Tmbk-v2w_Zwq7pv.webp) ![Aspire dashboard metrics view charting HTTP server request duration percentiles for the frontend with dimension filters.](/_astro/metrics-dark.CfqklzLO_Z1t72MK.webp) 06 Extensible by default ## Use the services you already trust. Add databases, caches, messaging, cloud resources, and community integrations through the same fluent application model. [![](/_astro/redis-icon.CxKuuAy2_1YTADb.webp)](/integrations/gallery/?search=redis)[![](/_astro/postgresql-icon.DTORe-rE_x9tSa.webp)](/integrations/gallery/?search=postgresql)[![](/_astro/docker.CnL-arqo_Z13ENcM.svg)](/integrations/gallery/?search=docker)[![](/_astro/kubernetes.C_WJle_s_Z2jPIKP.svg)](/integrations/gallery/?search=kubernetes)[![](/_astro/python.HT_z5JOp_Z1tEX6f.svg)](/integrations/gallery/?search=python)[![](/_astro/react-icon.BghDNlos_1cmamo.svg)](/integrations/gallery/?search=nodejs)[![](/_astro/rabbitmq-icon.D4S9ajGZ_Z1mEkvx.svg)](/integrations/gallery/?search=rabbitmq)[![](/_astro/azure-icon.Be9vXxEg_2qMheh.webp)](/integrations/gallery/?search=azure)[![](/_astro/aws-icon.CvC2PfNY_Z1qXY2b.webp)![](/_astro/aws-light-icon.U6rA_yMZ_2f5S6e.webp)](/integrations/gallery/?search=aws)[![](/_astro/github-icon.Byy9PfGk_Z21nOM3.webp)![](/_astro/github-light-icon.7mgvdsQN_2tWi1E.webp)](/integrations/gallery/?search=github)[![](/_astro/openai-icon.gefPjpNr_Zfhqox.webp)![](/_astro/openai-light-icon.DDa3Da58_1Ts5nU.webp)](/integrations/gallery/?search=openai)[![](/_astro/activemq-icon.BAGN0GK8_2pTDM0.webp)![](/_astro/activemq-light-icon.BOyxsb5v_Z1kBerf.webp)](/integrations/gallery/?search=activemq) [![](/_astro/redis-icon.CxKuuAy2_1YTADb.webp)](/integrations/gallery/?search=redis)[![](/_astro/postgresql-icon.DTORe-rE_x9tSa.webp)](/integrations/gallery/?search=postgresql)[![](/_astro/docker.CnL-arqo_Z13ENcM.svg)](/integrations/gallery/?search=docker)[![](/_astro/kubernetes.C_WJle_s_Z2jPIKP.svg)](/integrations/gallery/?search=kubernetes)[![](/_astro/python.HT_z5JOp_Z1tEX6f.svg)](/integrations/gallery/?search=python)[![](/_astro/react-icon.BghDNlos_1cmamo.svg)](/integrations/gallery/?search=nodejs)[![](/_astro/rabbitmq-icon.D4S9ajGZ_Z1mEkvx.svg)](/integrations/gallery/?search=rabbitmq)[![](/_astro/azure-icon.Be9vXxEg_2qMheh.webp)](/integrations/gallery/?search=azure)[![](/_astro/aws-icon.CvC2PfNY_Z1qXY2b.webp)![](/_astro/aws-light-icon.U6rA_yMZ_2f5S6e.webp)](/integrations/gallery/?search=aws)[![](/_astro/github-icon.Byy9PfGk_Z21nOM3.webp)![](/_astro/github-light-icon.7mgvdsQN_2tWi1E.webp)](/integrations/gallery/?search=github)[![](/_astro/openai-icon.gefPjpNr_Zfhqox.webp)![](/_astro/openai-light-icon.DDa3Da58_1Ts5nU.webp)](/integrations/gallery/?search=openai)[![](/_astro/activemq-icon.BAGN0GK8_2pTDM0.webp)![](/_astro/activemq-light-icon.BOyxsb5v_Z1kBerf.webp)](/integrations/gallery/?search=activemq) [![](/_astro/mongodb-icon.KisFuM9l_1Pblkv.webp)](/integrations/gallery/?search=mongodb)[![](/_astro/mysqlconnector-icon.De_epwTX_ZE7kja.webp)](/integrations/gallery/?search=mysql)[![](/_astro/sqlite-icon.5bKv4aj0_9M9RX.webp)](/integrations/gallery/?search=sqlite)[![](/_astro/nats-icon.D9qyPulo_Z1AoX1N.webp)](/integrations/gallery/?search=nats)[![](/_astro/dapr-icon.MGF9MsE6_27pDjG.webp)![](/_astro/dapr-light-icon.ELr6tnVg_Z2fLebY.webp)](/integrations/gallery/?search=dapr)[![](/_astro/keycloak-icon.DX4K8To8_25jVSr.svg)](/integrations/gallery/?search=keycloak)[![](/_astro/minio-icon.AQf9ocD3_15Kd9V.webp)](/integrations/gallery/?search=minio)[![](/_astro/meilisearch-icon.CR2umVYU_W7fpN.webp)](/integrations/gallery/?search=meilisearch)[![](/_astro/elastic-icon.C7IAv20j_ZYvbIK.webp)](/integrations/gallery/?search=elasticsearch)[![](/_astro/ollama-icon.4z_rvyfm_flhRy.webp)](/integrations/gallery/?search=ollama)[![](/_astro/qdrant-icon.wEc8LmZK_Z1DVRFB.svg)](/integrations/gallery/?search=qdrant)[![](/_astro/milvus-icon.QcNRKvVY_2lVTlP.webp)](/integrations/gallery/?search=milvus) [![](/_astro/mongodb-icon.KisFuM9l_1Pblkv.webp)](/integrations/gallery/?search=mongodb)[![](/_astro/mysqlconnector-icon.De_epwTX_ZE7kja.webp)](/integrations/gallery/?search=mysql)[![](/_astro/sqlite-icon.5bKv4aj0_9M9RX.webp)](/integrations/gallery/?search=sqlite)[![](/_astro/nats-icon.D9qyPulo_Z1AoX1N.webp)](/integrations/gallery/?search=nats)[![](/_astro/dapr-icon.MGF9MsE6_27pDjG.webp)![](/_astro/dapr-light-icon.ELr6tnVg_Z2fLebY.webp)](/integrations/gallery/?search=dapr)[![](/_astro/keycloak-icon.DX4K8To8_25jVSr.svg)](/integrations/gallery/?search=keycloak)[![](/_astro/minio-icon.AQf9ocD3_15Kd9V.webp)](/integrations/gallery/?search=minio)[![](/_astro/meilisearch-icon.CR2umVYU_W7fpN.webp)](/integrations/gallery/?search=meilisearch)[![](/_astro/elastic-icon.C7IAv20j_ZYvbIK.webp)](/integrations/gallery/?search=elasticsearch)[![](/_astro/ollama-icon.4z_rvyfm_flhRy.webp)](/integrations/gallery/?search=ollama)[![](/_astro/qdrant-icon.wEc8LmZK_Z1DVRFB.svg)](/integrations/gallery/?search=qdrant)[![](/_astro/milvus-icon.QcNRKvVY_2lVTlP.webp)](/integrations/gallery/?search=milvus) [![](/_astro/nodejs-icon.KKaryUxz_2piKMS.webp)![](/_astro/nodejs-light-icon.CKqeM1Fh_1vESNe.webp)](/integrations/gallery/?search=nodejs)[![](/_astro/bun-icon.DvKtlocW_soFda.webp)](/integrations/gallery/?search=bun)[![](/_astro/deno-icon.BlzooUTA_1ojaHs.webp)![](/_astro/deno-light-icon.BCKIGHbu_PmFj6.webp)](/integrations/gallery/?search=deno)[![](/_astro/go-icon.BtjqguRP_ZqTfPH.webp)![](/_astro/go-light-icon.CrPrQ3fH_Z1bdGXJ.webp)](/integrations/gallery/?search=golang%20gofeature)[![](/_astro/java-icon.R5ekku_P_2vtgzU.webp)](/integrations/gallery/?search=java)[![](/_astro/rust-icon.Dbc7QCHB_ZSuF44.webp)](/integrations/gallery/?search=rust)[![](/_astro/typescript.C9-blvjE_Z1tEX6f.svg)](/integrations/gallery/?search=typescript)[![](/_astro/azure-cosmosdb-icon.4tqzZo4Z_ZbGM2C.webp)](/integrations/gallery/?search=cosmos)[![](/_astro/azure-servicebus-icon.DHL-Rg8b_10C69q.webp)](/integrations/gallery/?search=servicebus)[![](/_astro/azure-ai-foundry-icon.0G-oiuDs_gGBuy.webp)](/integrations/gallery/?search=foundry)[![](/_astro/seq-icon.DTfU7WN9_25GRY2.webp)](/integrations/gallery/?search=seq)[![](/_astro/kurrent-icon.DqAKa12N_Z1nSyB4.webp)![](/_astro/kurrent-light-icon.D13VPSBq_Z2jhAXc.webp)](/integrations/gallery/?search=kurrentdb) [![](/_astro/nodejs-icon.KKaryUxz_2piKMS.webp)![](/_astro/nodejs-light-icon.CKqeM1Fh_1vESNe.webp)](/integrations/gallery/?search=nodejs)[![](/_astro/bun-icon.DvKtlocW_soFda.webp)](/integrations/gallery/?search=bun)[![](/_astro/deno-icon.BlzooUTA_1ojaHs.webp)![](/_astro/deno-light-icon.BCKIGHbu_PmFj6.webp)](/integrations/gallery/?search=deno)[![](/_astro/go-icon.BtjqguRP_ZqTfPH.webp)![](/_astro/go-light-icon.CrPrQ3fH_Z1bdGXJ.webp)](/integrations/gallery/?search=golang%20gofeature)[![](/_astro/java-icon.R5ekku_P_2vtgzU.webp)](/integrations/gallery/?search=java)[![](/_astro/rust-icon.Dbc7QCHB_ZSuF44.webp)](/integrations/gallery/?search=rust)[![](/_astro/typescript.C9-blvjE_Z1tEX6f.svg)](/integrations/gallery/?search=typescript)[![](/_astro/azure-cosmosdb-icon.4tqzZo4Z_ZbGM2C.webp)](/integrations/gallery/?search=cosmos)[![](/_astro/azure-servicebus-icon.DHL-Rg8b_10C69q.webp)](/integrations/gallery/?search=servicebus)[![](/_astro/azure-ai-foundry-icon.0G-oiuDs_gGBuy.webp)](/integrations/gallery/?search=foundry)[![](/_astro/seq-icon.DTfU7WN9_25GRY2.webp)](/integrations/gallery/?search=seq)[![](/_astro/kurrent-icon.DqAKa12N_Z1nSyB4.webp)![](/_astro/kurrent-light-icon.D13VPSBq_Z2jhAXc.webp)](/integrations/gallery/?search=kurrentdb) [Browse every integration](/integrations/) 07 Developer momentum ## Keep teams moving forward. Aspire shortens onboarding, clarifies distributed behavior, and keeps the local loop productive as systems grow. “ > Aspire lets developers be developers again. ![](/_astro/steven-price.DWQ6ydsD_Z21RxUP.webp)**[Steven Price](https://www.linkedin.com/in/steve-m-price/)**Software Engineering Manager, Iceland Foods “ > I had someone start on a Monday morning and they were contributing code by lunch. ![](/_astro/russ-harding.U2FjyOyA_Z24mevC.webp)**[Russ Harding](https://www.linkedin.com/in/russharding1/)**VP Engineering, EQengineered “ > I was surprised by how quickly Aspire got me from idea to running services. ![](/_astro/milan-jovanovic.Czwrk8nx_Z1kCUyc.webp)**[Milan Jovanović](https://www.milanjovanovic.tech)**Educator & Content Creator, MJ Tech “ > I've never wanted to commit to a Microsoft technology this much. ![](/_astro/nk54.B9ILi8AF_ZP3cDo.webp)**[Nk54 (Reddit User)](https://www.reddit.com/user/Nk54)** “ > Hit F5 to begin. Skip the setup boss fight and ship code faster. ![](/_astro/craig-taylor.BVQgR7jU_Z2el35I.webp)**[Craig Taylor](https://www.linkedin.com/in/craig-taylor-2594895/)**Principal Architect, Xbox Live “ > Aspire was easy to integrate with our existing container orchestration. ![](/_astro/sean-killeen.DnR5W1LG_Z245gBB.webp)**[Sean Killeen](https://www.linkedin.com/in/seankilleen/)**VP Innovation, SCT Software “ > OpenTelemetry out-of-the-box in the Aspire dashboard is a game changer for observability! ![](/_astro/dan-clarke.BBqMB07W_Z20OpLX.webp)**[Dan Clarke](https://www.danclarke.com)**Developer & Podcaster, Everstack [Join the Aspire community→](/community/) 08 Start with your stack ## Start building with less friction. Install the Aspire CLI, start with an existing repository or a new one, and bring every resource into one development loop. [Install Aspire](/get-started/install-cli/)[Build your first app](/get-started/first-app/)[View on GitHub](https://github.com/dotnet/aspire) # 404 > — This page doesn't exist—or at least, not anymore. Perhaps the route was deprecated...or maybe it never existed at all? [Go Back](/) [Go Home](/) # Certificate configuration > Configure HTTPS endpoints and certificate trust for Aspire resources to enable secure local development, container-to-container TLS, and trusted browser connections. Aspire provides two complementary sets of certificate APIs: 1. **HTTPS endpoint APIs**: Configure the certificates that resources use for their own HTTPS endpoints (server authentication) 2. **Certificate trust APIs**: Configure which certificates resources trust when making outbound HTTPS connections (client authentication) Both sets of APIs work together to enable secure HTTPS communication during local development. For example, a Vite frontend might use `WithHttpsDeveloperCertificate` to serve HTTPS traffic, while also using `WithDeveloperCertificateTrust` to trust the dashboard’s OTLP endpoint certificate. Caution Certificate customization only applies at run time. Custom certificates aren’t included in publish or deployment artifacts. ## Why HTTPS matters [Section titled “Why HTTPS matters”](#why-https-matters) HTTPS is essential for protecting the security and privacy of data transmitted between services. It encrypts traffic to prevent eavesdropping, tampering, and man-in-the-middle attacks. For production environments, HTTPS is a fundamental security requirement. However, enabling HTTPS during local development to match the production configuration presents unique challenges. Development environments typically use self-signed certificates that browsers and applications don’t trust by default. Managing these certificates across multiple services, containers, and different language runtimes can be complex and time-consuming, often creating friction in the development workflow. Aspire simplifies HTTPS configuration for local development by providing APIs to: * Configure HTTPS endpoints with appropriate certificates for server authentication * Manage certificate trust so resources can communicate with services using self-signed certificates * Automatically handle the development certificate (a per-user self-signed certificate valid only for local domains) across different resource types ## Trusting the development certificate [Section titled “Trusting the development certificate”](#trusting-the-development-certificate) Many of the certificate features in Aspire rely on a development certificate. Before using these features, you need to ensure that a trusted development certificate is installed on your machine. ### Using the Aspire CLI (recommended) [Section titled “Using the Aspire CLI (recommended)”](#using-the-aspire-cli-recommended) The preferred way to manage the development certificate is to use the [Aspire CLI](/get-started/install-cli/). When you run `aspire run` in an interactive session, the CLI automatically ensures the development certificate is created and trusted. No additional manual steps are required. For non-C# AppHosts (such as [TypeScript](/app-host/typescript-apphost/) or Python AppHosts), the `dotnet` first-run experience that normally creates the HTTPS development certificate never runs, because these AppHosts launch a prebuilt native binary instead of invoking `dotnet`. The Aspire CLI fills this gap when `aspire run` starts and no development certificate exists: * In an interactive session—and on Linux, where establishing trust doesn’t require a prompt—the CLI creates *and* trusts the certificate, just as it does for C# AppHosts. * In a non-interactive session on macOS or Windows (for example, in CI), the CLI can’t show the macOS Keychain password prompt or the Windows trust dialog, so it *generates* the certificate without trusting it. This lets servers such as Kestrel load the certificate from the personal store, even though it isn’t trusted. If the certificate can’t be generated, a warning is displayed and the run continues. To opt out of automatic certificate generation, set the `ASPIRE_CLI_GENERATE_HTTPS_CERTIFICATE` environment variable to `false`. This mirrors the .NET SDK’s `DOTNET_GENERATE_ASPNET_CERTIFICATE` opt-out: * Bash Disable automatic HTTPS certificate generation ```bash ASPIRE_CLI_GENERATE_HTTPS_CERTIFICATE=false aspire run ``` * PowerShell Disable automatic HTTPS certificate generation ```powershell $env:ASPIRE_CLI_GENERATE_HTTPS_CERTIFICATE="false"; aspire run ``` You can also manage certificates explicitly with the Aspire CLI: Trust the development certificate ```bash aspire certs trust ``` Remove and re-trust (refresh) ```bash aspire certs clean aspire certs trust ``` Tip If you encounter unexpected HTTPS or certificate trust errors during local development, running `aspire certs clean` followed by `aspire certs trust` is a good first troubleshooting step. Linux certificate trust On Linux, the development certificate is exported to `~/.aspnet/dev-certs/trust`, but applications using OpenSSL won’t discover it unless the `SSL_CERT_DIR` environment variable includes that path. You can set `SSL_CERT_DIR` in your shell profile (\~/.bashrc, \~/.zshrc, \~/.profile, etc.): \~/.bashrc, \~/.zshrc, or \~/.profile ```bash # Confirm /usr/lib/ssl/certs is the correct certificate directory for # your Linux distribution. Common alternatives include: # /etc/ssl/certs # /etc/pki/tls/certs if [ -z "$SSL_CERT_DIR" ]; then export SSL_CERT_DIR="/usr/lib/ssl/certs:$HOME/.aspnet/dev-certs/trust" else export SSL_CERT_DIR="$SSL_CERT_DIR:$HOME/.aspnet/dev-certs/trust" fi ``` You may need to reload your profile or start a new terminal session for the change to take effect. ### Developer certificate for DCP communication [Section titled “Developer certificate for DCP communication”](#developer-certificate-for-dcp-communication) By default, Aspire uses the ASP.NET Core developer certificate to secure communication with its internal Developer Control Plane (DCP) server. This replaces the ephemeral localhost certificate that DCP would otherwise generate itself, and avoids certificate trust errors caused by that certificate not being in the system trust store. If no trusted developer certificate is found, Aspire automatically falls back to DCP’s ephemeral certificate. To opt out and use DCP’s default ephemeral certificate instead, set `ASPIRE_DCP_USE_DEVELOPER_CERTIFICATE` to `false` in your AppHost’s `launchSettings.json` or as an environment variable: Properties/launchSettings.json ```json { "profiles": { "https": { "commandName": "Project", "environmentVariables": { "ASPIRE_DCP_USE_DEVELOPER_CERTIFICATE": "false" } } } } ``` Note Prior to Aspire 13.4, this setting defaulted to `false` and was only supported on Windows. As of 13.4, it defaults to `true` and is supported on Windows, macOS, and Linux. ## HTTPS endpoint configuration [Section titled “HTTPS endpoint configuration”](#https-endpoint-configuration) HTTPS endpoint configuration determines which certificate a resource presents when serving HTTPS traffic. This is server-side certificate configuration for resources that host HTTPS/TLS endpoints. ### Default behavior [Section titled “Default behavior”](#default-behavior) For resources that have a certificate configuration defined with `WithHttpsCertificateConfiguration`, Aspire attempts to configure it to use the development certificate if available. This automatic configuration works for many common resource types including YARP, Redis, and Keycloak containers; Vite based JavaScript apps; and Python apps using Uvicorn. You can control this behavior using the HTTPS endpoint APIs described below. ### Use the development certificate [Section titled “Use the development certificate”](#use-the-development-certificate) To explicitly configure a resource to use the development certificate for its HTTPS endpoints: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Explicitly use the developer certificate const nodeApp = await builder.addViteApp("frontend", "../frontend") .withHttpsDeveloperCertificate(); // Use developer certificate with an encrypted private key const certPassword = await builder.addParameter("cert-password", { secret: true }); const pythonApp = await builder.addUvicornApp("api", "../api", "app:main") .withHttpsDeveloperCertificate({ password: certPassword }); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Explicitly use the developer certificate var nodeApp = builder.AddViteApp("frontend", "../frontend") .WithHttpsDeveloperCertificate(); // Use developer certificate with an encrypted private key var certPassword = builder.AddParameter("cert-password", secret: true); var pythonApp = builder.AddUvicornApp("api", "../api", "app:main") .WithHttpsDeveloperCertificate(certPassword); builder.Build().Run(); ``` The `WithHttpsDeveloperCertificate` method: * Configures the resource to use the development certificate * Only applies in run mode (local development) * Optionally accepts a password parameter for encrypted certificate private keys * Works with containers, Node.js, Python, and other resource types ### Use a custom certificate [Section titled “Use a custom certificate”](#use-a-custom-certificate) To configure a resource to use a specific X.509 certificate for HTTPS endpoints: * TypeScript apphost.mts ```typescript import { createBuilder, refExpr } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const api = await builder.addContainer("api", { image: "my-api", tag: "latest", }); api.createExecutionConfiguration() .withArgumentsConfig() .withEnvironmentVariablesConfig() .withHttpsCertificateConfig(async () => ({ certificatePath: refExpr`/certs/tls.crt`, keyPath: refExpr`/certs/tls.key`, pfxPath: refExpr`/certs/tls.pfx`, })); await builder.build().run(); ``` * C# AppHost.cs ```csharp using System.Security.Cryptography.X509Certificates; var builder = DistributedApplication.CreateBuilder(args); // Load your certificate var certificate = new X509Certificate2("path/to/certificate.pfx", "password"); // Use the certificate for HTTPS endpoints builder.AddContainer("api", "my-api:latest") .WithHttpsCertificate(certificate); // Use certificate with a password parameter var certPassword = builder.AddParameter("cert-password", secret: true); builder.AddNpmApp("frontend", "../frontend") .WithHttpsCertificate(certificate, certPassword); builder.Build().Run(); ``` The certificate must: * Include a private key * Be a valid X.509 certificate * Be appropriate for server authentication ### Disable HTTPS certificate configuration [Section titled “Disable HTTPS certificate configuration”](#disable-https-certificate-configuration) To prevent Aspire from configuring any HTTPS certificate for a resource: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Disable automatic HTTPS certificate configuration const redis = await builder.addRedis("cache") .withoutHttpsCertificate(); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Disable automatic HTTPS certificate configuration var redis = builder.AddRedis("cache") .WithoutHttpsCertificate(); builder.Build().Run(); ``` Use `WithoutHttpsCertificate` when: * The resource doesn’t support HTTPS * You want to manually configure certificates * The resource has its own certificate management ### Customize certificate configuration [Section titled “Customize certificate configuration”](#customize-certificate-configuration) For resources that need custom certificate configuration logic, use `WithHttpsCertificateConfiguration` to specify how certificate files should be passed to the resource: * TypeScript apphost.mts ```typescript import { createBuilder, refExpr } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const api = await builder.addContainer("api", { image: "myimage", tag: "latest", }); api.createExecutionConfiguration() .withArgumentsConfig() .withEnvironmentVariablesConfig() .withCertificateTrustConfig(async () => ({ certificateBundlePath: refExpr`/certs/ca-bundle.crt`, certificateDirectoriesPath: refExpr`/certs`, rootCertificatesPath: "/etc/ssl/certs", isContainer: true, })); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddContainer("api", "my-api:latest") .WithHttpsCertificateConfiguration(ctx => { // Pass certificate paths as command line arguments ctx.Arguments.Add("--tls-cert"); ctx.Arguments.Add(ctx.CertificatePath); ctx.Arguments.Add("--tls-key"); ctx.Arguments.Add(ctx.KeyPath); // Or set environment variables ctx.EnvironmentVariables["TLS_CERT_FILE"] = ctx.CertificatePath; ctx.EnvironmentVariables["TLS_KEY_FILE"] = ctx.KeyPath; // Use PFX format if the resource requires it ctx.EnvironmentVariables["TLS_PFX_FILE"] = ctx.PfxPath; // Include password if needed if (ctx.Password is not null) { ctx.EnvironmentVariables["TLS_KEY_PASSWORD"] = ctx.Password; } return Task.CompletedTask; }); builder.Build().Run(); ``` The callback receives an `HttpsCertificateConfigurationCallbackAnnotationContext` that provides: * `CertificatePath`: Path to the certificate file in PEM format * `KeyPath`: Path to the private key file in PEM format * `PfxPath`: Path to the certificate in PFX/PKCS#12 format * `Password`: The password for the private key, if configured * `Arguments`: Command line arguments list to modify * `EnvironmentVariables`: Environment variables dictionary to modify * `ExecutionContext`: The current execution context * `Resource`: The resource being configured ## Certificate trust configuration [Section titled “Certificate trust configuration”](#certificate-trust-configuration) Certificate trust configuration determines which certificates a resource trusts when making outbound HTTPS connections. This is client-side certificate configuration. ### When to use certificate trust [Section titled “When to use certificate trust”](#when-to-use-certificate-trust) Certificate trust customization is valuable when: * Resources need to trust the development certificate for local HTTPS communication * Containerized services must communicate with the dashboard over HTTPS * Python or Node.js applications need to trust custom certificate authorities * You’re working with services that have specific certificate trust requirements * Resources need to establish secure telemetry connections to the Aspire dashboard ### Development certificate trust [Section titled “Development certificate trust”](#development-certificate-trust) By default, Aspire attempts to add trust for the development certificate to resources that wouldn’t otherwise trust it. This enables resources to communicate with the dashboard OTLP collector endpoint over HTTPS and any other HTTPS endpoints secured by the development certificate. You can control this behavior per resource using the `WithDeveloperCertificateTrust` API or through AppHost configuration settings. #### Configure development certificate trust per resource [Section titled “Configure development certificate trust per resource”](#configure-development-certificate-trust-per-resource) To explicitly enable or disable development certificate trust for a specific resource: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Explicitly enable development certificate trust const nodeApp = await builder.addNodeApp("frontend", "../frontend", "index.js") .withDeveloperCertificateTrust(true); // Disable development certificate trust const pythonApp = await builder.addPythonApp("api", "../api", "main.py") .withDeveloperCertificateTrust(false); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Explicitly enable development certificate trust var nodeApp = builder.AddNpmApp("frontend", "../frontend") .WithDeveloperCertificateTrust(trust: true); // Disable development certificate trust var pythonApp = builder.AddPythonApp("api", "../api", "main.py") .WithDeveloperCertificateTrust(trust: false); builder.Build().Run(); ``` ### Certificate authority collections [Section titled “Certificate authority collections”](#certificate-authority-collections) Certificate authority collections allow you to bundle custom certificates and make them available to resources. You create a collection using the `AddCertificateAuthorityCollection` method and then reference it from resources that need to trust those certificates. #### Create and use a certificate authority collection [Section titled “Create and use a certificate authority collection”](#create-and-use-a-certificate-authority-collection) AppHost.cs ```csharp using System.Security.Cryptography.X509Certificates; var builder = DistributedApplication.CreateBuilder(args); // Load your custom certificates var certificates = new X509Certificate2Collection(); certificates.ImportFromPemFile("path/to/certificate.pem"); // Create a certificate authority collection var certBundle = builder.AddCertificateAuthorityCollection("my-bundle") .WithCertificates(certificates); // Apply the certificate bundle to resources builder.AddNpmApp("my-project", "../myapp") .WithCertificateAuthorityCollection(certBundle); builder.Build().Run(); ``` Note This API is not yet available in TypeScript AppHosts. In the preceding example, the certificate bundle is created with custom certificates and then applied to a Node.js application, enabling it to trust those certificates. ### Certificate trust scopes [Section titled “Certificate trust scopes”](#certificate-trust-scopes) Certificate trust scopes control how custom certificates interact with a resource’s default trusted certificates. Different scopes provide flexibility in managing certificate trust based on your application’s requirements. The `WithCertificateTrustScope` API accepts a `CertificateTrustScope` value to specify the trust behavior. #### Available trust scopes [Section titled “Available trust scopes”](#available-trust-scopes) Aspire supports the following certificate trust scopes: * **Append**: Appends custom certificates to the default trusted certificates * **Override**: Replaces the default trusted certificates with only the configured certificates * **System**: Combines custom certificates with system root certificates and uses them to override the defaults * **None**: Disables all custom certificate trust configuration #### Append mode [Section titled “Append mode”](#append-mode) Attempts to append the configured certificates to the default trusted certificates for a given resource. This mode is useful when you want to add trust for additional certificates while maintaining trust for the system’s default certificates. This is the default scope for most resources. For Python resources, only OTEL trust configuration will be applied in this mode. * TypeScript apphost.mts ```typescript import { createBuilder, CertificateTrustScope } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); await builder.addNodeApp("api", "../api", "index.js") .withCertificateTrustScope(CertificateTrustScope.Append); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddNodeApp("api", "../api") .WithCertificateTrustScope(CertificateTrustScope.Append); builder.Build().Run(); ``` Note Not all languages and runtimes support Append mode. For example, Python doesn’t natively support appending certificates to the default trust store. Linux system trust preservation On Linux, executable resources configured with Append mode preserve access to the system’s OpenSSL certificate roots in addition to Aspire’s generated development certificate. If `SSL_CERT_DIR` is set in the AppHost process environment, Aspire adds those directories after its generated certificate directory. An explicitly empty value is preserved by not inferring system directories. If `SSL_CERT_DIR` is unset, Aspire adds any well-known system certificate directories that exist on the machine (for example, `/etc/ssl/certs`) after its own. Aspire reads this value from the AppHost process environment, not from an individual resource’s configured environment. This avoids a scenario where Linux workloads launched with `dotnet run` or an IDE lose OpenSSL’s implicit system certificate roots and fail outbound HTTPS requests once Aspire configures the resource with only its generated certificate directory. #### Override mode [Section titled “Override mode”](#override-mode) Attempts to override a resource to only trust the configured certificates, replacing the default trusted certificates entirely. This mode is useful when you need strict control over which certificates are trusted. AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var certBundle = builder.AddCertificateAuthorityCollection("custom-certs") .WithCertificates(myCertificates); builder.AddPythonModule("api", "./api", "uvicorn") .WithCertificateAuthorityCollection(certBundle) .WithCertificateTrustScope(CertificateTrustScope.Override); builder.Build().Run(); ``` Note This API is not yet available in TypeScript AppHosts. #### System mode [Section titled “System mode”](#system-mode) Attempts to combine the configured certificates with the default system root certificates and use them to override the default trusted certificates for a resource. This mode is intended to support Python and similar runtimes that don’t work well with Append mode. This is the default scope for Python projects because Python only has mechanisms to fully override certificate trust. * TypeScript apphost.mts ```typescript import { createBuilder, CertificateTrustScope } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); await builder.addPythonApp("worker", "../worker", "main.py") .withCertificateTrustScope(CertificateTrustScope.System); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddPythonApp("worker", "../worker", "main.py") .WithCertificateTrustScope(CertificateTrustScope.System); builder.Build().Run(); ``` #### None mode [Section titled “None mode”](#none-mode) Disables all custom certificate trust for the resource, causing it to rely solely on its default certificate trust behavior. This is the default scope for .NET projects on Windows, as there’s no way to automatically change the default system store source. * TypeScript apphost.mts ```typescript import { createBuilder, CertificateTrustScope } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); await builder.addContainer("service", { image: "myimage", tag: "latest" }) .withCertificateTrustScope(CertificateTrustScope.None); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddContainer("service", "myimage") .WithCertificateTrustScope(CertificateTrustScope.None); builder.Build().Run(); ``` ### Custom certificate trust configuration [Section titled “Custom certificate trust configuration”](#custom-certificate-trust-configuration) For advanced scenarios, you can specify custom certificate trust behavior using a callback API. This callback allows you to customize the command line arguments and environment variables required to configure certificate trust for different resource types. #### Configure certificate trust with a callback [Section titled “Configure certificate trust with a callback”](#configure-certificate-trust-with-a-callback) Use `WithCertificateTrustConfiguration` to customize how certificate trust is configured for a resource: * TypeScript Note This API is not yet available in TypeScript AppHosts. * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddContainer("api", "myimage") .WithCertificateTrustConfiguration(ctx => { // Add a command line argument ctx.Arguments.Add("--use-system-ca"); // Set environment variables with certificate paths // CertificateBundlePath resolves to the path of the custom certificate bundle file ctx.EnvironmentVariables["MY_CUSTOM_CERT_VAR"] = ctx.CertificateBundlePath; // CertificateDirectoriesPath resolves to paths containing individual certificates ctx.EnvironmentVariables["CERTS_DIR"] = ctx.CertificateDirectoriesPath; return Task.CompletedTask; }); builder.Build().Run(); ``` The callback receives a `CertificateTrustConfigurationCallbackAnnotationContext` that provides: * `Scope`: The `CertificateTrustScope` for the resource. * `Arguments`: Command line arguments for the resource. Values can be strings or path providers like `CertificateBundlePath` or `CertificateDirectoriesPath`. * `EnvironmentVariables`: Environment variables for configuring certificate trust. The dictionary key is the environment variable name; values can be strings or path providers. By default, includes `SSL_CERT_DIR` and may include `SSL_CERT_FILE` if Override or System scope is configured. * `CertificateBundlePath`: A value provider that resolves to the path of a custom certificate bundle file. * `CertificateDirectoriesPath`: A value provider that resolves to paths containing individual certificates. Default implementations are provided for Node.js, Python, and container resources. Container resources rely on standard OpenSSL configuration options, with default values that support the majority of common Linux distributions. #### Configure container certificate paths [Section titled “Configure container certificate paths”](#configure-container-certificate-paths) For container resources, you can customize where certificates are stored and accessed using `WithContainerCertificatePaths`: AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddContainer("api", "myimage") .WithContainerCertificatePaths( customCertificatesDestination: "/custom/certs/path", defaultCertificateBundlePaths: ["/etc/ssl/certs/ca-certificates.crt"], defaultCertificateDirectoryPaths: ["/etc/ssl/certs"]); builder.Build().Run(); ``` Note This API is not yet available in TypeScript AppHosts. The `WithContainerCertificatePaths` API accepts three optional parameters: * `customCertificatesDestination`: Overrides the base path in the container where custom certificate files are placed. If not set or set to `null`, the default path of `/usr/lib/ssl/aspire` is used. * `defaultCertificateBundlePaths`: Overrides the path(s) in the container where a default certificate authority bundle file is located. When the `CertificateTrustScope` is Override or System, the custom certificate bundle is additionally written to these paths. If not set or set to `null`, a set of default certificate paths for common Linux distributions is used. * `defaultCertificateDirectoryPaths`: Overrides the path(s) in the container where individual trusted certificate files are found. When the `CertificateTrustScope` is Append, these paths are concatenated with the path to the uploaded certificate artifacts. If not set or set to `null`, a set of default certificate paths for common Linux distributions is used. Note All desired paths must be configured in a single call to `WithContainerCertificatePaths` as only the most recent call to the API is honored. ## Common scenarios [Section titled “Common scenarios”](#common-scenarios) This section demonstrates common patterns for configuring HTTPS endpoints and certificate trust together. ### Configure a service with HTTPS and enable dashboard telemetry [Section titled “Configure a service with HTTPS and enable dashboard telemetry”](#configure-a-service-with-https-and-enable-dashboard-telemetry) A typical scenario is configuring a Node.js service to serve HTTPS traffic while also enabling it to send telemetry to the dashboard: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Configure the service to use developer certificate for HTTPS endpoints // and trust the developer certificate for outbound connections (like dashboard telemetry) const frontend = await builder.addNodeApp("frontend", "../frontend", "index.js") .withHttpsDeveloperCertificate() // Server cert for HTTPS endpoints .withDeveloperCertificateTrust(true); // Client trust for dashboard await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Configure the service to use developer certificate for HTTPS endpoints // and trust the developer certificate for outbound connections (like dashboard telemetry) var frontend = builder.AddNpmApp("frontend", "../frontend") .WithHttpsDeveloperCertificate() // Server cert for HTTPS endpoints .WithDeveloperCertificateTrust(true); // Client trust for dashboard builder.Build().Run(); ``` ### Enable HTTPS with custom certificates [Section titled “Enable HTTPS with custom certificates”](#enable-https-with-custom-certificates) When working with corporate or custom CA certificates, you can configure both server and client certificates: AppHost.cs ```csharp using System.Security.Cryptography.X509Certificates; var builder = DistributedApplication.CreateBuilder(args); // Load custom certificates var serverCert = new X509Certificate2("server-cert.pfx", "password"); var customCA = new X509Certificate2Collection(); customCA.Import("corporate-ca.pem"); var caBundle = builder.AddCertificateAuthorityCollection("corporate-certs") .WithCertificates(customCA); // Configure service with custom server cert and CA trust builder.AddContainer("api", "my-api:latest") .WithHttpsCertificate(serverCert) // Server cert for HTTPS .WithCertificateAuthorityCollection(caBundle); // Trust corporate CA builder.Build().Run(); ``` Note This API is not yet available in TypeScript AppHosts. ### Configure Redis with TLS [Section titled “Configure Redis with TLS”](#configure-redis-with-tls) Redis resources can be configured to use HTTPS (TLS) for secure connections: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Configure Redis to use the developer certificate for TLS const redis = await builder.addRedis("cache") .withHttpsDeveloperCertificate(); // Or disable TLS entirely const redisNoTls = await builder.addRedis("cache-notls") .withoutHttpsCertificate(); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Configure Redis to use the developer certificate for TLS var redis = builder.AddRedis("cache") .WithHttpsDeveloperCertificate(); // Or disable TLS entirely var redisNoTls = builder.AddRedis("cache-notls") .WithoutHttpsCertificate(); builder.Build().Run(); ``` ### Disable certificate configuration for specific resources [Section titled “Disable certificate configuration for specific resources”](#disable-certificate-configuration-for-specific-resources) To disable both HTTPS endpoint configuration and certificate trust for a resource that manages its own certificates: * TypeScript apphost.mts ```typescript import { createBuilder, CertificateTrustScope } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Disable all automatic certificate configuration await builder.addPythonModule("api", "./api", "uvicorn") .withoutHttpsCertificate() // No server cert config .withCertificateTrustScope(CertificateTrustScope.None); // No client trust config await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Disable all automatic certificate configuration builder.AddPythonModule("api", "./api", "uvicorn") .WithoutHttpsCertificate() // No server cert config .WithCertificateTrustScope(CertificateTrustScope.None); // No client trust config builder.Build().Run(); ``` ## Limitations [Section titled “Limitations”](#limitations) Certificate configuration has the following limitations: * Currently supported only in run mode, not in publish mode * Not all languages and runtimes support all trust scope modes * Python applications don’t natively support Append mode for certificate trust * Custom certificate configuration requires appropriate runtime support within the resource * HTTPS endpoint APIs are marked as experimental (`ASPIRECERTIFICATES001`) # AppHost configuration > Configure the Aspire AppHost — environment variables, launch profiles, network ports, container runtime selection, and the options that change orchestration behavior. The AppHost project configures and starts your distributed application. Configuration includes settings for the resource service, the [Aspire dashboard](/dashboard/overview/), and internal settings used by integrations. AppHost configuration is provided through launch profiles: Select your programming language TypeScriptC# C# AppHosts come in two forms, and each stores launch profiles differently: * **Project-based AppHost** (the default `dotnet new aspire-apphost` template): profiles live in `Properties/launchSettings.json`. * **File-based AppHost** (created with `aspire new` using the empty C# template): profiles live in `apphost.run.json`. In this template, `aspire.config.json` only points at the entry file — it does **not** contain a `profiles` block. Project-based AppHost — Properties/launchSettings.json ```json { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "https": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "https://localhost:17134;http://localhost:15170", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21030", "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22057" } } } } ``` File-based AppHost — apphost.run.json ```json { "profiles": { "https": { "applicationUrl": "https://localhost:17134;http://localhost:15170", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21030", "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22057" } } } } ``` File-based AppHost — aspire.config.json ```json { "appHost": { "path": "apphost.cs" } } ``` Note `aspire run`, `dotnet run apphost.cs`, and C# Dev Kit all read `apphost.run.json` for launch profiles when it is present. In the `aspire new` empty C# template, `aspire.config.json` intentionally omits `profiles` to avoid duplicating that data. In TypeScript AppHosts, profiles live in `aspire.config.json`: aspire.config.json ```json { "appHost": { "path": "apphost.mts", "language": "typescript/nodejs" }, "profiles": { "https": { "applicationUrl": "https://localhost:17134;http://localhost:15170", "environmentVariables": { "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21030", "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22057" } } } } ``` Note Configuration described on this page is for the Aspire AppHost project. To configure the standalone dashboard, see [dashboard configuration](/dashboard/configuration/). ## Common configuration [Section titled “Common configuration”](#common-configuration) | Option | Default value | Description | | -------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ASPIRE_ALLOW_UNSECURED_TRANSPORT` | `false` | Allows communication with the AppHost without https. `ASPNETCORE_URLS` (dashboard address) and `ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL` (AppHost resource service address) must be secured with HTTPS unless true. | | `ASPIRE_CONTAINER_RUNTIME` | `docker` | Allows the user of alternative container runtimes for resources backed by containers. Possible values are `docker` (default) or `podman`. | | `ASPIRE_DCP_USE_DEVELOPER_CERTIFICATE` | `true` | When `true` (the default), Aspire uses the ASP.NET Core developer certificate to secure the internal DCP server instead of an ephemeral certificate generated by DCP. On Windows, Aspire passes the certificate thumbprint to DCP. On macOS and Linux, Aspire passes the certificate and private key file paths (plus the thumbprint) so DCP can verify the loaded certificate. Set to `false` to opt out and use DCP’s default ephemeral certificate. If no trusted developer certificate is found, Aspire automatically falls back to the ephemeral certificate. For more information, see [Certificate configuration](/app-host/certificate-configuration/). | | `ASPIRE_ENVIRONMENT` | `null` | Configures the AppHost environment when no higher-priority environment source is set. If no environment is configured, the AppHost uses `Production`. | | `ASPIRE_VERSION_CHECK_DISABLED` | `false` | When set to `true`, Aspire doesn’t check for newer versions on startup. | ## AppHost environment [Section titled “AppHost environment”](#apphost-environment) Use `ASPIRE_ENVIRONMENT` to set the environment name used by the AppHost while it evaluates the application model. Precedence is `--environment`, `DOTNET_ENVIRONMENT`, `ASPIRE_ENVIRONMENT`, then `Production`. This doesn’t configure the dashboard’s `ASPNETCORE_ENVIRONMENT` or automatically flow to child resources; set framework-specific variables on resources as needed. For details, see [Aspire environments](/deployment/environments/). ## Version update notifications [Section titled “Version update notifications”](#version-update-notifications) When an Aspire app starts, it checks if a newer version of Aspire is available on NuGet. If a new version is found, a notification appears in the dashboard with the latest version number, [a link to upgrade instructions](https://aka.ms/dotnet/aspire/update-latest), and button to ignore that version in the future. ![Screenshot of dashboard showing a version update notification with upgrade options.](/_astro/dashboard-update-notification.CbuDufvf_Z1d7vOO.webp) The version check runs only when: * The dashboard is enabled (interaction service is available). * At least 2 days have passed since the last check. * The check hasn’t been disabled via the `ASPIRE_VERSION_CHECK_DISABLED` configuration setting. * The app is not running in publish mode. Updates are manual. You need to edit your project file to upgrade the Aspire SDK and package versions. ## Resource service [Section titled “Resource service”](#resource-service) A resource service is hosted by the AppHost. The resource service is used by the dashboard to fetch information about resources which are being orchestrated by Aspire. | Option | Default value | Description | | ----------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL` | `null` | Configures the address of the resource service hosted by the AppHost. Automatically generated with *launchSettings.json* to have a random port on localhost. For example, `https://localhost:17037`. | | `ASPIRE_DASHBOARD_RESOURCESERVICE_APIKEY` | Automatically generated 128-bit entropy token. | The API key used to authenticate requests made to the AppHost’s resource service. The API key is required if the AppHost is in run mode, the dashboard isn’t disabled, and the dashboard isn’t configured to allow anonymous access with `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS`. | ## Dashboard [Section titled “Dashboard”](#dashboard) By default, the dashboard is automatically started by the AppHost. The dashboard supports [its own set of configuration](/dashboard/configuration/), and some settings can be configured from the AppHost. | Option | Default value | Description | | -------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ASPNETCORE_URLS` | `null` | Dashboard address. Must be `https` unless `ASPIRE_ALLOW_UNSECURED_TRANSPORT` or `DistributedApplicationOptions.AllowUnsecuredTransport` is true. Automatically generated with *launchSettings.json* to have a random port on localhost. The value in launch settings is set on the `applicationUrls` property. | | `ASPNETCORE_ENVIRONMENT` | `Production` | Configures the environment the dashboard runs as. For more information, see [Use multiple environments in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/environments). | | `ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL` | `http://localhost:18889` if no gRPC endpoint is configured. | Configures the dashboard OTLP gRPC address. Used by the dashboard to receive telemetry over OTLP. Set on resources as the `OTEL_EXPORTER_OTLP_ENDPOINT` env var. The `OTEL_EXPORTER_OTLP_PROTOCOL` env var is `grpc`. Automatically generated with *launchSettings.json* to have a random port on localhost. | | `ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL` | `null` | Configures the dashboard OTLP HTTP address. Used by the dashboard to receive telemetry over OTLP. If only `ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL` is configured then it is set on resources as the `OTEL_EXPORTER_OTLP_ENDPOINT` env var. The `OTEL_EXPORTER_OTLP_PROTOCOL` env var is `http/protobuf`. | | `ASPIRE_DASHBOARD_CORS_ALLOWED_ORIGINS` | `null` | Overrides the CORS allowed origins configured in the dashboard. This setting replaces the default behavior of calculating allowed origins based on resource endpoints. | | `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS` | `false` | Configures the dashboard to not use authentication and accept anonymous access. Sets frontend, OTLP, MCP, and API auth modes to `Unsecured`. See [Dashboard security considerations](/dashboard/security-considerations/#anonymous-access) for the security implications. | | `ASPIRE_DASHBOARD_FRONTEND_BROWSERTOKEN` | Automatically generated 128-bit entropy token. | Configures the frontend browser token. This is the value that must be entered to access the dashboard when the auth mode is BrowserToken. If no browser token is specified then a new token is generated each time the AppHost is launched. | | `ASPIRE_DASHBOARD_TELEMETRY_OPTOUT` | `false` | Configures the dashboard to never send [usage telemetry](/dashboard/microsoft-collected-dashboard-telemetry/). | | `ASPIRE_DASHBOARD_API_ENABLED` | `true` | Enables the dashboard [telemetry API](/dashboard/configuration/#api) (`/api/telemetry/*`) endpoints. The AppHost always sets this to `true`. | | `ASPIRE_DASHBOARD_FORWARDEDHEADERS_ENABLED` | `false` | Enables the Forwarded headers middleware that replaces the scheme and host values on the Request context with the values coming from the `X-Forwarded-Proto` and `X-Forwarded-Host` headers. | ## Internal [Section titled “Internal”](#internal) Internal settings are used by the AppHost and integrations. Internal settings aren’t designed to be configured directly. | Option | Default value | Description | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AppHost:Directory` | The content root if there’s no project. | Directory of the project where the AppHost is located. Accessible from the `IDistributedApplicationBuilder.AppHostDirectory`. | | `AppHost:Path` | The directory combined with the application name. | The path to the AppHost. It combines the directory with the application name. | | `AppHost:Sha256` | It is created from the AppHost name when the AppHost is in publish mode. Otherwise it is created from the AppHost path. | Hex encoded hash for the current application. The hash is based on the location of the app on the current machine so it is stable between launches of the AppHost. | | `AppHost:OtlpApiKey` | Automatically generated 128-bit entropy token. | The API key used to authenticate requests sent to the dashboard OTLP service. The value is present if needed: the AppHost is in run mode, the dashboard isn’t disabled, and the dashboard isn’t configured to allow anonymous access with `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS`. | | `AppHost:DashboardApiKey` | Automatically generated 128-bit entropy token. | The API key used to authenticate requests to the dashboard telemetry API. Also used as a fallback for MCP authentication if `AppHost:McpApiKey` is not set. The value is present if needed: the AppHost is in run mode, the dashboard isn’t disabled, and the dashboard isn’t configured to allow anonymous access. | | `AppHost:BrowserToken` | Automatically generated 128-bit entropy token. | The browser token used to authenticate browsing to the dashboard when it is launched by the AppHost. The browser token can be set by `ASPIRE_DASHBOARD_FRONTEND_BROWSERTOKEN`. The value is present if needed: the AppHost is in run mode, the dashboard isn’t disabled, and the dashboard isn’t configured to allow anonymous access with `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS`. | | `AppHost:ResourceService:AuthMode` | `ApiKey`. If `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS` is true then the value is `Unsecured`. | The authentication mode used to access the resource service. The value is present if needed: the AppHost is in run mode and the dashboard isn’t disabled. | | `AppHost:ResourceService:ApiKey` | Automatically generated 128-bit entropy token. | The API key used to authenticate requests made to the AppHost’s resource service. The API key can be set by `ASPIRE_DASHBOARD_RESOURCESERVICE_APIKEY`. The value is present if needed: the AppHost is in run mode, the dashboard isn’t disabled, and the dashboard isn’t configured to allow anonymous access with `ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS`. | ## Advanced orchestration properties [Section titled “Advanced orchestration properties”](#advanced-orchestration-properties) Caution The following MSBuild properties and command-line arguments are intended for internal use by the Aspire SDK and tooling. You should not need to set these manually under normal circumstances. Incorrectly configuring these values can break your AppHost. ### DcpCliPath [Section titled “DcpCliPath”](#dcpclipath) The `DcpCliPath` property specifies the path to the **Developer Control Plane (DCP)** executable. The DCP is the core orchestration engine that Aspire uses to run and manage distributed application resources locally during development. #### How it works [Section titled “How it works”](#how-it-works) When you use the [Aspire SDK](/get-started/aspire-sdk/), the build system automatically: 1. Imports the platform-specific `Aspire.Hosting.Orchestration` NuGet package (for example, `Aspire.Hosting.Orchestration.win-x64`). 2. Sets `DcpCliPath` to point to the `dcp` executable within that package. 3. Embeds this path as assembly metadata in your compiled AppHost. At runtime, the AppHost reads this metadata to locate and start the DCP process, which then orchestrates your application’s resources. #### Override options [Section titled “Override options”](#override-options) In rare cases, you may need to override the default DCP path: | Method | Example | | --------------------- | --------------------------------------------- | | MSBuild property | `C:\path\to\dcp.exe` | | Command-line argument | `--dcp-cli-path /path/to/dcp` | | Configuration | `DcpPublisher:CliPath` | #### When to use [Section titled “When to use”](#when-to-use) You might override `DcpCliPath` in these scenarios: * **Aspire contributors**: Testing with a custom or debug build of DCP when developing Aspire itself. * **CI/CD pipelines**: Non-standard SDK layouts where automatic discovery doesn’t work. * **Troubleshooting**: Temporarily pointing to a specific DCP version to diagnose issues. Tip If your AppHost fails to start with an error about missing DCP or orchestration dependencies, ensure you have the [Aspire SDK](/get-started/aspire-sdk/) properly configured rather than manually setting `DcpCliPath`. # Container files > Inject files and directories into Aspire container resources at development and publish time using WithContainerFiles, with options for source paths and permissions. Aspire provides APIs to inject files and directories into containers, enabling you to configure containerized resources with custom configuration files, scripts, certificates, and other assets. There are two complementary APIs: * **`WithContainerFiles`**: Injects files into containers at development time when they start during `aspire run`. * **`PublishWithContainerFiles`**: Copies files from one resource’s container into another resource’s container as build artifacts at publish time during `aspire publish`. ## Inject files at development time [Section titled “Inject files at development time”](#inject-files-at-development-time) The `WithContainerFiles` extension method creates or updates files and directories inside a container at a specified destination path. It supports three approaches depending on your needs: inline entries for declarative file definitions, a source path for copying from the host file system, and an async callback for dynamic file generation. Caution `WithContainerFiles` is primarily intended for development-time configuration and is not supported at publish time. To inject files into containers during publish, use [`PublishWithContainerFiles`](#inject-files-at-publish-time) instead. ### Inline entries [Section titled “Inline entries”](#inline-entries) Use the inline entries overload to declaratively define files and directories using `ContainerFileSystemItem` objects. This is useful when file contents are known at build time or can be expressed as string literals. * TypeScript Note The `withContainerFiles` API is not yet available in the TypeScript AppHost SDK. * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddContainer("myapp", "myapp:latest") .WithContainerFiles("/app/config", [ new ContainerFile { Name = "appsettings.json", Contents = """ { "Logging": { "LogLevel": { "Default": "Information" } } } """ }, new ContainerDirectory { Name = "scripts", Entries = [ new ContainerFile { Name = "init.sh", Contents = "#!/bin/bash\necho 'Initializing...'", Mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute } ] } ]); builder.Build().Run(); ``` In the preceding example: * A JSON configuration file is created at `/app/config/appsettings.json` with the specified contents. * A nested `scripts` directory is created at `/app/config/scripts/` containing an executable shell script. ### Source path [Section titled “Source path”](#source-path) Use the source path overload to copy files from a directory on the host machine into the container. This is useful when you have existing configuration files or assets on disk. * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const frontend = await builder.addViteApp("frontend", "../frontend"); await frontend.withContainerFilesSource("/app/dist"); const api = await builder.addProject("api", "../Api/Api.csproj"); await api.publishWithContainerFiles(frontend, "./wwwroot"); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddContainer("myapp", "myapp:latest") .WithContainerFiles("/app/config", "./config-files"); builder.Build().Run(); ``` Unless the source path is a rooted (absolute) path, it’s interpreted as relative to the AppHost project directory. All files in the source directory are copied to the destination path in the container at startup. ### Async callback [Section titled “Async callback”](#async-callback) Use the callback overload to generate files dynamically when the container starts. The callback receives a `ContainerFileSystemCallbackContext` that provides access to the `IServiceProvider` and the resource’s `IResource` model, enabling you to resolve services or inspect the app model. * TypeScript Note The `withContainerFiles` API is not yet available in the TypeScript AppHost SDK. * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddContainer("worker", "worker:latest") .WithContainerFiles("/app/config", async (context, cancellationToken) => { var config = new { MachineName = Environment.MachineName, Timestamp = DateTime.UtcNow }; return [ new ContainerFile { Name = "runtime-config.json", Contents = JsonSerializer.Serialize(config) } ]; }); builder.Build().Run(); ``` The callback is invoked each time the container starts, so the generated files always reflect the current state. ## File types [Section titled “File types”](#file-types) The `WithContainerFiles` API uses a type hierarchy rooted at the abstract `ContainerFileSystemItem` class. Each type represents a different kind of file system entry. ### ContainerFile [Section titled “ContainerFile”](#containerfile) `ContainerFile` represents a standard file. Set either `Contents` (a string) or `SourcePath` (an absolute path on the host) to provide the file data — the two are mutually exclusive. AppHost.cs ```csharp // File with inline contents var configYaml = new ContainerFile { Name = "config.yaml", Contents = "key: value" }; // File sourced from the host file system var dataCsv = new ContainerFile { Name = "data.csv", SourcePath = "/path/to/data.csv" }; ``` Set `ContinueOnError` to `true` to allow the container to start even if creating this particular file fails: AppHost.cs ```csharp var optionalJson = new ContainerFile { Name = "optional-config.json", Contents = "{}", ContinueOnError = true }; ``` ### ContainerDirectory [Section titled “ContainerDirectory”](#containerdirectory) `ContainerDirectory` represents a directory that can contain nested `ContainerFileSystemItem` entries, allowing you to build arbitrary directory trees. AppHost.cs ```csharp var certsDir = new ContainerDirectory { Name = "certs", Entries = [ new ContainerFile { Name = "ca.pem", SourcePath = "/path/to/ca.pem" }, new ContainerDirectory { Name = "private", Entries = [ new ContainerFile { Name = "server.key", SourcePath = "/path/to/server.key", Mode = UnixFileMode.UserRead } ] } ] }; ``` You can also populate a `ContainerDirectory` from files on disk using the static `GetFileSystemItemsFromPath` method: AppHost.cs ```csharp var assetsDir = new ContainerDirectory { Name = "assets", Entries = ContainerDirectory.GetFileSystemItemsFromPath( "/path/to/assets", searchOptions: SearchOption.AllDirectories) }; ``` ### ContainerOpenSSLCertificateFile [Section titled “ContainerOpenSSLCertificateFile”](#containeropensslcertificatefile) `ContainerOpenSSLCertificateFile` represents a PEM-encoded public certificate. In addition to placing the certificate file in the container, Aspire automatically creates an OpenSSL-compatible symlink (`[subject hash].[n]`) in the same directory — equivalent to running `openssl rehash`. This enables containers that use OpenSSL for certificate validation to discover the certificate automatically. * TypeScript Note The `withContainerFiles` API is not yet available in the TypeScript AppHost SDK. * C# AppHost.cs ```csharp builder.AddContainer("myapp", "myapp:latest") .WithContainerFiles("/certs", [ new ContainerOpenSSLCertificateFile { Name = "ca-cert.pem", Contents = pemCertificateString } ]); ``` ## File permissions and ownership [Section titled “File permissions and ownership”](#file-permissions-and-ownership) All `WithContainerFiles` overloads accept optional parameters to control file ownership and permissions. ### Owner and group [Section titled “Owner and group”](#owner-and-group) The `defaultOwner` and `defaultGroup` parameters set the default UID and GID applied to all created files and directories. Both default to `0` (root) when not specified. You can override ownership on individual items using the `Owner` and `Group` properties on any `ContainerFileSystemItem`. * TypeScript Note The `withContainerFiles` API is not yet available in the TypeScript AppHost SDK. * C# AppHost.cs ```csharp builder.AddContainer("myapp", "myapp:latest") .WithContainerFiles("/app/data", [ new ContainerFile { Name = "shared.txt", Contents = "shared data" }, new ContainerFile { Name = "user-only.txt", Contents = "private data", Owner = 1000, Group = 1000 } ], defaultOwner: 33, // www-data defaultGroup: 33); ``` In this example, `shared.txt` inherits the default owner/group of `33`, while `user-only.txt` overrides with UID/GID `1000`. ### Umask [Section titled “Umask”](#umask) The `umask` parameter controls default permissions by subtracting (masking) permission bits from the base defaults. Without an explicit `Mode` set on an item: * **Directories** start with `0777` (read/write/execute for all) and have the umask subtracted * **Files** start with `0666` (read/write for all) and have the umask subtracted The default umask is `0022`, which results in: * Directories: `0755` (owner: rwx, group: r-x, others: r-x) * Files: `0644` (owner: rw-, group: r—, others: r—) You can set `Mode` directly on individual items to override the umask-based default: * TypeScript Note The `withContainerFiles` API is not yet available in the TypeScript AppHost SDK. * C# AppHost.cs ```csharp builder.AddContainer("myapp", "myapp:latest") .WithContainerFiles("/app/scripts", [ new ContainerFile { Name = "run.sh", Contents = "#!/bin/bash\necho 'Running'", Mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute } ], umask: UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute); ``` ### Persistent containers [Section titled “Persistent containers”](#persistent-containers) For containers with `ContainerLifetime.Persistent`, changing the contents of container file entries causes the container to be recreated. Ensure any data written through `WithContainerFiles` is idempotent for a given app model configuration to avoid unintended container restarts. ## Inject files at publish time [Section titled “Inject files at publish time”](#inject-files-at-publish-time) The `PublishWithContainerFiles` method copies files from one resource’s container into another resource’s container during `aspire publish`. This is the preferred approach for injecting files into containers at publish time. A key use case is embedding single-page application (SPA) or static JavaScript frontends into a reverse proxy or web server container for production deployment. During development, frontend apps like Vite or React typically run as standalone dev servers. In production, however, the compiled static assets are often served by a backend API or a dedicated web server like Nginx. `PublishWithContainerFiles` bridges this gap by copying the built frontend output into the serving container as part of the publish process — no manual file copying or multi-stage Dockerfile required. ### Embed a frontend in a backend [Section titled “Embed a frontend in a backend”](#embed-a-frontend-in-a-backend) * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const frontend = await builder.addViteApp("frontend", "../frontend"); const api = await builder.addProject("api", "./Api/Api.csproj") .publishWithContainerFiles(frontend, "./wwwroot"); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var frontend = builder.AddViteApp("frontend", "../frontend"); var api = builder.AddProject("api") .PublishWithContainerFiles(frontend, "./wwwroot"); builder.Build().Run(); ``` In this example: 1. The `frontend` resource builds inside its container, producing compiled JavaScript, CSS, and HTML. 2. During publish, Aspire copies those files from the `frontend` container into the `api` container at `./wwwroot`. 3. The resulting `api` container includes both the API code and the frontend static assets, ready to serve the full application. ### Serve a frontend from YARP [Section titled “Serve a frontend from YARP”](#serve-a-frontend-from-yarp) You can also embed frontend assets into a dedicated reverse proxy container: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const frontend = await builder.addViteApp("frontend", "../frontend"); const nginx = await builder.addYarp("gateway") .publishWithStaticFiles(frontend); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var frontend = builder.AddViteApp("frontend", "../frontend"); var nginx = builder.AddYarp("gateway") .PublishWithStaticFiles(frontend); builder.Build().Run(); ``` This produces a self-contained Nginx container that serves the frontend application, with no external volume mounts or runtime file copying needed. `PublishWithContainerFiles` only applies in publish mode — it has no effect during `aspire run`. The destination resource must implement `IContainerFilesDestinationResource` (such as `ProjectResource`), and the source resource must implement `IResourceWithContainerFiles`. ### Customize the source path [Section titled “Customize the source path”](#customize-the-source-path) By default, the source resource exports files from its container based on its configured output paths. Use `WithContainerFilesSource` to specify which path inside the source container to copy from: * TypeScript Note The `withContainerFiles` API is not yet available in the TypeScript AppHost SDK. * C# AppHost.cs ```csharp var frontend = builder.AddViteApp("frontend", "../frontend") .WithContainerFilesSource("/app/dist"); var api = builder.AddProject("api") .PublishWithContainerFiles(frontend, "./wwwroot"); ``` Use `ClearContainerFilesSources` to remove any previously configured source paths before adding new ones. ## See also [Section titled “See also”](#see-also) * [Certificate configuration](/app-host/certificate-configuration/) * [Add Dockerfiles to your app model](/app-host/withdockerfile/) * [Resource lifetimes](/app-host/resource-lifetimes/) # Container registry configuration > Configure container registries for Aspire — generic registries, Docker Hub, Azure Container Registry, GitHub Container Registry, and per-resource image tagging. Aspire 13.1 introduced explicit container registry configuration, giving developers control over where and when container images are pushed during deployment. This article explains how to configure container registries for your Aspire applications. ## Container registry configuration [Section titled “Container registry configuration”](#container-registry-configuration) When deploying Aspire applications to production environments, your containerized services need to be pushed to a container registry. Prior to Aspire 13.1, registry configuration was often implicit, making it difficult to control and understand the deployment process. The new `ContainerRegistryResource` provides explicit configuration for: * **Generic container registries** — DockerHub, GitHub Container Registry (GHCR), Harbor, or any Docker-compatible registry * **Azure Container Registry** — First-class support with automatic credential management * **Pipeline integration** — Control when images are built and pushed using `aspire do push` * **Authentication** — Configure registry credentials securely ## Generic container registries [Section titled “Generic container registries”](#generic-container-registries) Caution This API is experimental and may change in future releases. Use diagnostic code `ASPIRECOMPUTE003` to suppress the experimental warning. For more information, see [ASPIRECOMPUTE003](/diagnostics/aspirecompute003/). Use the `AddContainerRegistry` method to configure a generic container registry for your application. This works with any Docker-compatible registry including DockerHub, GitHub Container Registry, Harbor, and private registries. ### Basic usage [Section titled “Basic usage”](#basic-usage) The following example configures a container registry and associates it with a project resource: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Add a container registry const registry = await builder.addContainerRegistry("myregistry", "registry.example.com"); // Associate the registry with a project const api = await builder.addProject("api", "../Api/Api.csproj"); await api.withContainerRegistry(registry); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Add a container registry var registry = builder.AddContainerRegistry("myregistry", "registry.example.com"); // Associate the registry with a project var api = builder.AddProject("api") .WithContainerRegistry(registry); builder.Build().Run(); ``` The preceding code: * Creates a container registry resource pointing to `registry.example.com`. * Associates the registry with the `api` project. * When deploying, the `api` project will be built as a container image and pushed to the specified registry. ### DockerHub example [Section titled “DockerHub example”](#dockerhub-example) To push images to DockerHub, specify `docker.io` as the registry endpoint: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const registry = await builder.addContainerRegistry("dockerhub", "docker.io"); const api = await builder.addProject("api", "../Api/Api.csproj"); await api.withContainerRegistry(registry); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var registry = builder.AddContainerRegistry("dockerhub", "docker.io"); var api = builder.AddProject("api") .WithContainerRegistry(registry); ``` ### GitHub Container Registry example [Section titled “GitHub Container Registry example”](#github-container-registry-example) To push images to GitHub Container Registry (GHCR): * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const registry = await builder.addContainerRegistry("ghcr", "ghcr.io"); const api = await builder.addProject("api", "../Api/Api.csproj"); await api.withContainerRegistry(registry); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var registry = builder.AddContainerRegistry("ghcr", "ghcr.io"); var api = builder.AddProject("api") .WithContainerRegistry(registry); ``` ### Private registry example [Section titled “Private registry example”](#private-registry-example) For private registries, provide the full registry URL: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const registry = await builder.addContainerRegistry( "private-registry", "registry.mycompany.com:5000"); const api = await builder.addProject("api", "../Api/Api.csproj"); await api.withContainerRegistry(registry); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var registry = builder.AddContainerRegistry( "private-registry", "registry.mycompany.com:5000"); var api = builder.AddProject("api") .WithContainerRegistry(registry); ``` ## Authentication and credentials [Section titled “Authentication and credentials”](#authentication-and-credentials) Container registries typically require authentication for pushing images. You can configure credentials using parameters and secrets. ### Using parameters for registry credentials [Section titled “Using parameters for registry credentials”](#using-parameters-for-registry-credentials) Parameters allow you to provide registry configuration dynamically: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const registryEndpoint = await builder.addParameter("registry-endpoint"); const registryRepository = await builder.addParameter("registry-repository"); const registry = await builder.addContainerRegistry( "myregistry", registryEndpoint, registryRepository); const api = await builder.addProject("api", "../Api/Api.csproj"); await api.withContainerRegistry(registry); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var registryEndpoint = builder.AddParameter("registry-endpoint"); var registryRepository = builder.AddParameter("registry-repository"); var registry = builder.AddContainerRegistry( "myregistry", registryEndpoint, registryRepository); var api = builder.AddProject("api") .WithContainerRegistry(registry); ``` For more information about parameters, see [External parameters](/fundamentals/external-parameters/). ### Configuring credentials [Section titled “Configuring credentials”](#configuring-credentials) Registry credentials should be configured through your deployment environment: #### Docker login [Section titled “Docker login”](#docker-login) Before pushing images, ensure you’re authenticated with the registry: Docker login ```bash docker login registry.example.com ``` For DockerHub: DockerHub login ```bash docker login docker.io -u username ``` For GitHub Container Registry: GHCR login ```bash echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin ``` #### CI/CD configuration [Section titled “CI/CD configuration”](#cicd-configuration) In CI/CD environments (GitHub Actions, Azure Pipelines, and so on), configure credentials using secrets: GitHub Actions example ```yaml - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Push images run: aspire do push ``` ## Azure Container Registry [Section titled “Azure Container Registry”](#azure-container-registry) Azure Container Registry (ACR) provides first-class integration with Aspire, with automatic credential management and parallel provisioning. ### Explicit ACR configuration [Section titled “Explicit ACR configuration”](#explicit-acr-configuration) Aspire 13.1 introduces explicit container registry configuration for Azure Container Apps environments: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const environment = await builder.addAzureContainerAppEnvironment("myenv"); const api = await builder.addProject("api", "../Api/Api.csproj"); await api.withContainerRegistry(environment); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var environment = builder.AddAzureContainerAppEnvironment("myenv"); var api = builder.AddProject("api") .WithContainerRegistry(environment); builder.Build().Run(); ``` In the preceding example: * The code creates an Azure Container Apps environment with an associated ACR. * The ACR is provisioned in parallel with that environment. * Images are pushed as soon as the registry is available. * Credentials are automatically managed through Azure authentication. Note Prior to Aspire 13.1, ACR was provisioned implicitly as part of the Container Apps environment. The explicit configuration provides better control and visibility into the deployment process. For more information, see [Azure Container Registry integration](/integrations/cloud/azure/azure-container-registry/azure-container-registry-get-started/). ### Using an existing ACR [Section titled “Using an existing ACR”](#using-an-existing-acr) To use an existing Azure Container Registry, call the `PublishAsExisting` method when you add the ACR: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const registryName = await builder.addParameter("registryName"); const rgName = await builder.addParameter("rgName"); const acr = await builder.addAzureContainerRegistry("my-acr"); await acr.publishAsExisting(registryName, rgName); const environment = await builder.addAzureContainerAppEnvironment("env"); await environment.withAzureContainerRegistry(acr); const api = await builder.addProject("api", "../Api/Api.csproj"); await api.withContainerRegistry(acr); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var registryName = builder.AddParameter("registryName"); var rgName = builder.AddParameter("rgName"); var acr = builder.AddAzureContainerRegistry("my-acr") .PublishAsExisting(registryName, rgName); builder.AddAzureContainerAppEnvironment("env") .WithAzureContainerRegistry(acr); var api = builder.AddProject("api") .WithContainerRegistry(acr); ``` ## Pipeline integration [Section titled “Pipeline integration”](#pipeline-integration) Aspire’s deployment pipeline includes a dedicated `push` step for pushing container images to registries. ### Using aspire do push [Section titled “Using aspire do push”](#using-aspire-do-push) The `aspire do push` command builds container images and pushes them to configured registries: Aspire CLI — Push images ```bash aspire do push ``` This command: 1. Builds all container images for compute resources 2. Tags images with the appropriate registry and repository names 3. Pushes images to their configured registries Example output: Output ```plaintext 16:03:38 (pipeline-execution) → Starting pipeline-execution... 16:03:38 (build-api) → Starting build-api... 16:03:43 (push-api) → Starting push-api... 16:03:43 (push-api) → Pushing api to container-registry 16:03:44 (push-api) i [INF] Docker tag for api -> docker.io/username/api:latest succeeded. 16:04:05 (push-api) i [INF] Docker push for docker.io/username/api:latest succeeded. 16:04:05 (push-api) ✓ Successfully pushed api to docker.io/username/api:latest (21.3s) 16:04:05 (push-api) ✓ push-api completed successfully ``` For more information about pipeline commands, see [`aspire do` command](/reference/cli/commands/aspire-do/). ### Pipeline step dependencies [Section titled “Pipeline step dependencies”](#pipeline-step-dependencies) The `push` step automatically handles dependencies: * **`build-prereq`** — Ensures prerequisites are met before building * **`build-`** — Builds container images for each resource * **`push-`** — Pushes images to registries You can execute individual steps or the entire pipeline: Aspire CLI — Build only ```bash aspire do build ``` Aspire CLI — Full deployment ```bash aspire do deploy ``` The `deploy` step includes building, pushing, and deploying all resources. ## Complete examples [Section titled “Complete examples”](#complete-examples) ### Multi-registry deployment [Section titled “Multi-registry deployment”](#multi-registry-deployment) You can configure different registries for different resources: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const publicRegistry = await builder.addContainerRegistry("dockerhub", "docker.io"); const privateRegistry = await builder.addContainerRegistry( "private", "registry.company.com"); const publicApi = await builder.addProject("public-api", "../PublicApi/PublicApi.csproj"); await publicApi.withContainerRegistry(publicRegistry); const internalApi = await builder.addProject("internal-api", "../InternalApi/InternalApi.csproj"); await internalApi.withContainerRegistry(privateRegistry); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var publicRegistry = builder.AddContainerRegistry("dockerhub", "docker.io"); var privateRegistry = builder.AddContainerRegistry( "private", "registry.company.com"); var publicApi = builder.AddProject("public-api") .WithContainerRegistry(publicRegistry); var internalApi = builder.AddProject("internal-api") .WithContainerRegistry(privateRegistry); ``` ### Parameterized registry configuration [Section titled “Parameterized registry configuration”](#parameterized-registry-configuration) You can use parameters when you need flexible deployment across environments: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const registryEndpoint = await builder.addParameter("registry-endpoint"); const registryRepository = await builder.addParameter("registry-repository"); const registry = await builder.addContainerRegistry( "container-registry", registryEndpoint, registryRepository); const api = await builder.addProject("api", "../Api/Api.csproj"); await api.withContainerRegistry(registry); const worker = await builder.addProject("worker", "../Worker/Worker.csproj"); await worker.withContainerRegistry(registry); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var registryEndpoint = builder.AddParameter("registry-endpoint"); var registryRepository = builder.AddParameter("registry-repository"); var registry = builder.AddContainerRegistry( "container-registry", registryEndpoint, registryRepository); var api = builder.AddProject("api") .WithContainerRegistry(registry); var worker = builder.AddProject("worker") .WithContainerRegistry(registry); builder.Build().Run(); ``` Configure the parameters in your AppHost configuration: appsettings.json ```json { "Parameters": { "registry-endpoint": "ghcr.io", "registry-repository": "myorg" } } ``` Alternatively, use environment variables to configure them: Environment variables ```bash export Parameters__registry_endpoint="ghcr.io" export Parameters__registry_repository="myorg" ``` ### Azure deployment with ACR [Section titled “Azure deployment with ACR”](#azure-deployment-with-acr) The following code constitutes a complete AppHost example with Azure Container Apps and ACR: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Create Azure Container Apps environment with ACR const environment = await builder.addAzureContainerAppEnvironment("production"); // Add Redis cache const cache = await builder.addRedis("cache"); // Add API with registry configuration const api = await builder.addProject("api", "../Api/Api.csproj"); await api.withReference(cache); await api.withContainerRegistry(environment); // Add frontend with registry configuration const web = await builder.addProject("web", "../Web/Web.csproj"); await web.withReference(api); await web.withContainerRegistry(environment); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Create Azure Container Apps environment with ACR var acaEnv = builder.AddAzureContainerAppEnvironment("production"); // Add Redis cache var cache = builder.AddRedis("cache"); // Add API with registry configuration var api = builder.AddProject("api") .WithReference(cache) .WithContainerRegistry(acaEnv); // Add frontend with registry configuration var web = builder.AddProject("web") .WithReference(api) .WithContainerRegistry(acaEnv); builder.Build().Run(); ``` ## Benefits of explicit configuration [Section titled “Benefits of explicit configuration”](#benefits-of-explicit-configuration) The explicit container registry configuration introduced in Aspire 13.1 provides several benefits: * **Visibility** — Clear understanding of where images are pushed * **Control** — Explicit configuration of registry endpoints and credentials * **Parallelization** — Registry provisioning happens in parallel with other resources * **Early feedback** — Faster deployments with images pushing as soon as registries are ready * **Flexibility** — Support for any Docker-compatible registry For a deeper dive into container registry improvements, see [Safia Abdalla’s blog post on fixing Aspire’s image problem](https://blog.safia.rocks/2025/12/15/aspire-image-push/). ## See also [Section titled “See also”](#see-also) * [Azure Container Registry integration](/integrations/cloud/azure/azure-container-registry/azure-container-registry-get-started/) * [Configure Azure Container Apps environments](/integrations/cloud/azure/configure-container-apps/) * [`aspire do` command](/reference/cli/commands/aspire-do/) * [External parameters](/fundamentals/external-parameters/) * [Pipelines and app topology](/deployment/pipelines/) # Docker Compose to Aspire AppHost reference > Quick reference for converting Docker Compose YAML syntax to Aspire AppHost API calls — services, networks, volumes, environment variables, and health checks. This reference provides systematic mappings from Docker Compose YAML syntax to equivalent Aspire AppHost API calls. Use these tables as a quick reference when converting your existing Docker Compose files to Aspire application host configurations. ## Service definitions [Section titled “Service definitions”](#service-definitions) | Docker Compose | Aspire | Notes | | --------------- | ---------------------------------------------------------- | ------------------------------------------------------------------- | | `services:` | `var builder = DistributedApplication.CreateBuilder(args)` | Root application builder used for adding and representing resources | | `service_name:` | `builder.Add*("service_name")` | Service name becomes resource name | Learn more about [Docker Compose services](https://docs.docker.com/compose/compose-file/05-services/). ## Images and builds [Section titled “Images and builds”](#images-and-builds) | Docker Compose | Aspire | Notes | | ------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------- | | `image: nginx:latest` | `builder.AddContainer("name", "nginx", "latest")` | Direct image reference | | `build: .` | `builder.AddDockerfile("name", ".")` | Build from Dockerfile | | `build: ./path` | `builder.AddDockerfile("name", "./path")` | Build from specific path | | `build.context: ./app` | `builder.AddDockerfile("name", "./app")` | Build context | | `build.dockerfile: Custom.dockerfile` | `builder.Add*("name").WithDockerfile("Custom.dockerfile")` | Custom Dockerfile name | | Generated Dockerfile | `builder.AddDockerfileBuilder("name", "./app", callback, stage: "runtime")` | Generate the Dockerfile from AppHost code | Learn more about [Docker Compose build reference](https://docs.docker.com/compose/compose-file/build/) and [WithDockerfile](/app-host/withdockerfile/). ## Pull policy [Section titled “Pull policy”](#pull-policy) | Docker Compose | Aspire | Notes | | ---------------------- | ----------------------------------------------- | -------------------------------- | | `pull_policy: always` | `.WithImagePullPolicy(ImagePullPolicy.Always)` | Always pull the image | | `pull_policy: missing` | `.WithImagePullPolicy(ImagePullPolicy.Missing)` | Pull only if not present locally | | `pull_policy: never` | `.WithImagePullPolicy(ImagePullPolicy.Never)` | Never pull from registry | Learn more about [Docker Compose pull\_policy](https://docs.docker.com/reference/compose-file/services/#pull_policy) and [image pull policy](/integrations/compute/docker/#configure-image-pull-policy). ## .NET projects [Section titled “.NET projects”](#net-projects) | Docker Compose | Aspire | Notes | | --------------------------- | --------------------------------------------- | ----------------------------- | | `build: ./MyApi` (for .NET) | `builder.AddProject("myapi")` | Direct .NET project reference | Learn more about [adding .NET projects](/get-started/app-host/). ## Port mappings [Section titled “Port mappings”](#port-mappings) | Docker Compose | Aspire | Notes | | -------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------- | | `ports: ["8080:80"]` | `.WithHttpEndpoint(port: 8080, targetPort: 80)` | HTTP endpoint mapping. Ports are optional; dynamic ports are used if omitted | | `ports: ["443:443"]` | `.WithHttpsEndpoint(port: 443, targetPort: 443)` | HTTPS endpoint mapping. Ports are optional; dynamic ports are used if omitted | | `expose: ["8080"]` | `.WithEndpoint(port: 8080)` | Internal port exposure. Ports are optional; dynamic ports are used if omitted | Learn more about [Docker Compose ports](https://docs.docker.com/compose/compose-file/05-services/#ports) and [endpoint configuration](/fundamentals/networking-overview/). ## Environment variables [Section titled “Environment variables”](#environment-variables) | Docker Compose | Aspire / Notes | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | `environment: KEY=value` | `.WithEnvironment("KEY", "value")` Static environment variable | | `environment: KEY=${HOST_VAR}` | `.WithEnvironment(context => context.EnvironmentVariables["KEY"] = hostVar)` Environment variable with callback context | | `environment: KEY=${PARAM}` | `.AsEnvironmentPlaceholder(resource)` inside `.PublishAsDockerComposeService(...)` Compose environment variable placeholder | | `env_file: .env` | `.ConfigureEnvFile(env => { ... })` Environment file customization (available in 13.1+) | Learn more about [Docker Compose environment](https://docs.docker.com/compose/compose-file/05-services/#environment) and [external parameters](/fundamentals/external-parameters/). ## Volumes and storage [Section titled “Volumes and storage”](#volumes-and-storage) | Docker Compose | Aspire | Notes | | -------------------------------- | ------------------------------------------------------ | -------------------- | | `volumes: ["data:/app/data"]` | `.WithVolume("data", "/app/data")` | Named volume | | `volumes: ["./host:/container"]` | `.WithBindMount("./host", "/container")` | Bind mount | | `volumes: ["./config:/app:ro"]` | `.WithBindMount("./config", "/app", isReadOnly: true)` | Read-only bind mount | Learn more about [Docker Compose volumes](https://docs.docker.com/compose/compose-file/05-services/#volumes) and [persist container data](/fundamentals/persist-data-volumes/). ## Dependencies and ordering [Section titled “Dependencies and ordering”](#dependencies-and-ordering) | Docker Compose | Aspire | Notes | | -------------------------------------------- | ------------------------ | --------------------------------------------------- | | `depends_on: [db]` | `.WithReference(db)` | Service dependency with connection string injection | | `depends_on: db: condition: service_started` | `.WaitFor(db)` | Wait for service start | | `depends_on: db: condition: service_healthy` | `.WaitForCompletion(db)` | Wait for health check to pass | Learn more about [Docker Compose depends\_on](https://docs.docker.com/compose/compose-file/05-services/#depends_on) and [launch profiles](/integrations/dotnet/launch-profiles/). ## Networks [Section titled “Networks”](#networks) | Docker Compose | Aspire | Notes | | -------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------- | | `networks: [backend]` | Automatic | Aspire handles networking automatically | | Service host name on a Compose network | `.GetHostAddressExpression(endpoint)` | Produces the generated Docker Compose service host name for an endpoint | | Custom networks | `.ConfigureComposeFile(file => { ... })` | Customize the generated Compose file when automatic networking isn’t enough | Learn more about [Docker Compose networks](https://docs.docker.com/compose/compose-file/05-services/#networks) and [service discovery](/fundamentals/service-discovery/). ## Resource limits [Section titled “Resource limits”](#resource-limits) | Docker Compose | Aspire | Notes | | -------------------------------------- | ------------- | ------------------------------------------ | | `deploy.resources.limits.memory: 512m` | Not supported | Resource limits aren’t supported in Aspire | | `deploy.resources.limits.cpus: 0.5` | Not supported | Resource limits aren’t supported in Aspire | Learn more about [Docker Compose deploy reference](https://docs.docker.com/compose/compose-file/deploy/). ## Health checks [Section titled “Health checks”](#health-checks) | Docker Compose | Aspire | Notes | | -------------------------------------------------------------- | --------------------------- | -------------------------------------------------- | | `healthcheck.test: ["CMD", "curl", "http://localhost/health"]` | Built-in for integrations | Aspire integrations include health checks | | `healthcheck.interval: 30s` | Configurable in integration | Health check configuration varies by resource type | Learn more about [Docker Compose healthcheck](https://docs.docker.com/compose/compose-file/05-services/#healthcheck) and [health checks](/fundamentals/health-checks/). ## Restart policies [Section titled “Restart policies”](#restart-policies) | Docker Compose | Aspire | Notes | | ------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------- | | `restart: unless-stopped` | `.PublishAsDockerComposeService((resource, service) => service.Restart = "unless-stopped")` | Customize the generated Docker Compose service | | `restart: always` | `.PublishAsDockerComposeService((resource, service) => service.Restart = "always")` | Customize the generated Docker Compose service | | `restart: no` | Default | No restart policy | Learn more about [Docker Compose restart](https://docs.docker.com/compose/compose-file/05-services/#restart). ## Logging [Section titled “Logging”](#logging) | Docker Compose | Aspire | Notes | | ------------------------------- | ----------------------- | ---------------------------------- | | `logging.driver: json-file` | Built-in | Aspire provides integrated logging | | `logging.options.max-size: 10m` | Dashboard configuration | Managed through Aspire dashboard | Learn more about [Docker Compose logging](https://docs.docker.com/compose/compose-file/05-services/#logging) and [telemetry](/fundamentals/telemetry/). ## Database services [Section titled “Database services”](#database-services) | Docker Compose | Aspire | Notes | | --------------------- | ----------------------------- | --------------------------------------- | | `image: postgres:15` | `builder.AddPostgres("name")` | PostgreSQL with automatic configuration | | `image: mysql:8` | `builder.AddMySql("name")` | MySQL with automatic configuration | | `image: redis:7` | `builder.AddRedis("name")` | Redis with automatic configuration | | `image: mongo:latest` | `builder.AddMongoDB("name")` | MongoDB with automatic configuration | Learn more about [Docker Compose services](https://docs.docker.com/compose/compose-file/05-services/) and [database integrations](/integrations/gallery/?search=database). ## See also [Section titled “See also”](#see-also) * [Migrate from Docker Compose to Aspire](/app-host/migrate-from-docker-compose/) * [AppHost overview](/get-started/app-host/) * [WithDockerfile](/app-host/withdockerfile/) # AppHost eventing APIs > Use the Aspire AppHost eventing APIs for lifecycle events, custom event publishing, and reactive integrations that respond to resource state transitions at runtime. In Aspire, eventing allows you to publish and subscribe to events during various AppHost life cycles. Eventing is more flexible than life cycle events. Both let you run arbitrary code during event callbacks, but eventing offers finer control of event timing, publishing, and provides supports for custom events. ## AppHost eventing [Section titled “AppHost eventing”](#apphost-eventing) The following events are available in the AppHost and occur in the following order: 1. `BeforeStartEvent`: This event is raised before the AppHost starts. 2. `ResourceEndpointsAllocatedEvent`: This event is raised per resource after its endpoints are allocated. 3. `AfterResourcesCreatedEvent`: This event is raised after resources are created. ### Subscribe to AppHost events [Section titled “Subscribe to AppHost events”](#subscribe-to-apphost-events) To subscribe to built-in AppHost events, use the typed API available for each event. C# provides builder extension methods for selected events and the lower-level `Eventing.Subscribe()` API for others. TypeScript provides named subscription methods: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); await builder.subscribeBeforeStart(async () => { console.log('BeforeStartEvent'); }); await builder.subscribeAfterResourcesCreated(async () => { console.log('AfterResourcesCreatedEvent'); }); await builder.build().run(); ``` * C# AppHost.cs ```csharp using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; var builder = DistributedApplication.CreateBuilder(args); builder.OnBeforeStart(static (@event, cancellationToken) => { var logger = @event.Services.GetRequiredService>(); logger.LogInformation("BeforeStartEvent"); return Task.CompletedTask; }); builder.Eventing.Subscribe( static (@event, cancellationToken) => { var logger = @event.Services.GetRequiredService>(); logger.LogInformation("AfterResourcesCreatedEvent"); return Task.CompletedTask; }); builder.Build().Run(); ``` The following C# builder-level helper methods are available for AppHost events: | Method | Event | | ----------------- | ---------------------------------------------------------------- | | `OnBeforeStart` | `BeforeStartEvent` — raised before the AppHost starts | | `OnBeforePublish` | `BeforePublishEvent` — raised before manifest publishing begins | | `OnAfterPublish` | `AfterPublishEvent` — raised after manifest publishing completes | For the full API surface, see the [C# `DistributedApplicationEventingExtensions` API reference](/reference/api/csharp/aspire.hosting/distributedapplicationeventingextensions/) and the [TypeScript `Aspire.Hosting` API reference](/reference/api/typescript/aspire.hosting/). If you need to subscribe via `IDistributedApplicationEventing` directly (for example, inside an `IDistributedApplicationEventingSubscriber`), you can use the lower-level `Eventing.Subscribe()` API: AppHost.cs ```csharp builder.Eventing.Subscribe( static (@event, cancellationToken) => { var logger = @event.Services.GetRequiredService>(); logger.LogInformation("BeforeStartEvent"); return Task.CompletedTask; }); builder.Eventing.Subscribe( static (@event, cancellationToken) => { var logger = @event.Services.GetRequiredService>(); logger.LogInformation("AfterResourcesCreatedEvent"); return Task.CompletedTask; }); ``` Note TypeScript AppHosts don’t expose the generic `builder.Eventing.Subscribe()` API. Use the named callback APIs generated for each supported event, such as `subscribeBeforeStart`, `subscribeAfterResourcesCreated`, `addEventingSubscriber`, and resource callbacks like `onResourceReady`. Caution `Eventing.Subscribe()` requires `T` to be a concrete event type. Subscribing to an interface or abstract class throws an `ArgumentException`. For `IDistributedApplicationEvent`, the exception message is `Cannot subscribe to interface or abstract type 'IDistributedApplicationEvent'. Subscribe to a concrete event type instead.` Use a concrete event type such as `BeforeStartEvent` or `AfterResourcesCreatedEvent`. When the AppHost is run, by the time the Aspire dashboard is displayed, you should see the following log output in the console: ```plaintext info: Program[0] BeforeStartEvent info: Aspire.Hosting.DistributedApplication[0] Aspire version: 13.5.3 info: Aspire.Hosting.DistributedApplication[0] Distributed application starting. info: Aspire.Hosting.DistributedApplication[0] Application host directory is: ../AspireApp/AspireApp.AppHost info: Aspire.Hosting.DistributedApplication[0] Now listening on: https://localhost:17178 info: Aspire.Hosting.DistributedApplication[0] Login to the dashboard at https://localhost:17178/login?t= info: Program[0] AfterResourcesCreatedEvent info: Aspire.Hosting.DistributedApplication[0] Distributed application started. Press Ctrl+C to shut down. ``` The log output confirms that event handlers are executed in the order of the AppHost life cycle events. The subscription order doesn’t affect execution order. The `BeforeStartEvent` is triggered before the AppHost starts, and `AfterResourcesCreatedEvent` is triggered after resources are created. ## Resource eventing [Section titled “Resource eventing”](#resource-eventing) In addition to the AppHost events, you can also subscribe to resource events. Resource events are raised specific to an individual resource. Resource events are defined as implementations of the `IDistributedApplicationResourceEvent` interface. The following resource events are available in the listed order: 1. `InitializeResourceEvent`: Raised by orchestrators to signal to resources that they should initialize themselves. 2. `ResourceEndpointsAllocatedEvent`: Raised when the orchestrator allocates endpoints for a resource. 3. `ConnectionStringAvailableEvent`: Raised when a connection string becomes available for a resource. 4. `BeforeResourceStartedEvent`: Raised before the orchestrator starts a new resource. 5. `ResourceReadyEvent`: Raised when a resource initially transitions to a ready state. ### Subscribe to resource events [Section titled “Subscribe to resource events”](#subscribe-to-resource-events) To subscribe to resource events, use the convenience-based extension methods. After you have a distributed application builder instance, and a resource builder, walk up to the instance and chain a call to the desired event API: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const cache = await builder.addRedis('cache'); await cache.onInitializeResource(async () => { console.log('1. onInitializeResource'); }); await cache.onResourceEndpointsAllocated(async (event) => { const resource = await event.resource(); console.log(`2. endpoints allocated for ${resource.getResourceName()}`); }); await cache.onConnectionStringAvailable(async (event) => { const resource = await event.resource(); console.log( `3. connection string available for ${resource.getResourceName()}` ); }); await cache.onBeforeResourceStarted(async () => { console.log('4. onBeforeResourceStarted'); }); await cache.onResourceReady(async () => { console.log('5. onResourceReady'); }); await builder.build().run(); ``` * C# AppHost.cs ```csharp using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; var builder = DistributedApplication.CreateBuilder(args); var cache = builder.AddRedis("cache"); cache.OnResourceReady(static (resource, @event, cancellationToken) => { var logger = @event.Services.GetRequiredService>(); logger.LogInformation("5. OnResourceReady"); return Task.CompletedTask; }); cache.OnInitializeResource( static (resource, @event, cancellationToken) => { var logger = @event.Services.GetRequiredService>(); logger.LogInformation("1. OnInitializeResource"); return Task.CompletedTask; }); cache.OnBeforeResourceStarted( static (resource, @event, cancellationToken) => { var logger = @event.Services.GetRequiredService>(); logger.LogInformation("4. OnBeforeResourceStarted"); return Task.CompletedTask; }); cache.OnResourceEndpointsAllocated( static (resource, @event, cancellationToken) => { var logger = @event.Services.GetRequiredService>(); logger.LogInformation("2. OnResourceEndpointsAllocated"); return Task.CompletedTask; }); cache.OnConnectionStringAvailable( static (resource, @event, cancellationToken) => { var logger = @event.Services.GetRequiredService>(); logger.LogInformation("3. OnConnectionStringAvailable"); return Task.CompletedTask; }); var apiService = builder.AddProject("apiservice"); builder.AddProject("webfrontend") .WithExternalHttpEndpoints() .WithReference(cache) .WaitFor(cache) .WithReference(apiService) .WaitFor(apiService); builder.Build().Run(); ``` The preceding code subscribes to the `InitializeResourceEvent`, `ResourceReadyEvent`, `ResourceEndpointsAllocatedEvent`, `ConnectionStringAvailableEvent`, and `BeforeResourceStartedEvent` events on the `cache` resource. Chain calls to the event methods to subscribe to multiple events on the same resource. Note TypeScript callbacks receive the event object. Use `await event.resource()` and `await event.services()` when you need the resource or service provider. * `OnInitializeResource` / `onInitializeResource`: Subscribes to the `InitializeResourceEvent`. * `OnResourceEndpointsAllocated` / `onResourceEndpointsAllocated`: Subscribes to the `ResourceEndpointsAllocatedEvent` event. * `OnConnectionStringAvailable` / `onConnectionStringAvailable`: Subscribes to the `ConnectionStringAvailableEvent` event. * `OnBeforeResourceStarted` / `onBeforeResourceStarted`: Subscribes to the `BeforeResourceStartedEvent` event. * `OnResourceReady` / `onResourceReady`: Subscribes to the `ResourceReadyEvent` event. * `OnResourceStopped` / `onResourceStopped`: Subscribes to the `ResourceStoppedEvent` event. When the AppHost is run, by the time the Aspire dashboard is displayed, you should see the following log output in the console: ```plaintext info: Aspire.Hosting.DistributedApplication[0] Aspire version: 13.5.3 info: Aspire.Hosting.DistributedApplication[0] Distributed application starting. info: Aspire.Hosting.DistributedApplication[0] Application host directory is: ../AspireApp/AspireApp.AppHost info: Program[0] 1. OnInitializeResource info: Program[0] 2. OnResourceEndpointsAllocated info: Program[0] 3. OnConnectionStringAvailable info: Program[0] 4. OnBeforeResourceStarted info: Aspire.Hosting.DistributedApplication[0] Now listening on: https://localhost:17222 info: Aspire.Hosting.DistributedApplication[0] Login to the dashboard at https://localhost:17222/login?t= info: Program[0] 5. OnResourceReady info: Aspire.Hosting.DistributedApplication[0] Distributed application started. Press Ctrl+C to shut down. ``` Note Some events block execution. For example, when the `BeforeResourceStartedEvent` is published, the resource startup blocks until all subscriptions for that event on a given resource finish executing. Whether an event blocks or not depends on how you publish it (see the following section). ## Publish events [Section titled “Publish events”](#publish-events) When subscribing to any of the built-in events, you don’t need to publish the event yourself as the AppHost orchestrator manages to publish built-in events on your behalf. However, you can publish custom events with the eventing API. To publish an event, you have to first define an event as an implementation of either the `IDistributedApplicationEvent` or `IDistributedApplicationResourceEvent` interface. You need to determine which interface to implement based on whether the event is a global AppHost event or a resource-specific event. Then, you can subscribe and publish the event by calling the either of the following APIs: * `PublishAsync(T, CancellationToken)`: Publishes an event to all subscribes of the specific event type. * `PublishAsync(T, EventDispatchBehavior, CancellationToken)`: Publishes an event to all subscribes of the specific event type with a specified dispatch behavior. ### Provide an `EventDispatchBehavior` [Section titled “Provide an EventDispatchBehavior”](#provide-an-eventdispatchbehavior) When events are dispatched, you can control how the events are dispatched to subscribers. The event dispatch behavior is specified with the `EventDispatchBehavior` enum. The following behaviors are available: * `EventDispatchBehavior.BlockingSequential`: Fires events sequentially and blocks until they’re all processed. * `EventDispatchBehavior.BlockingConcurrent`: Fires events concurrently and blocks until they’re all processed. * `EventDispatchBehavior.NonBlockingSequential`: Fires events sequentially but doesn’t block. * `EventDispatchBehavior.NonBlockingConcurrent`: Fires events concurrently but doesn’t block. The default behavior is `EventDispatchBehavior.BlockingSequential`. To override this behavior, when calling a publishing API such as `PublishAsync`, provide the desired behavior as an argument. ## Eventing subscribers [Section titled “Eventing subscribers”](#eventing-subscribers) In some cases, such as extension libraries, you may need to access lifecycle events from a service rather than directly from the Aspire application model. You can implement `IDistributedApplicationEventingSubscriber` and register the service with `AddEventingSubscriber` (or `TryAddEventingSubscriber` if you want to avoid duplicate registrations). AppHost.cs ```csharp using Aspire.Hosting.Eventing; using Aspire.Hosting.Lifecycle; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; var builder = DistributedApplication.CreateBuilder(args); builder.Services.AddEventingSubscriber(); builder.Build().Run(); internal sealed class LifecycleLoggerSubscriber(ILogger logger) : IDistributedApplicationEventingSubscriber { public Task SubscribeAsync( IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) { eventing.Subscribe((@event, ct) => { logger.LogInformation("1. BeforeStartEvent"); return Task.CompletedTask; }); eventing.Subscribe((@event, ct) => { logger.LogInformation("2. {Resource} ResourceEndpointsAllocatedEvent", @event.Resource.Name); return Task.CompletedTask; }); eventing.Subscribe((@event, ct) => { logger.LogInformation("3. AfterResourcesCreatedEvent"); return Task.CompletedTask; }); return Task.CompletedTask; } } ``` Note C# eventing subscriber service classes are specific to the C# AppHost hosting model. TypeScript AppHosts can register callback-based subscribers with `addEventingSubscriber` or `tryAddEventingSubscriber`. apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); builder.addEventingSubscriber(async (events) => { events.onBeforeStart(async () => { console.log('1. BeforeStartEvent'); }); events.onAfterResourcesCreated(async () => { console.log('3. AfterResourcesCreatedEvent'); }); }); await builder.build().run(); ``` The subscriber approach keeps builder code minimal while still letting you respond to the same lifecycle moments as inline subscriptions: * `AddEventingSubscriber()` (or `TryAddEventingSubscriber()`) ensures the subscriber participates whenever the AppHost starts. * `SubscribeAsync` is called once per AppHost execution, giving you access to `IDistributedApplicationEventing` and the `DistributedApplicationExecutionContext` should you need model- or environment-specific data. * You can register handlers for any built-in event (AppHost or resource) or for your own custom `IDistributedApplicationEvent` types. Use this pattern whenever you previously relied on `IDistributedApplicationLifecycleHook`. The lifecycle hook APIs remain only for backward compatibility and will be removed in a future release. ### Migrating from lifecycle hooks [Section titled “Migrating from lifecycle hooks”](#migrating-from-lifecycle-hooks) If you’re migrating from the deprecated `IDistributedApplicationLifecycleHook` interface, use the following mapping: | Old pattern (deprecated) | New pattern | | -------------------------------- | ---------------------------------------------- | | `BeforeStartAsync()` | Subscribe to `BeforeStartEvent` | | `AfterEndpointsAllocatedAsync()` | Subscribe to `ResourceEndpointsAllocatedEvent` | | `AfterResourcesCreatedAsync()` | Subscribe to `AfterResourcesCreatedEvent` | | `TryAddLifecycleHook()` | `TryAddEventingSubscriber()` | **Before (deprecated):** OldLifecycleHook.cs ```csharp public class MyHook : IDistributedApplicationLifecycleHook { public Task AfterResourcesCreatedAsync( DistributedApplicationModel model, CancellationToken cancellationToken) { // Handle event return Task.CompletedTask; } } // Registration builder.Services.TryAddLifecycleHook(); ``` **After (recommended):** NewEventingSubscriber.cs ```csharp public class MySubscriber : IDistributedApplicationEventingSubscriber { public Task SubscribeAsync( IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext context, CancellationToken cancellationToken) { eventing.Subscribe((@event, ct) => { // Handle event using context.Model return Task.CompletedTask; }); return Task.CompletedTask; } } // Registration builder.Services.TryAddEventingSubscriber(); ``` Caution The `IDistributedApplicationLifecycleHook` interface is deprecated as of Aspire 9.0 and will be removed in a future release. Migrate to `IDistributedApplicationEventingSubscriber` for new code. ## Additional events [Section titled “Additional events”](#additional-events) Beyond the core lifecycle events, Aspire provides additional events for specific scenarios: ### Publishing events [Section titled “Publishing events”](#publishing-events) When publishing your application (generating deployment manifests), these events are raised: | Event | When raised | Purpose | | -------------------- | -------------------------- | ------------------------------------------------------- | | `BeforePublishEvent` | Before publishing begins | Validate or modify resources before manifest generation | | `AfterPublishEvent` | After publishing completes | Perform cleanup or post-publish actions | * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); await builder.subscribeBeforePublish(async (event) => { const model = await event.model(); console.log(`Publishing ${model.getResources().length} resources`); }); await builder.subscribeAfterPublish(async (event) => { const services = await event.services(); console.log('Publish completed', services); }); ``` * C# AppHost.cs ```csharp builder.OnBeforePublish((@event, ct) => { // Validate resources before publishing return Task.CompletedTask; }); builder.OnAfterPublish((@event, ct) => { // Post-publish actions return Task.CompletedTask; }); ``` For details on what happens during publishing, see [Publishing and deployment overview](/deployment/deploy-with-aspire/). ### Resource stopped event [Section titled “Resource stopped event”](#resource-stopped-event) The `ResourceStoppedEvent` is raised when a resource stops execution: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const cache = await builder.addRedis('cache'); await cache.onResourceStopped(async (event) => { const resource = await event.resource(); console.log(`Resource ${resource.getResourceName()} stopped`); }); ``` * C# AppHost.cs ```csharp using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; var builder = DistributedApplication.CreateBuilder(args); var cache = builder.AddRedis("cache"); cache.OnResourceStopped( static (resource, @event, ct) => { var logger = @event.Services.GetRequiredService>(); logger.LogInformation("Resource {Name} stopped", resource.Name); return Task.CompletedTask; }); builder.Build().Run(); ``` Note Event publishing is **synchronous and blocking** — event handlers can delay further execution. Keep handlers lightweight and avoid long-running operations. ## See also [Section titled “See also”](#see-also) * [Custom resources](/extensibility/custom-resources/) * [Resource annotations](/fundamentals/annotations-overview/) # Host external executables in Aspire > Host external executable applications in your Aspire AppHost using AddExecutable — model CLI tools, daemons, and language runtimes alongside containers and projects. In Aspire, you can host external executable applications alongside your projects using the `AddExecutable` method. This capability is useful when you need to integrate executable applications or tools into your distributed application, such as Node.js applications, Python scripts, or specialized CLI tools. ## When to use executable resources [Section titled “When to use executable resources”](#when-to-use-executable-resources) Use executable resources when you need to: * Run applications or tools directly on the host instead of in a container. * Integrate command-line tools or utilities into your application. * Run external processes that other resources depend on. * Develop with tools that provide local development servers. Common examples include: * **Frontend development servers**: Tools like [Vercel CLI](https://vercel.com/docs/cli) or webpack dev server. * **Language-specific applications**: Node.js apps, Python scripts, or Go applications. * **Database tools**: Migration utilities or database seeders. * **Build tools**: Asset processors or code generators. ## Basic usage [Section titled “Basic usage”](#basic-usage) The `AddExecutable` method requires a resource name, the executable path, and optionally command-line arguments and a working directory: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Basic executable without arguments const nodeApp = await builder.addExecutable("frontend", "node", ".", ["server.js"]); // Executable with command-line arguments const pythonApp = await builder.addExecutable("api", "python", ".", ["-m", "uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"]); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Basic executable without arguments var nodeApp = builder.AddExecutable("frontend", "node", ".", "server.js"); // Executable with command-line arguments var pythonApp = builder.AddExecutable( "api", "python", ".", "-m", "uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"); builder.Build().Run(); ``` This code demonstrates setting up a basic executable resource. The first example runs a Node.js server script, while the second starts a Python application using Uvicorn with specific configuration options passed as arguments directly to the `AddExecutable` method. ## Resource dependencies and environment configuration [Section titled “Resource dependencies and environment configuration”](#resource-dependencies-and-environment-configuration) You can provide command-line arguments directly in the `AddExecutable` call and configure environment variables for resource dependencies. Executable resources can reference other resources and access their connection information. ### Arguments in the AddExecutable call [Section titled “Arguments in the AddExecutable call”](#arguments-in-the-addexecutable-call) * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Arguments provided directly in addExecutable const app = await builder.addExecutable("vercel-dev", "vercel", ".", ["dev", "--listen", "3000"]); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Arguments provided directly in AddExecutable var app = builder.AddExecutable( "vercel-dev", "vercel", ".", "dev", "--listen", "3000"); ``` ### Resource dependencies with environment variables [Section titled “Resource dependencies with environment variables”](#resource-dependencies-with-environment-variables) For arguments that depend on other resources, use environment variables: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const redis = await builder.addRedis("cache"); const postgres = (await builder.addPostgres("postgres")).addDatabase("appdb"); const app = await builder.addExecutable("worker", "python", ".", ["worker.py"]) .withReference(redis) // Provides ConnectionStrings__cache .withReference(postgres); // Provides ConnectionStrings__appdb ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var redis = builder.AddRedis("cache"); var postgres = builder.AddPostgres("postgres").AddDatabase("appdb"); var app = builder.AddExecutable("worker", "python", ".", "worker.py") .WithReference(redis) // Provides ConnectionStrings__cache .WithReference(postgres); // Provides ConnectionStrings__appdb ``` When one resource depends on another, `WithReference` passes along environment variables containing the dependent resource’s connection details. For example, the `worker` executable’s reference to `redis` and `postgres` provides it with the `ConnectionStrings__cache` and `ConnectionStrings__appdb` environment variables, which contain connection strings to these resources. ### Access specific endpoint information [Section titled “Access specific endpoint information”](#access-specific-endpoint-information) For more control over how connection information is passed to your executable: * TypeScript apphost.mts ```typescript import { createBuilder, EndpointProperty } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const redis = await builder.addRedis("cache"); const redisEndpoint = await redis.getEndpoint("tcp"); const redisHost = await redisEndpoint.property(EndpointProperty.Host); const redisPort = await redisEndpoint.property(EndpointProperty.Port); const app = await builder.addExecutable("app", "node", ".", ["app.js"]) .withReference(redis) .withEnvironment("REDIS_HOST", redisHost) .withEnvironment("REDIS_PORT", redisPort); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var redis = builder.AddRedis("cache"); var app = builder.AddExecutable("app", "node", ".", "app.js") .WithReference(redis) .WithEnvironment(context => { // Provide individual connection details context.EnvironmentVariables["REDIS_HOST"] = redis.Resource.PrimaryEndpoint.Property(EndpointProperty.Host); context.EnvironmentVariables["REDIS_PORT"] = redis.Resource.PrimaryEndpoint.Property(EndpointProperty.Port); }); ``` ## Practical example: Vercel CLI [Section titled “Practical example: Vercel CLI”](#practical-example-vercel-cli) Here’s a complete example using the [Vercel CLI](https://vercel.com/docs/cli) to host a frontend application with a backend API: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Backend API const api = await builder.addProject("api", "./Api/Api.csproj") .withExternalHttpEndpoints(); // Frontend with Vercel CLI const frontend = await builder.addExecutable("vercel-dev", "vercel", ".", ["dev", "--listen", "3000"]) .withEnvironment("API_URL", api.getEndpoint("http")) .withHttpEndpoint({ port: 3000, name: "http" }); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Backend API var api = builder.AddProject("api") .WithExternalHttpEndpoints(); // Frontend with Vercel CLI var frontend = builder.AddExecutable( "vercel-dev", "vercel", ".", "dev", "--listen", "3000") .WithEnvironment("API_URL", api.GetEndpoint("http")) .WithHttpEndpoint(port: 3000, name: "http"); builder.Build().Run(); ``` ## Configure endpoints [Section titled “Configure endpoints”](#configure-endpoints) Executable resources can expose HTTP endpoints that other resources can reference: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const frontend = await builder.addExecutable("webpack-dev", "npx", ".", ["webpack", "serve", "--port", "8080", "--host", "0.0.0.0"]) .withHttpEndpoint({ port: 8080, name: "http" }); // Another service can reference the frontend const e2eTests = await builder.addExecutable("playwright", "npx", ".", ["playwright", "test"]) .withEnvironment("BASE_URL", frontend.getEndpoint("http")); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var frontend = builder.AddExecutable( "webpack-dev", "npx", ".", "webpack", "serve", "--port", "8080", "--host", "0.0.0.0") .WithHttpEndpoint(port: 8080, name: "http"); // Another service can reference the frontend var e2eTests = builder.AddExecutable("playwright", "npx", ".", "playwright", "test") .WithEnvironment("BASE_URL", frontend.GetEndpoint("http")); ``` ## Environment configuration [Section titled “Environment configuration”](#environment-configuration) Configure environment variables for your executable: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const app = await builder.addExecutable("api", "uvicorn", ".", ["main:app", "--reload", "--host", "0.0.0.0"]) .withEnvironment("DEBUG", "true") .withEnvironment("LOG_LEVEL", "info") .withEnvironment("START_TIME", new Date().toISOString()); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var app = builder.AddExecutable( "api", "uvicorn", ".", "main:app", "--reload", "--host", "0.0.0.0") .WithEnvironment("DEBUG", "true") .WithEnvironment("LOG_LEVEL", "info") .WithEnvironment(context => { // Dynamic environment variables context.EnvironmentVariables["START_TIME"] = DateTimeOffset.UtcNow.ToString(); }); ``` ### `withEnvironment` API unification in Aspire 13.3 [Section titled “withEnvironment API unification in Aspire 13.3”](#withenvironment-api-unification-in-aspire-133) Aspire 13.3 unified non-C# AppHost environment assignment behind a single `withEnvironment(name, value)` pattern. The public TypeScript API accepts plain strings, reference expressions, endpoint references, parameter resources, supported resources that expose connection strings, expression values, and awaitable forms of supported values. When upgrading to Aspire 13.3, replace every earlier per-kind environment helper call with `withEnvironment(name, value)`. The 13.3 TypeScript SDK doesn’t generate compatibility aliases for those helpers. ## Publishing with PublishAsDockerFile [Section titled “Publishing with PublishAsDockerFile”](#publishing-with-publishasdockerfile) For production deployment, executable resources need to be containerized. Use the `PublishAsDockerFile` method to specify how the executable should be packaged: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const app = await builder.addExecutable("frontend", "npm", ".", ["start", "--port", "3000"]) .publishAsDockerFile(async () => {}); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var app = builder.AddExecutable( "frontend", "npm", ".", "start", "--port", "3000") .PublishAsDockerFile(); ``` When you call `PublishAsDockerFile()`, Aspire generates a Dockerfile during the publish process. You can customize this by providing your own Dockerfile: ### Custom Dockerfile for publishing [Section titled “Custom Dockerfile for publishing”](#custom-dockerfile-for-publishing) Create a `Dockerfile` in your executable’s working directory: Dockerfile ```dockerfile FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . EXPOSE 3000 CMD ["npm", "start"] ``` Then reference it in your AppHost: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const app = await builder.addExecutable("frontend", "npm", ".", ["start"]) .publishAsDockerFile(async () => {}); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var app = builder.AddExecutable("frontend", "npm", ".", "start") .PublishAsDockerFile([new DockerfileBuildArg("NODE_ENV", "production")]); ``` ## Best practices [Section titled “Best practices”](#best-practices) When working with executable resources: 1. **Use explicit paths**: For better reliability, use full paths to executables when possible. 2. **Handle dependencies**: Use `WithReference` to establish proper dependency relationships. 3. **Configure explicit start**: Use `WithExplicitStart()` for executables that shouldn’t start automatically. 4. **Prepare for deployment**: Always use `PublishAsDockerFile()` for production scenarios. 5. **Environment isolation**: Use environment variables rather than command-line arguments for sensitive configuration. 6. **Resource naming**: Use descriptive names that clearly identify the executable’s purpose. # Hot Reload and watch > Learn how hot reload works in Aspire and how `aspire watch` rebuilds and restarts resources automatically when project files change during development. Aspire has two levels of watch behavior: 1. **AppHost watch** - watches the AppHost itself so changes to the application model restart the AppHost-managed application. 2. **Resource watch and hot reload** - depend on the application or framework backing each resource. Aspire’s CLI watch support is centered on the AppHost. Aspire supports two AppHost languages, C# and TypeScript, and `defaultWatchEnabled` applies to the AppHost-managed application regardless of which AppHost language you use. When watch mode is enabled, Aspire owns the file-watching loop for the AppHost-managed application. File changes cause Aspire to restart the application topology so the updated AppHost model and resources are applied. Aspire watch mode is the recommended CLI workflow when you want hot reload-like behavior for AppHost changes. It is restart-based: Aspire restarts the AppHost-managed application after changes instead of applying runtime-specific hot reload semantics inside every resource process. ## Default Aspire behavior [Section titled “Default Aspire behavior”](#default-aspire-behavior) By default, `aspire run` and `aspire start` start the Aspire application once. They don’t watch the AppHost or resource source files. After you change AppHost code, restart the Aspire application manually: * For `aspire run`, stop the process with ⌃+C`⌃+C`Control + C`CtrlC`Control + C`CtrlC`, and then run `aspire run` again. * For `aspire start`, run `aspire start` again. The command stops the previous detached instance and starts a new one. ## Enable default watch mode [Section titled “Enable default watch mode”](#enable-default-watch-mode) Aspire includes an opt-in `defaultWatchEnabled` feature flag. When enabled, Aspire uses watch mode by default and automatically restarts the Aspire application after supported AppHost or resource file changes: Aspire CLI ```bash aspire config set features.defaultWatchEnabled true ``` To enable watch mode for every Aspire project on your machine, set the value globally: Aspire CLI ```bash aspire config set features.defaultWatchEnabled true --global ``` To see the current value and available feature flags, run: Aspire CLI ```bash aspire config list --all ``` Watch mode is useful when you want Aspire to restart the AppHost-managed application for you after AppHost changes. It supports both C# and TypeScript AppHosts and is a restart-based workflow, not the same experience as runtime-specific or IDE-specific hot reload. ## AppHost language guidance [Section titled “AppHost language guidance”](#apphost-language-guidance) * TypeScript For a TypeScript AppHost, `defaultWatchEnabled` watches the AppHost. When AppHost code changes, Aspire restarts the AppHost-managed application so the updated model is applied. TypeScript AppHost watch doesn’t automatically provide hot reload for every resource in the application. Use resource-specific watch, reload, restart, or rebuild workflows for changes inside individual resources. Use this workflow when changes affect: * The AppHost model in `apphost.mts`. * Resource configuration, endpoints, parameters, or integration setup. * Multiple services that need to be restarted together under Aspire orchestration. Aspire CLI ```bash aspire config set features.defaultWatchEnabled true aspire run ``` * C# For a C# AppHost, `defaultWatchEnabled` watches the AppHost project. When AppHost code changes, Aspire restarts the AppHost-managed application so the updated model is applied. Today, C# project resources are also controlled by this setting. That means changes to C# project resources can trigger Aspire to restart the AppHost-managed application too. Use this workflow when changes affect: * The AppHost model in `AppHost.cs`. * C# project resources that are part of the AppHost. * Resource configuration, endpoints, parameters, or integration setup. * Multiple services that need to be restarted together under Aspire orchestration. Aspire CLI ```bash aspire config set features.defaultWatchEnabled true aspire run ``` ## Hot reload for Aspire resources [Section titled “Hot reload for Aspire resources”](#hot-reload-for-aspire-resources) AppHost watch and resource hot reload are separate concerns. The AppHost describes and starts the application topology, but each resource is backed by a framework or runtime with its own development loop. In general, keep the AppHost running while you work on individual resources. Don’t stop and restart the AppHost just because one resource changed. If a resource needs to be restarted or rebuilt, do that for the individual resource from the Aspire CLI or the Aspire Dashboard. Use the `aspire resource` command to control individual resources from the CLI: Aspire CLI ```bash aspire resource stop aspire resource start ``` For C# project resources, rebuild the individual resource when the project needs to be rebuilt: Aspire CLI ```bash aspire resource rebuild ``` * C# projects `dotnet watch` natively supports C# Aspire AppHosts and transitively watches .NET projects in the application: * Changes to the AppHost restart the AppHost. * Changes to an individual .NET project, or to one of its dependencies, restart that project. * Rude edits restart the application. Run `dotnet watch` against the AppHost project when you want the .NET SDK’s watch loop for the AppHost and its C# projects: .NET CLI ```bash dotnet watch --project './src/MyApp.AppHost/MyApp.AppHost.csproj' ``` Important This experience has some quirks today. Some changes are applied but can still require an explicit restart, and it isn’t always easy to tell when that happened. If you don’t observe an expected change, restart the resource with `aspire resource stop` and `aspire resource start`, or rebuild a C# project resource with `aspire resource rebuild`. When you use Aspire watch mode instead, C# project resources are special today: `defaultWatchEnabled` controls both the C# AppHost and C# project resources. * Vite resources Vite resources use Vite’s development server behavior. Vite can provide browser refresh and Hot Module Replacement for the frontend application, but that behavior is separate from AppHost watch. Use Aspire watch when changes affect the AppHost model. Use the Vite development loop when changes affect the frontend application and you want Vite’s browser refresh or Hot Module Replacement behavior. If a Vite resource needs a restart, restart the Vite resource from the Aspire CLI or dashboard instead of restarting the AppHost. * Other resources Other Aspire resources follow the watch, reload, or restart behavior of the runtime or framework that backs the resource. For example, a container resource, executable resource, or framework-specific resource might not support hot reload at all, or it might require its own watch command. Use Aspire watch when the AppHost model should be re-evaluated. Use the resource’s native development command when you want the tightest inner loop for that resource. If the resource needs to be restarted or rebuilt, restart or rebuild that individual resource from the Aspire CLI or dashboard. ## Recommended workflow [Section titled “Recommended workflow”](#recommended-workflow) Use these workflows based on what you’re editing: | Goal | Recommended command | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | Run the whole distributed app once | `aspire run` | | Run the whole distributed app in the background | `aspire start` | | Watch a C# or TypeScript AppHost from the CLI | `aspire config set features.defaultWatchEnabled true`, then `aspire run` or `aspire start` | | Watch C# project resources through Aspire | `aspire config set features.defaultWatchEnabled true`, then `aspire run` or `aspire start` | | Restart one resource | `aspire resource stop`, then `aspire resource start` | | Rebuild one C# project resource | `aspire resource rebuild` | | Use a runtime-specific hot reload loop for one resource | The resource’s native watch, reload, or development-server command | Aspire CLI doesn’t currently provide a single hot reload command that applies every runtime’s hot reload semantics across an AppHost-managed distributed application. Aspire default watch supports both AppHost languages by restarting the AppHost-managed application after AppHost changes. C# project resources are also controlled by this setting today. For other resources, keep the AppHost running and use the resource’s framework, runtime, CLI action, or dashboard action for resource-specific reloads, restarts, and rebuilds. ## IDE hot reload and debugging [Section titled “IDE hot reload and debugging”](#ide-hot-reload-and-debugging) Visual Studio, Visual Studio Code, and JetBrains Rider provide their own hot reload and debugging experiences. When you run the AppHost under a debugger in one of these IDEs, Aspire delegates debugging and IDE-managed hot reload behavior to that IDE. This doesn’t integrate with or overlap Aspire’s CLI restart, rebuild, or watch behavior. Important Use an IDE workflow when you want the IDE to manage debugging or hot reload for supported resources. Use Aspire CLI and dashboard actions when you want Aspire to restart, rebuild, or watch the AppHost-managed application and its resources. ### Visual Studio Code [Section titled “Visual Studio Code”](#visual-studio-code) Use the Aspire extension for Visual Studio Code when you want VS Code to start the AppHost, attach debuggers, and manage supported resource debugging experiences. VS Code hot reload or framework-specific refresh behavior still belongs to the debugger or framework backing the resource, not to Aspire restart, rebuild, or watch behavior. ### Visual Studio [Section titled “Visual Studio”](#visual-studio) Use Visual Studio when you want its built-in debugging and hot reload experience for supported resources. Visual Studio can run and debug Aspire apps, but IDE hot reload is still separate from Aspire restart, rebuild, and watch behavior. ### JetBrains Rider [Section titled “JetBrains Rider”](#jetbrains-rider) Use JetBrains Rider when you want Rider’s debugging and hot reload experience for supported resources. Rider’s IDE-managed hot reload behavior is separate from Aspire restart, rebuild, and watch behavior. ## See also [Section titled “See also”](#see-also) * [`aspire run`](/reference/cli/commands/aspire-run/) * [`aspire start`](/reference/cli/commands/aspire-start/) * [`aspire config set`](/reference/cli/commands/aspire-config-set/) * [Aspire VS Code extension](/get-started/aspire-vscode-extension/) # Migrate from Docker Compose to Aspire > Compare Aspire and Docker Compose for local development, service discovery, and observability. Map Compose services and dependencies to TypeScript or C# AppHosts. Docker Compose and Aspire both describe and run multi-service applications. This guide compares their local development workflows and maps Compose services, dependencies, and configuration to an Aspire AppHost written in TypeScript or C#. ## Understand the differences [Section titled “Understand the differences”](#understand-the-differences) Keep Docker Compose when a container-focused YAML workflow meets your needs. Consider Aspire when you want to compose containers with processes running directly on the host, manage service references in code, and inspect application telemetry alongside resource health. ### Docker Compose vs Aspire [Section titled “Docker Compose vs Aspire”](#docker-compose-vs-aspire) | Feature | Docker Compose | Aspire | | ----------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | **Primary purpose** | Define and run multi-container applications | Compose application resources for development and deployment | | **Scope** | Containers | Containers, Python and Node.js apps, .NET projects, executables, and cloud resources | | **Configuration** | Declarative YAML | Strongly typed TypeScript or C# AppHost | | **Target environment** | Docker environments | Local development and deployment through publishing integrations, including Docker Compose | | **Service discovery** | Service names and DNS on Compose networks | Service references and connection information passed to resources | | **Local observability** | Container logs and health checks; add OpenTelemetry tooling for application telemetry | Integrated resource health, console logs, and an OpenTelemetry dashboard; application instrumentation is still required | ### Key conceptual shifts [Section titled “Key conceptual shifts”](#key-conceptual-shifts) When migrating from Docker Compose to Aspire, consider these conceptual differences: * **From YAML to an AppHost** — Express configuration in strongly typed TypeScript or C# code * **From containers to resources** — Compose containers with local application processes, parameters, and cloud resources * **From container DNS to resource references** — Pass service endpoints and connection information to dependent resources * **From separate tools to a shared dashboard** — Inspect resource health and instrumented application logs, traces, and metrics together * **Startup orchestration differs** — Compose `depends_on` supports startup order and health conditions; Aspire references supply connection information, while wait relationships control startup dependencies You don’t need to migrate orchestration just to view OpenTelemetry. Point instrumented Compose services at the [standalone Aspire dashboard](/dashboard/standalone/) to inspect logs, traces, and metrics, including through [coding-agent CLI or MCP workflows](/dashboard/ai-coding-agents/#standalone-mode). Standalone mode doesn’t add AppHost resource controls to Compose. For detailed API mappings, see [Docker Compose to Aspire AppHost API reference](/app-host/docker-compose-to-apphost-reference/). ## Common migration patterns [Section titled “Common migration patterns”](#common-migration-patterns) This section demonstrates practical migration scenarios you’ll likely encounter when moving from Docker Compose to Aspire. Each pattern shows a complete Docker Compose example alongside its accurate Aspire equivalent. ### Multi-service web application [Section titled “Multi-service web application”](#multi-service-web-application) This example shows a typical three-tier application with a frontend, API, and database. **Docker Compose example:** compose.yaml ```yaml version: '3.8' services: frontend: build: ./frontend ports: - "3000:3000" depends_on: api: condition: service_healthy environment: - API_URL=http://api:5000 api: build: ./api ports: - "5000:5000" depends_on: database: condition: service_healthy environment: - ConnectionStrings__DefaultConnection=Host=database;Database=myapp;Username=postgres;Password=secret healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5000/health"] interval: 10s timeout: 3s retries: 3 database: image: postgres:15 environment: - POSTGRES_DB=myapp - POSTGRES_USER=postgres - POSTGRES_PASSWORD=secret volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 3s retries: 3 volumes: postgres_data: ``` **Aspire equivalent:** * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Add PostgreSQL with explicit version and persistent storage const database = (await builder.addPostgres("postgres") .withImageTag("15") .withDataVolume()) .addDatabase("myapp"); // Add the API project with proper dependencies const api = await builder.addProject("api", "./MyApp.Api/MyApp.Api.csproj") .withHttpEndpoint({ port: 5000 }) .withHttpHealthCheck("/health") .withReference(database, "DefaultConnection") .waitFor(database); // Add the frontend project with dependencies const frontend = await builder.addProject("frontend", "./MyApp.Frontend/MyApp.Frontend.csproj") .withHttpEndpoint({ port: 3000 }) .withReference(api) .withEnvironment("API_URL", api.getEndpoint("http")) .waitFor(api); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Add PostgreSQL with explicit version and persistent storage var database = builder.AddPostgres("postgres") .WithImageTag("15") .WithDataVolume() .AddDatabase("myapp"); // Add the API project with proper dependencies var api = builder.AddProject("api") .WithHttpEndpoint(port: 5000) .WithHttpHealthCheck("/health") .WithReference(database, "DefaultConnection") .WaitFor(database); // Add the frontend project with dependencies var frontend = builder.AddProject("frontend") .WithHttpEndpoint(port: 3000) .WithReference(api) .WithEnvironment("API_URL", api.GetEndpoint("http")) .WaitFor(api); builder.Build().Run(); ``` build: services become project or Dockerfile resources In Docker Compose, the `build:` directive creates container images from Dockerfiles. In Aspire, .NET services are added directly as project references with `AddProject()`, providing better debugging, hot reload, and telemetry integration. For services that still build from Dockerfiles, use `AddDockerfile()` for an existing Dockerfile or `AddDockerfileBuilder()` when the AppHost should generate the Dockerfile programmatically. **Key differences explained:** * **Build vs. project** — Docker Compose `build:` services become `AddProject()` for .NET apps, which runs them directly instead of in containers * **Ports** — Both examples explicitly map ports (3000 and 5000) * **Startup order** — Docker Compose uses `depends_on` with health conditions; Aspire uses `WaitFor()` for startup ordering * **Service discovery** — `WithReference()` only configures service discovery and connection strings; it doesn’t control startup order * **Connection strings** — By default, `WithReference(database)` provides `ConnectionStrings__myapp` using the resource name from `AddDatabase()`. To match a different name like `DefaultConnection`, use a named reference: `.WithReference(database, "DefaultConnection")` * **Volumes** — `WithDataVolume()` must be called explicitly to add persistent storage; it’s not automatic * **Image versions** — `WithImageTag("15")` pins PostgreSQL to version 15 ### Container-based services [Section titled “Container-based services”](#container-based-services) This example shows a mix of existing container images and a Dockerfile-built service being orchestrated. **Docker Compose example:** compose.yaml ```yaml version: '3.8' services: web: build: . ports: - "8080:8080" depends_on: redis: condition: service_started postgres: condition: service_healthy environment: - REDIS_URL=redis://redis:6379 - DATABASE_URL=postgresql://postgres:secret@postgres:5432/main redis: image: redis:7 ports: - "6379:6379" postgres: image: postgres:15 environment: POSTGRES_PASSWORD: secret volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready"] interval: 10s volumes: postgres_data: ``` **Aspire equivalent:** * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Add backing services with explicit versions const redis = await builder.addRedis("redis") .withImageTag("7") .withHostPort(6379); const postgres = (await builder.addPostgres("postgres") .withImageTag("15") .withDataVolume()) .addDatabase("main"); // Build the web app from a Dockerfile (matches Docker Compose "build: .") const web = await builder.addDockerfile("web", ".") .withHttpEndpoint({ port: 8080, targetPort: 8080 }) .withReference(redis) .withReference(postgres) .waitFor(redis) .waitFor(postgres); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Add backing services with explicit versions var redis = builder.AddRedis("redis") .WithImageTag("7") .WithHostPort(6379); var postgres = builder.AddPostgres("postgres") .WithImageTag("15") .WithDataVolume() .AddDatabase("main"); // Build the web app from a Dockerfile (matches Docker Compose "build: .") var web = builder.AddDockerfile("web", ".") .WithHttpEndpoint(port: 8080, targetPort: 8080) .WithReference(redis) .WithReference(postgres) .WaitFor(redis) .WaitFor(postgres); builder.Build().Run(); ``` Connection string format differences Aspire generates .NET-format connection strings, which differ from Docker Compose URL formats: * Docker Compose: `REDIS_URL=redis://redis:6379` * Aspire: `ConnectionStrings__redis=localhost:54321` * Docker Compose: `DATABASE_URL=postgresql://postgres:secret@postgres:5432/main` * Aspire: `ConnectionStrings__main=Host=localhost;Port=12345;Username=postgres;Password=;Database=main` If your application expects URL-format environment variables, construct them manually with `WithEnvironment()`. See [Environment variables and configuration](#environment-variables-and-configuration) for details. **Key differences explained:** * **Image versions** — Explicitly specified with `WithImageTag()` to match Docker Compose * **Dockerfile builds** — Docker Compose `build: .` maps to `AddDockerfile("web", ".")`, which builds a container image from a Dockerfile. Use `AddContainer()` for pre-built images that use `image:` in Docker Compose * **Ports** — `WithHostPort()` maps to a static host port; without it, Aspire assigns a random port * **Volumes** — `WithDataVolume()` must be called explicitly to add persistent storage * **Startup ordering** — `WaitFor()` controls startup order, similar to Docker Compose `depends_on` with conditions * **Connection strings** — `WithReference()` provides Aspire-format connection strings (`ConnectionStrings__*`), not URL-format variables ### Environment variables and configuration [Section titled “Environment variables and configuration”](#environment-variables-and-configuration) This example shows different approaches to configuration management. **Docker Compose approach:** compose.yaml ```yaml services: app: image: myapp:latest environment: - DATABASE_URL=postgresql://user:pass@db:5432/myapp - REDIS_URL=redis://cache:6379 - API_KEY=${API_KEY} - LOG_LEVEL=info ``` **Aspire approach:** * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); // Add external parameter for secrets const apiKey = builder.addParameter("apiKey", { secret: true }); const database = (await builder.addPostgres("db")) .addDatabase("myapp"); const cache = await builder.addRedis("cache"); const app = await builder.addContainer("app", { image: "myapp", tag: "latest" }) .withReference(database) .withReference(cache) .withEnvironment("API_KEY", apiKey) .withEnvironment("LOG_LEVEL", "info"); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Add external parameter for secrets var apiKey = builder.AddParameter("apiKey", secret: true); var database = builder.AddPostgres("db") .AddDatabase("myapp"); var cache = builder.AddRedis("cache"); var app = builder.AddContainer("app", "myapp", "latest") .WithReference(database) .WithReference(cache) .WithEnvironment("API_KEY", apiKey) .WithEnvironment("LOG_LEVEL", "info"); builder.Build().Run(); ``` Aspire connection strings differ from Docker Compose URLs `WithReference()` provides connection strings in .NET format, not URL format: * `ConnectionStrings__myapp=Host=localhost;Port=12345;Username=postgres;Password=;Database=myapp` * `ConnectionStrings__cache=localhost:54321` If your application expects URL-format variables like `DATABASE_URL` or `REDIS_URL`, construct them manually using the `WithEnvironment` callback: * TypeScript apphost.mts ```typescript const dbPassword = builder.addParameter("dbPassword", { secret: true }); const db = (await builder.addPostgres("db", { password: dbPassword })) .addDatabase("myapp"); const app = await builder.addContainer("app", "myapp:latest") .withReference(db) .withEnvironment("DATABASE_URL", builder.createReferenceExpression`postgresql://postgres:${dbPassword}@db:5432/myapp`) .withEnvironment("REDIS_URL", "redis://cache:6379"); ``` * C# AppHost.cs ```csharp var dbPassword = builder.AddParameter("dbPassword", secret: true); var db = builder.AddPostgres("db", password: dbPassword) .AddDatabase("myapp"); var app = builder.AddContainer("app", "myapp", "latest") .WithReference(db) .WithEnvironment(context => { context.EnvironmentVariables["DATABASE_URL"] = ReferenceExpression.Create( $"postgresql://postgres:{dbPassword}@db:5432/myapp"); context.EnvironmentVariables["REDIS_URL"] = "redis://cache:6379"; }); ``` ### Custom volumes and bind mounts [Section titled “Custom volumes and bind mounts”](#custom-volumes-and-bind-mounts) **Docker Compose example:** compose.yaml ```yaml version: '3.8' services: app: image: myapp:latest volumes: - app_data:/data - ./config:/app/config:ro worker: image: myworker:latest volumes: - app_data:/shared volumes: app_data: ``` **Aspire equivalent:** * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const app = await builder.addContainer("app", { image: "myapp", tag: "latest" }) .withVolume("/data", { name: "app-data", isReadOnly: true }) .withBindMount("./config", "/app/config", { isReadOnly: true }); const worker = await builder.addContainer("worker", { image: "myworker", tag: "latest" }) .withVolume("/shared", { name: "app-data" }); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Create a named volume for sharing data var appData = builder.AddVolume("app-data"); var app = builder.AddContainer("app", "myapp", "latest") .WithVolume(appData, "/data") .WithBindMount("./config", "/app/config", isReadOnly: true); var worker = builder.AddContainer("worker", "myworker", "latest") .WithVolume(appData, "/shared"); builder.Build().Run(); ``` **Key differences:** * **Named volumes** — Created with `AddVolume()` and shared between containers * **Bind mounts** — Use `WithBindMount()` for host directory access ### Networking [Section titled “Networking”](#networking) Docker Compose supports custom networks to isolate groups of services from each other: compose.yaml ```yaml services: proxy: build: ./proxy networks: - frontend app: build: ./app networks: - frontend - backend db: image: postgres networks: - backend networks: frontend: backend: ``` Aspire doesn’t have an equivalent for custom network isolation. Instead, Aspire automatically creates a shared container network for all container resources and uses service discovery to manage inter-service communication. All containers in an Aspire AppHost can reach each other by resource name. .NET projects and executables run on the host and access containers through injected host/port endpoints. Note If your Docker Compose setup relies on network isolation (for example, preventing a frontend service from directly accessing the database), Aspire doesn’t provide a direct equivalent. Consider using application-level access controls or firewall rules in your deployment environment instead. ## Migration strategy [Section titled “Migration strategy”](#migration-strategy) Successfully migrating from Docker Compose to Aspire requires a systematic approach. 1. ### Assess your current setup [Section titled “Assess your current setup”](#assess-your-current-setup) Before migrating, inventory your Docker Compose setup: * **Services** — Identify all services including databases, caches, APIs, and web applications * **Dependencies** — Map out service dependencies from `depends_on` declarations * **Data persistence** — Catalog all volumes and bind mounts used for data storage * **Environment variables** — List all configuration variables and secrets * **Health checks** — Document any custom health check commands * **Image versions** — Note specific versions used in production 2. ### Create the Aspire AppHost [Section titled “Create the Aspire AppHost”](#create-the-aspire-apphost) Start by creating a new Aspire project: ```bash aspire new aspire-starter -o MyApp ``` 3. ### Migrate services incrementally [Section titled “Migrate services incrementally”](#migrate-services-incrementally) Migrate services one by one, starting with backing services: * **Add backing services** like PostgreSQL, Redis with specific versions using `WithImageTag()` * **Add persistent storage** using `WithDataVolume()` where needed * **Convert .NET applications** to project references with `AddProject()` for better integration * **Convert Dockerfile-built containers** using `AddDockerfile()` to match `build:` directives * **Convert pre-built images** using `AddContainer()` to match `image:` directives * **Configure dependencies** with `WithReference()` for service discovery * **Add startup ordering** with `WaitFor()` to match `depends_on` behavior * **Set up environment variables** — Note that connection string formats will differ * **Migrate health checks** — Use `WithHttpHealthCheck()` or `WithHealthCheck()` for custom checks 4. ### Handle data migration [Section titled “Handle data migration”](#handle-data-migration) For persistent data: * Use `WithDataVolume()` for automatic volume management with integrations * Use `WithVolume()` for named volumes that need to persist data * Use `WithBindMount()` for host directory mounts when you need direct access to host files 5. ### Test and validate [Section titled “Test and validate”](#test-and-validate) * Start the Aspire AppHost and verify all services start correctly * Check the dashboard to confirm service health and connectivity status * Validate that inter-service communication works as expected * **Verify connection strings** — If your app expects specific URL formats, you may need to adjust environment variables ## Migration troubleshooting [Section titled “Migration troubleshooting”](#migration-troubleshooting) ### Common issues and solutions [Section titled “Common issues and solutions”](#common-issues-and-solutions) #### Connection string format mismatch [Section titled “Connection string format mismatch”](#connection-string-format-mismatch) Aspire generates .NET-style connection strings (`ConnectionStrings__*`) rather than URL formats like `postgresql://` or `redis://`. **Solution**: If your application expects specific URL formats, construct them manually using `WithEnvironment()`: * TypeScript apphost.mts ```typescript const dbPassword = builder.addParameter("dbPassword", { secret: true }); const postgres = (await builder.addPostgres("db", { password: dbPassword })) .addDatabase("myapp"); const app = await builder.addContainer("app", "myapp:latest") .withReference(postgres) .withEnvironment("DATABASE_URL", builder.createReferenceExpression`postgresql://postgres:${dbPassword}@db:5432/myapp`); ``` * C# AppHost.cs ```csharp var dbPassword = builder.AddParameter("dbPassword", secret: true); var postgres = builder.AddPostgres("db", password: dbPassword) .AddDatabase("myapp"); var app = builder.AddContainer("app", "myapp", "latest") .WithReference(postgres) .WithEnvironment(context => { context.EnvironmentVariables["DATABASE_URL"] = ReferenceExpression.Create( $"postgresql://postgres:{dbPassword}@db:5432/myapp"); }); ``` #### Service startup order issues [Section titled “Service startup order issues”](#service-startup-order-issues) `WithReference()` only configures service discovery, not startup ordering. **Solution**: Use `WaitFor()` to ensure dependencies are ready: * TypeScript apphost.mts ```typescript const api = await builder.addProject("api", "./Api/Api.csproj", "https") .withReference(database) // Service discovery .waitFor(database); // Startup ordering ``` * C# AppHost.cs ```csharp var api = builder.AddProject("api") .WithReference(database) // Service discovery .WaitFor(database); // Startup ordering ``` #### Volume mounting issues [Section titled “Volume mounting issues”](#volume-mounting-issues) * Use absolute paths for bind mounts to avoid path resolution issues * Ensure the host directory exists and has proper permissions * Use `WithDataVolume()` for database integrations — this must be called explicitly #### Port conflicts [Section titled “Port conflicts”](#port-conflicts) Aspire automatically assigns random ports by default. **Solution**: Use `WithHostPort()` or `WithHttpEndpoint(port:)` for static port mapping: * TypeScript apphost.mts ```typescript const redis = await builder.addRedis("cache") .withHostPort(6379); ``` * C# AppHost.cs ```csharp var redis = builder.AddRedis("cache") .WithHostPort(6379); ``` #### Health check migration [Section titled “Health check migration”](#health-check-migration) Docker Compose health checks use shell commands. Aspire integrations (like PostgreSQL and Redis) include built-in health checks automatically. For custom health checks, Aspire offers different approaches depending on the resource type. **Solution**: For resources with HTTP endpoints, use `WithHttpHealthCheck()`: * TypeScript apphost.mts ```typescript const api = await builder.addProject("api", "./Api/Api.csproj", "https") .withHttpHealthCheck("/health"); ``` * C# AppHost.cs ```csharp var api = builder.AddProject("api") .WithHttpHealthCheck("/health"); ``` For custom container health checks that need shell commands (like RabbitMQ), register a custom health check and associate it with the resource: * TypeScript apphost.mts ```typescript const rabbit = await builder.addContainer("rabbitmq", "rabbitmq", "4.1.4-management-alpine") .withHealthCheck("rabbitmq-health"); // WaitFor uses the registered health check to determine readiness const app = await builder.addProject("app", "./App/App.csproj", "https") .waitFor(rabbit); ``` * C# AppHost.cs ```csharp builder.Services.AddHealthChecks() .AddCheck("rabbitmq-health", () => { // Implement your custom health check logic here, // for example, attempting a TCP connection to the service return HealthCheckResult.Healthy(); }); var rabbit = builder.AddContainer("rabbitmq", "rabbitmq", "4.1.4-management-alpine") .WithHealthCheck("rabbitmq-health"); // WaitFor uses the registered health check to determine readiness var app = builder.AddProject("app") .WaitFor(rabbit); ``` Note Aspire integration packages (like `Aspire.Hosting.PostgreSQL` or `Aspire.Hosting.Redis`) include built-in health checks. You don’t need to define custom health checks for these services — `WaitFor()` automatically waits for the built-in health check to pass. ## Next steps [Section titled “Next steps”](#next-steps) After migrating to Aspire: * Explore [Aspire integrations](/integrations/overview/) to replace custom container configurations * Set up [health checks](/fundamentals/health-checks/) for better monitoring * Learn about [deployment options](/deployment/deploy-with-aspire/) for production environments * Consider [testing](/testing/overview/) your distributed application * Review [telemetry configuration](/fundamentals/telemetry/) for observability # Configure resource lifetimes in Aspire > Learn how session, persistent, resource-scoped, and parent-process lifetimes, plus explicit start, control Aspire containers, executables, and projects. Aspire resources support a number of different lifetime modes. For example, the default **session lifetime** starts a resource when the AppHost starts and shuts it down when the AppHost exits. A **persistent lifetime** leaves a resource running when the AppHost exits and can reuse the same instance on the next run. Resource lifetimes apply to containers, executables, and projects. Persistent executable and project lifetimes are experimental in Aspire 13.4. You can use different lifetimes for resources that take time to initialize, need stable local endpoints, should remain available while you restart or rebuild the AppHost, or need to match another resource’s lifetime. Experimental shared lifetime APIs The shared lifetime APIs are experimental and emit the [`ASPIREPERSISTENCE001`](/diagnostics/aspirepersistence001/) diagnostic. The existing container-specific `WithLifetime(ContainerLifetime.Persistent)` and `WithLifetime(ContainerLifetime.Session)` APIs remain supported for container resources. ## Lifetime modes [Section titled “Lifetime modes”](#lifetime-modes) Use the shared lifetime APIs for new code. They support container, executable, and project resources. ### Session lifetime [Section titled “Session lifetime”](#session-lifetime) A session lifetime creates the resource when the AppHost starts and disposes of it when the AppHost stops. This is the default lifetime for resources, so you usually don’t need to configure it explicitly. Use session lifetime for resources that should only exist while the AppHost is running, such as local test dependencies, temporary containers, or processes that don’t need stable state across runs. Session resources also default to proxied endpoints, which are available while the AppHost is running. If you previously configured another lifetime and want to return a resource to the default behavior, call `WithSessionLifetime()`. For container resources, `WithLifetime(ContainerLifetime.Session)` is still supported. ### Persistent lifetime [Section titled “Persistent lifetime”](#persistent-lifetime) A persistent lifetime reuses a previously created resource when possible and doesn’t dispose of it when the AppHost stops. Configure this behavior with `WithPersistentLifetime()`, or with the existing container-specific `WithLifetime(ContainerLifetime.Persistent)` API for container resources. Use persistent lifetime for resources that are expensive to initialize, need stable local endpoints, or should remain available while you restart or rebuild the AppHost. Common examples include databases, message brokers, emulators, long-running executables, and project resources that should continue running after the AppHost exits. Configuration changes can recreate persistent resources Persistent resources are automatically recreated when the AppHost detects meaningful configuration changes. If the configuration differs, the resource is recreated with the new settings. Persistent containers use proxied endpoints by default, just like session containers. The proxy runs only while the AppHost is running, so the proxy address isn’t reachable after the AppHost stops. Persistent executables and projects default to proxyless endpoints so their direct addresses stay stable and reachable even after the AppHost stops. You can still configure endpoint proxy behavior explicitly on any persistent resource. Set `isProxied: false` on an individual endpoint, or call `WithEndpointProxySupport(false)` to make every endpoint on a resource proxyless. When a proxyless endpoint doesn’t specify a public `port`, Aspire allocates one before the resource is created. For persistent resources, Aspire stores the allocated port in user secrets when user secrets are available and reuses it on later AppHost runs. For more information, see [Allocate ports for dynamic proxyless endpoints](/fundamentals/networking-overview/#allocate-ports-for-dynamic-proxyless-endpoints). Persistent container ≠ persistent data Persistent container lifetime doesn’t guarantee data durability. For details, see [Container lifetime vs. data durability](#container-lifetime-vs-data-durability). Persistent resources and replicas Persistent resources don’t support replicas because they depend on a single unique resource identifier to be resolved across AppHost runs. Persistent resources aren’t compatible with Aspire IDE debugging sessions. If you need to debug a persistent executable or project, use your debugger’s attach mode if one is available. ### Parent-process lifetime [Section titled “Parent-process lifetime”](#parent-process-lifetime) A parent-process lifetime keeps a resource available across AppHost restarts, but scopes cleanup to a parent process. Configure this behavior with `WithParentProcessLifetime(processId)`. Use parent-process lifetime for resources that should outlive an individual AppHost run but still be cleaned up when a broader development tool, IDE, or other owning process exits. Parent-process lifetime resources share persistent resource behavior across AppHost runs. If Aspire detects meaningful configuration changes on a subsequent run, the resource is recreated with the new settings. The parent process ID must be the valid ID of a running process. Aspire records both the process ID and the process identity timestamp so cleanup follows the specific process instance instead of accidentally matching a reused process ID. ### Resource-scoped lifetime [Section titled “Resource-scoped lifetime”](#resource-scoped-lifetime) A resource-scoped lifetime configures one resource to use another resource’s effective lifetime. Configure this behavior with `WithLifetimeOf(resource)`. Use resource-scoped lifetime when a companion resource should follow the lifetime choice of another resource. This is useful for sidecars, helper executables, or child resources that should become persistent only when the resource they support is persistent. Aspire evaluates the source resource’s lifetime when it prepares the application model, so later lifetime changes to the source resource are reflected by the dependent resource. The source and dependent resources must both support lifetime configuration. ## Defer resource start with explicit start [Section titled “Defer resource start with explicit start”](#defer-resource-start-with-explicit-start) Use `WithExplicitStart()` to prevent a resource from starting automatically with the rest of the AppHost. The resource appears in the dashboard but remains stopped until you start it manually. Starting in Aspire 13.5, `WithExplicitStart()` also affects when execution configuration callbacks (for example, `WithEnvironment` and `WithArgs`) are evaluated, depending on whether the resource is session-scoped or persistent. ### Session-scoped explicit-start resources [Section titled “Session-scoped explicit-start resources”](#session-scoped-explicit-start-resources) For session-scoped resources (the default lifetime), Aspire defers Developer Control Plane (DCP) registration until you manually start the resource from the dashboard. This means execution configuration callbacks — such as `WithEnvironment(context => ...)` — are **not** evaluated during AppHost startup. They run only when the resource is manually started. AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // The callback below is NOT evaluated during AppHost startup. // It runs only when the resource is manually started from the dashboard. var job = builder.AddExecutable("batch-job", "dotnet", ".", "run", "--project", "BatchJob") .WithExplicitStart() .WithEnvironment(async context => { // Prompt or compute dynamic configuration at start time context.EnvironmentVariables["API_KEY"] = await GetApiKeyAsync(); }); builder.Build().Run(); ``` Placeholder API used for demonstration `GetApiKeyAsync` is a placeholder used to demonstrate deferred, callback-based configuration. Its implementation isn’t shown because the relevant behavior is retrieving a value when the callback runs and assigning that value to an environment variable on the target resource. ### Persistent explicit-start resources [Section titled “Persistent explicit-start resources”](#persistent-explicit-start-resources) For persistent resources, Aspire must register the resource with the Developer Control Plane (DCP) immediately at startup so it can discover any existing running instance. However, when you manually start a persistent explicit-start resource, Aspire patches the existing DCP resource to start it rather than deleting and recreating it. This means the execution configuration callbacks run once during startup registration and are **not** re-evaluated when you manually start the resource. AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // Persistent explicit-start resources are registered at startup to detect existing instances. // The callback runs during startup registration — not again when manually started. #pragma warning disable ASPIREPERSISTENCE001 var cache = builder.AddContainer("long-lived-cache", "my-cache-image") .WithPersistentLifetime() .WithExplicitStart() .WithEnvironment(context => { context.EnvironmentVariables["CACHE_SIZE"] = "512mb"; }); #pragma warning restore ASPIREPERSISTENCE001 builder.Build().Run(); ``` ## Configure a persistent container [Section titled “Configure a persistent container”](#configure-a-persistent-container) For new code, configure a persistent container with `WithPersistentLifetime()`: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const postgres = await builder.addPostgres('postgres'); await postgres.withPersistentLifetime(); await postgres.withDataVolume(); const db = postgres.addDatabase('inventorydb'); const inventory = await builder.addProject( 'inventory', './InventoryService/InventoryService.csproj' ); await inventory.withReference(db); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var postgres = builder.AddPostgres("postgres") .WithPersistentLifetime() .WithDataVolume(); var db = postgres.AddDatabase("inventorydb"); builder.AddProject("inventory") .WithReference(db); builder.Build().Run(); ``` In the preceding example, the PostgreSQL container persists between AppHost runs, and `WithDataVolume()` stores database data in a named volume that survives container recreation. The `inventory` project references the database as normal. ## Configure a persistent executable [Section titled “Configure a persistent executable”](#configure-a-persistent-executable) Executable resources can also use persistent lifetimes. Persistent executables are useful for local services that have expensive startup, need stable process identity, or should remain reachable while the AppHost restarts. * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const worker = await builder.addExecutable('worker', 'node', '../worker', [ 'server.js', ]); await worker.withHttpEndpoint({ port: 5050, targetPort: 5050 }); await worker.withPersistentLifetime(); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var worker = builder.AddExecutable("worker", "node", "../worker", "server.js") .WithHttpEndpoint(port: 5050, targetPort: 5050) .WithPersistentLifetime(); builder.Build().Run(); ``` The preceding example configures a concrete `port` so the endpoint address is explicit. If you omit the public `port`, Aspire allocates one before the executable starts and reuses it from user secrets on later AppHost runs when user secrets are available. ## Configure a persistent project [Section titled “Configure a persistent project”](#configure-a-persistent-project) Project resources can use persistent lifetimes when you want the project process to continue running after the AppHost exits. * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const api = await builder.addProject('api', '../ApiService/ApiService.csproj'); await api.withPersistentLifetime(); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var api = builder.AddProject("api") .WithPersistentLifetime(); builder.Build().Run(); ``` Persistent project and executable resources are run by Aspire’s orchestrator so it can manage their lifecycle consistently. Persistent project and executable resources don’t support replicas. ## Match another resource’s lifetime [Section titled “Match another resource’s lifetime”](#match-another-resources-lifetime) Use `WithLifetimeOf` when a companion resource should follow another resource’s lifetime. This is useful when a sidecar, helper process, or supporting service should become persistent only when its source resource is persistent. * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const database = await builder.addPostgres('postgres'); await database.withPersistentLifetime(); await database.withDataVolume(); const companion = await builder.addExecutable( 'companion', 'dotnet', '../Companion', ['Companion.dll'] ); await companion.withLifetimeOf(database); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var database = builder.AddPostgres("postgres") .WithPersistentLifetime() .WithDataVolume(); var companion = builder.AddExecutable("companion", "dotnet", "../Companion", "Companion.dll") .WithLifetimeOf(database); builder.Build().Run(); ``` The dependent resource’s lifetime is evaluated when Aspire prepares the application model, so later changes to the source resource’s lifetime are reflected by the dependent resource. ## Scope cleanup to a parent process [Section titled “Scope cleanup to a parent process”](#scope-cleanup-to-a-parent-process) Use `WithParentProcessLifetime` when a resource should survive AppHost restarts but be cleaned up when another process exits. Aspire records the parent process identity instead of retaining a live process handle, so the cleanup scope follows the specific process instance instead of a reused process ID. * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const parentProcessId = 1234; const worker = await builder.addExecutable( 'scoped-worker', 'node', '../worker', ['server.js'] ); await worker.withParentProcessLifetime(parentProcessId); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var parentProcessId = int.Parse(builder.Configuration["RESOURCE_PARENT_PROCESS_ID"]!); var worker = builder.AddExecutable("scoped-worker", "node", "../worker", "server.js") .WithParentProcessLifetime(parentProcessId); builder.Build().Run(); ``` The parent process ID must be greater than zero and identify a running process. ## Use the container-specific lifetime API [Section titled “Use the container-specific lifetime API”](#use-the-container-specific-lifetime-api) The older container-specific lifetime API is still supported. Use `WithLifetime(ContainerLifetime.Persistent)` to keep a container running across AppHost restarts, or `WithLifetime(ContainerLifetime.Session)` to explicitly use the default session behavior. For new code, prefer the shared `WithPersistentLifetime()` and `WithSessionLifetime()` APIs because they work consistently across containers, executables, and projects. * TypeScript apphost.mts ```typescript import { ContainerLifetime, createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const postgres = await builder.addPostgres('postgres'); await postgres.withLifetime(ContainerLifetime.Persistent); await postgres.withDataVolume(); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var postgres = builder.AddPostgres("postgres") .WithLifetime(ContainerLifetime.Persistent) .WithDataVolume(); builder.Build().Run(); ``` ## Dashboard visualization [Section titled “Dashboard visualization”](#dashboard-visualization) The Aspire dashboard shows persistent resources with a distinctive pin icon to help you identify them: ![Screenshot of the Aspire dashboard showing a persistent resource with a pin icon.](/_astro/persistent-container.B3qvqKn7_1vkmV2.webp) After the AppHost stops, persistent containers continue running and can be seen in your container runtime (such as Docker Desktop): ![Screenshot of Docker Desktop showing a persistent RabbitMQ container still running after the AppHost stopped.](/_astro/persistent-container-docker-desktop.fBhTDXpR_1uyp24.webp) ## Container naming and uniqueness [Section titled “Container naming and uniqueness”](#container-naming-and-uniqueness) By default, persistent containers use a naming pattern that combines: * The service name you specify in your AppHost. * A postfix based on a hash of the AppHost project path. This naming scheme ensures that persistent containers are unique to each AppHost project, preventing conflicts when multiple Aspire projects use the same service names. For example, if you have a service named `"postgres"` in an AppHost project located at `/path/to/MyApp.AppHost`, the container name might be `postgres-abc123def` where `abc123def` is derived from the project path hash. ### Custom container names [Section titled “Custom container names”](#custom-container-names) For advanced scenarios, you can set a custom container name using the `WithContainerName` method: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const postgres = await builder.addPostgres('postgres'); await postgres.withPersistentLifetime(); await postgres.withContainerName('my-shared-postgres'); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var postgres = builder.AddPostgres("postgres") .WithPersistentLifetime() .WithContainerName("my-shared-postgres"); builder.Build().Run(); ``` When you specify a custom container name, Aspire first checks if a container with that name already exists. If a container with that name exists and was previously created by Aspire, it follows the normal persistent container behavior and can be automatically recreated if the configuration changes. If a container with that name exists but wasn’t created by Aspire, it won’t be managed or recreated by the AppHost. If no container with the custom name exists, Aspire creates a new one. ## Executable and project naming and uniqueness [Section titled “Executable and project naming and uniqueness”](#executable-and-project-naming-and-uniqueness) Persistent executable and project resources are scoped to a specific AppHost instance and uniquely identified by their resource name within that scope. Two executable or project resources with the same name in different AppHosts don’t collide with each other; they result in separate process instances. ## Manual cleanup [Section titled “Manual cleanup”](#manual-cleanup) Caution Persistent resources aren’t automatically removed when you stop the AppHost. To delete them, stop and remove the underlying container or process with the resource’s runtime or operating system tools. For persistent containers, use Docker CLI commands, Docker Desktop, or your preferred container management tool to stop and remove the container: Stop and remove a persistent container ```bash # Stop the container docker stop my-container-name # Remove the container docker rm my-container-name ``` For persistent executable and project resources, stop the running process with your operating system process manager or terminal tools. You can also stop a persistent resource from the Aspire dashboard if the runtime-specific cleanup option isn’t straightforward. ## Container lifetime vs. data durability [Section titled “Container lifetime vs. data durability”](#container-lifetime-vs-data-durability) `WithPersistentLifetime()` and `WithDataVolume()` serve different purposes and are often used together. The following table summarizes the behavior of each combination for container resources: | Configuration | Container behavior | Data behavior | | -------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------ | | Neither (default) | Created on start, destroyed on stop | Lost every time the AppHost stops | | `WithPersistentLifetime()` only | Stays running between AppHost runs | Survives AppHost restarts, but **lost if the container is recreated** (config change, pruning, image update) | | `WithDataVolume()` only | Created on start, destroyed on stop | Persists in a named volume—**survives container recreation** | | Both (recommended for databases) | Stays running between AppHost runs | Persists in a named volume—survives container recreation | For **databases and other stateful services**, use both APIs together so you get fast startup (the container stays running) *and* data safety (a volume protects data even if the container is recreated): * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const postgres = await builder.addPostgres('postgres'); await postgres.withPersistentLifetime(); await postgres.withDataVolume(); ``` * C# AppHost.cs ```csharp var postgres = builder.AddPostgres("postgres") .WithPersistentLifetime() .WithDataVolume(); ``` For **caches or other ephemeral state**, `WithPersistentLifetime()` alone may be sufficient because losing data on container recreation is acceptable. Tip For more details on volumes and bind mounts, see [Persist data using volumes](/fundamentals/persist-data-volumes/). # TypeScript AppHost project structure > Learn the files and configuration that make up a TypeScript AppHost project — entry point, package manifest, dependencies, and how the AppHost runs Aspire resources. When you create a TypeScript AppHost with `aspire new`, the CLI scaffolds a project with the following structure: * my-apphost/ * .aspire/modules/ Generated TypeScript SDK (do not edit) * aspire.mts * base.mts * transport.mts * apphost.mts Your AppHost entry point * aspire.config.json Aspire configuration * package.json * tsconfig.apphost.json When you run `aspire init --language typescript` in an existing JavaScript or TypeScript app that already has a root `package.json`, Aspire creates the AppHost in a nested `aspire-apphost/` package. The root `aspire.config.json` points to `aspire-apphost/apphost.mts`, and the root `package.json` gets Aspire delegate scripts so the existing app package keeps its own module and toolchain settings: * my-existing-app/ * aspire-apphost/ * .aspire/modules/ Generated TypeScript SDK (do not edit) * aspire.mts * base.mts * transport.mts * apphost.mts Your AppHost entry point * package.json * tsconfig.apphost.json * aspire.config.json Aspire configuration * package.json Existing app package with Aspire delegate scripts ## aspire.config.json [Section titled “aspire.config.json”](#aspireconfigjson) The `aspire.config.json` file is the central configuration for your AppHost. It replaces the older `.aspire/settings.json` and `apphost.run.json` files. aspire.config.json ```json { "appHost": { "path": "apphost.mts", "language": "typescript/nodejs" }, "packages": { "Aspire.Hosting.JavaScript": "13.5.3" }, "profiles": { "https": { "applicationUrl": "https://localhost:17127;http://localhost:15118", "environmentVariables": { "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21169", "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22260" } } } } ``` ### Key sections [Section titled “Key sections”](#key-sections) | Section | Description | | ------------------ | ------------------------------------------------------------------------------------- | | `appHost.path` | Path to your AppHost entry point (`apphost.mts`) | | `appHost.language` | Language runtime (`typescript/nodejs`) | | `packages` | Hosting integration packages and their versions. Added automatically by `aspire add`. | | `profiles` | Launch profiles with dashboard URLs and environment variables | ### Add and restore integrations [Section titled “Add and restore integrations”](#add-and-restore-integrations) Use `aspire add` from the AppHost root to add hosting integrations. The CLI adds the package to the `packages` section, restores AppHost dependencies, and regenerates the TypeScript SDK in `.aspire/modules/`: Add an integration ```bash aspire add redis ``` This updates `aspire.config.json` so the package is restored the next time the AppHost runs: aspire.config.json ```diff { "packages": { "Aspire.Hosting.JavaScript": "13.5.3", "Aspire.Hosting.Redis": "13.5.3" } } ``` Run `aspire restore` when you want to regenerate `.aspire/modules/` without starting the AppHost, such as after switching branches, updating package versions, or preparing a CI job: Restore a TypeScript AppHost ```bash aspire restore ``` ### Project references for local development [Section titled “Project references for local development”](#project-references-for-local-development) You can reference a local hosting integration project by using a `.csproj` path instead of a version: aspire.config.json ```json { "packages": { "MyIntegration": "../src/MyIntegration/MyIntegration.csproj" } } ``` See [Multi-language integrations](/extensibility/multi-language-integration-authoring/) for details on building hosting integrations that work with TypeScript AppHosts. ## .aspire/modules/ directory [Section titled “.aspire/modules/ directory”](#aspiremodules-directory) The `.aspire/modules/` directory under the AppHost root contains the generated TypeScript SDK. It’s created and updated automatically by the Aspire CLI — **do not edit these files**. | File | Purpose | | --------------- | ------------------------------------------------------- | | `aspire.mts` | Generated typed API for all your installed integrations | | `base.mts` | Base types and handle infrastructure | | `transport.mts` | JSON-RPC transport layer | The SDK regenerates when: * You run `aspire add` to add or update an integration * You run `aspire run` or `aspire start` and the package list has changed * You run `aspire restore` to manually regenerate Your `apphost.mts` imports from this SDK: apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; ``` Tip Add `.aspire/` to your `.gitignore` — it contains generated artifacts that can be recreated from `aspire.config.json` at any time. ## apphost.mts [Section titled “apphost.mts”](#apphostmts) The entry point for your AppHost. This is where you define your application’s resources and their relationships: apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const cache = await builder.addRedis("cache"); const api = await builder .addNodeApp("api", "./api", "src/index.ts") .withHttpEndpoint({ env: "PORT" }) .withReference(cache); await builder.build().run(); ``` Note Projects created with Aspire CLI versions earlier than 13.4 used `apphost.ts` and `./.modules/`. They continue to work on Aspire 13.4. For compatibility details and optional migration steps, see [Legacy `apphost.ts` projects in the Aspire 13.4 release notes](/whats-new/aspire-13-4/#legacy-apphostts-projects-pre-134). ## Package managers [Section titled “Package managers”](#package-managers) The Aspire CLI supports the following package managers at the **AppHost root** — the directory that contains your `apphost.mts` and `aspire.config.json`. The CLI selects between them by inspecting package manager signals, including the `packageManager` field in `package.json`, lock files, and package manager configuration in the AppHost root. | Package manager | Detection signals | Version expectation | | ----------------- | ------------------------------------------------------------------------- | ----------------------------------- | | npm | `packageManager`, `package-lock.json`, or no other signal | npm 10 or later; npm is the default | | pnpm | `packageManager` or `pnpm-lock.yaml` | pnpm 10 or later | | Yarn | `packageManager`, `yarn.lock`, `.yarnrc.yml`, or `.yarn/` | Yarn 4 or later (Berry) | | Bun | `packageManager`, `bun.lock`, or `bun.lockb` | Bun 1.2 or later | | Yarn Classic (v1) | `yarn.lock` with `# yarn lockfile v1` or `packageManager` with `yarn@1.x` | Not supported | This policy governs the **AppHost root only**. Apps the AppHost orchestrates — for example, a Node.js service added with `addNodeApp`, a Bun guest app, or a workspace package — can use any package manager their own tooling requires; they are independent of the AppHost-root toolchain. Aspire end-to-end tests cover TypeScript AppHosts with representative `packageManager` pins such as `npm@10.0.0`, `pnpm@10.0.0`, `yarn@4.14.1`, and `bun@1.2.0`. These tested versions are representative points within the supported ranges, not the only versions you can use. Caution **Yarn Classic (v1) is not supported.** If the Aspire CLI detects a Yarn Classic lock file (`# yarn lockfile v1`) or a `packageManager` field such as `"yarn@1.x"` in `package.json`, it throws an error and stops. Upgrade to Yarn 4 or later, or switch to npm, pnpm, or Bun. To upgrade to Yarn 4, run: Upgrade to Yarn 4 ```bash corepack enable yarn corepack use yarn@stable ``` ## package.json [Section titled “package.json”](#packagejson) The scaffolded `package.json` includes convenience scripts and the required Node.js version: package.json ```json { "name": "my-apphost", "private": true, "type": "module", "scripts": { "aspire:lint": "eslint apphost.mts", "aspire:start": "aspire run", "aspire:build": "tsc -p tsconfig.apphost.json", "aspire:dev": "tsc --watch -p tsconfig.apphost.json", "lint": "npm run aspire:lint", "dev": "npm run aspire:start", "build": "npm run aspire:build", "watch": "npm run aspire:dev" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } } ``` The `dev` script means you can also start your AppHost with `npm run dev` (or the equivalent for your toolchain, for example `bun run dev` or `pnpm run dev`). ### Supported Node.js engine [Section titled “Supported Node.js engine”](#supported-nodejs-engine) TypeScript AppHosts target the Node.js engine range that `aspire init` writes into the scaffolded AppHost `package.json`: package.json — supported engines.node ```json { "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } } ``` The supported ranges are: * **Node.js 20.19+** — supported. * **Node.js 22.13+** (22.x LTS) — supported. * **Node.js 24.x and later** — supported by the scaffolded `engines.node` constraint. Older Node.js versions are not supported. Package managers may enforce `engines.node` on install (for example, pnpm does by default, while npm only does when `engine-strict` is configured), so unsupported runtimes are best treated as blocked even when a given toolchain only warns. Note If you widen `engines.node` beyond the scaffolded constraint, you take on responsibility for compatibility. Aspire CLI behavior, TypeScript SDK generation, and the `tsc --noEmit` startup validation are tested against the ranges listed above. ## Package manager toolchain [Section titled “Package manager toolchain”](#package-manager-toolchain) The Aspire CLI automatically detects which Node-compatible package manager your project uses and adjusts install and run commands accordingly. The following toolchains are supported: **npm** (default), **Bun**, **Yarn**, and **pnpm**. ### Toolchain detection [Section titled “Toolchain detection”](#toolchain-detection) Detection follows the [supported AppHost-root package managers](#package-managers) policy. The CLI resolves the toolchain by inspecting the AppHost directory and its parent directories (up to eight levels). It checks, in order: 1. The `packageManager` field in `package.json` — for example, `"packageManager": "pnpm@10.0.0"`. 2. Lockfiles: `bun.lock` or `bun.lockb` → Bun; `pnpm-lock.yaml` → pnpm; `yarn.lock`, `.yarnrc.yml`, or a `.yarn/` directory → Yarn. 3. If nothing is found, npm is used as the default. The search walks up parent directories, so a workspace-level `packageManager` setting or lockfile is picked up automatically by nested AppHosts. ### Declaring your toolchain [Section titled “Declaring your toolchain”](#declaring-your-toolchain) The recommended way to pin the toolchain is with the `packageManager` field in `package.json`, which Aspire uses for toolchain detection. This field is also used by [Node.js Corepack](https://nodejs.org/api/corepack.html) for package managers such as Yarn and pnpm: * npm (default) No extra configuration is required — npm is the default. If you want to pin npm explicitly, set the `packageManager` field: package.json ```diff { "packageManager": "npm@10.0.0", "name": "my-apphost", "private": true, "type": "module" } ``` * pnpm package.json ```diff { "packageManager": "pnpm@10.0.0", "name": "my-apphost", "private": true, "type": "module" } ``` * Yarn package.json ```diff { "packageManager": "yarn@4.14.1", "name": "my-apphost", "private": true, "type": "module" } ``` * Bun package.json ```diff { "packageManager": "bun@1.2.0", "name": "my-apphost", "private": true, "type": "module" } ``` Alternatively, committing a toolchain-specific lockfile (`pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, etc.) is sufficient for detection. ### Install and run commands by toolchain [Section titled “Install and run commands by toolchain”](#install-and-run-commands-by-toolchain) When a non-npm toolchain is detected, the CLI substitutes the matching commands: | Toolchain | Install command | Execute command | Watch command | | --------- | --------------- | ----------------------- | ------------------------------- | | npm | `npm install` | `npx tsx ...` | `npx nodemon ...` | | pnpm | `pnpm install` | `pnpm exec tsx ...` | `pnpm exec nodemon ...` | | Yarn | `yarn install` | `yarn exec tsx ...` | `yarn exec nodemon ...` | | Bun | `bun install` | `bun run {appHostFile}` | `bun --watch run {appHostFile}` | Note Bun has built-in TypeScript support, so it runs `apphost.mts` directly without `tsx`. ### aspire doctor checks [Section titled “aspire doctor checks”](#aspire-doctor-checks) The `aspire doctor` command checks that the required JavaScript toolchain executable is available. If Bun, Yarn, or pnpm is detected but not installed, the command reports an error with install guidance. Aspire CLI ```bash aspire doctor ``` ## Async chaining [Section titled “Async chaining”](#async-chaining) TypeScript AppHosts support fluent chaining for builder methods — for example, `builder.addContainer(...).withReference(...)` — so you can build resource graphs in a compact, readable style. Starting with Aspire 13.4, the generated SDK extends this to **all** generated async methods that return a chainable wrapper type: environment helpers, execution-context queries, and endpoint property accessors. Previously, using these methods required splitting the chain or using a double `await`: apphost.ts (before) ```typescript // Two separate awaits were needed when chaining through async wrapper-returning methods const envContext = await builder.environment(); const isDevelopment = await envContext.isDevelopment(); ``` Now you can chain through them with a **single `await`**: apphost.ts (after) ```typescript const isDevelopment = await builder.environment().isDevelopment(); const isRunMode = await context.executionContext().isRunMode(); const endpointHost = await container.getEndpoint("http").property(EndpointProperty.Host); ``` This works because the code generator now emits a thenable wrapper for every generated async method whose return type is itself a chainable wrapper. Note The thenable wrappers are generated automatically — you do not need to change `aspire.config.json` or run any extra commands. Re-running `aspire run` or `aspire restore` after upgrading your `Aspire.Hosting.JavaScript` package version regenerates the `.aspire/modules/` SDK with the updated types. ## TypeScript validation before startup [Section titled “TypeScript validation before startup”](#typescript-validation-before-startup) Before starting a TypeScript AppHost, the Aspire CLI runs `tsc --noEmit` to check for type errors to prevent the dashboard and resources from starting in a partially broken state. If your AppHost has TypeScript compile errors, `aspire run` and `aspire publish` stop before the AppHost launches and display the diagnostic output: ```text apphost.mts(22,7): error TS2322: Type 'string' is not assignable to type 'number'. ``` ### Watch mode behavior [Section titled “Watch mode behavior”](#watch-mode-behavior) When you use `aspire run` in watch mode, the TypeScript validation is embedded in the nodemon restart command. The watcher **can still start** even if there are initial type errors — it will recover automatically as you edit and save files that fix the errors. ## HTTPS development certificates [Section titled “HTTPS development certificates”](#https-development-certificates) When you run a TypeScript AppHost with an HTTPS launch profile, the Aspire CLI needs a trusted HTTPS development certificate to be present on your machine. Unlike .NET AppHost users who typically have the .NET SDK on their `PATH`, TypeScript AppHost users may not have `dotnet` available, so the Aspire CLI provides its own certificate management commands. If you see an error similar to the following when running `aspire run`: ```text Unable to configure HTTPS endpoint. No server certificate was specified, and the default developer certificate could not be found or is out of date. To generate and trust a developer certificate run 'aspire certs trust'. For more information on configuring HTTPS see https://aspire.dev/docs/. ``` Run the following command to create and trust the development certificate: Aspire CLI ```bash aspire certs trust ``` Tip If you continue to see certificate errors after running `aspire certs trust`, try cleaning existing certificates first: Aspire CLI ```bash aspire certs clean aspire certs trust ``` ### Trusting the certificate for outbound TLS connections [Section titled “Trusting the certificate for outbound TLS connections”](#trusting-the-certificate-for-outbound-tls-connections) When your AppHost code opens TLS connections to Aspire-managed resources at runtime (for example, connecting directly to the dashboard’s OTLP endpoint from custom AppHost logic), the Aspire CLI ensures Node.js trusts the same development certificate that the Developer Control Plane (DCP) uses. The CLI exports the trusted development certificate into a content-addressed PEM cache under the Aspire home directory (`ASPIRE_HOME`), and Node.js is configured to trust that bundle through the `NODE_EXTRA_CA_CERTS` environment variable. If you’ve already set `NODE_EXTRA_CA_CERTS` for your own certificates, the CLI preserves your value by generating a combined bundle that includes both your certificates and the Aspire development certificate, rather than overwriting your setting. See [Certificate configuration](/app-host/certificate-configuration/) for details on HTTPS certificate management in Aspire, including Linux-specific setup. ## Troubleshooting codegen failures [Section titled “Troubleshooting codegen failures”](#troubleshooting-codegen-failures) When the Aspire CLI runs a TypeScript (Node.js) AppHost, it contacts a managed server to generate the TypeScript SDK. That server loads NuGet-restored packages (`Aspire.Hosting.JavaScript`, `Aspire.TypeSystem`, and related assemblies). If the CLI version and the SDK version in `aspire.config.json` differ in major, minor, patch, or prerelease identifiers (excluding build metadata), code generation can fail. ### CLI/SDK version mismatch warning [Section titled “CLI/SDK version mismatch warning”](#clisdk-version-mismatch-warning) Before starting the AppHost, the CLI compares its built-in SDK version against the `sdk.version` value in `aspire.config.json`. When the two differ, the CLI prints a warning: ```text ⚠ The installed Aspire CLI version () differs from the configured Aspire SDK version (). If you run into errors, run 'aspire update' to align them. ``` The AppHost still starts after the warning. If codegen succeeds, no further action is required. If codegen fails, run `aspire update` to realign the CLI and SDK to the same version. ### Codegen failure output [Section titled “Codegen failure output”](#codegen-failure-output) When code generation fails, the CLI exits immediately and prints: ```text ❌ TypeScript (Node.js) SDK code generation failed because the installed Aspire CLI appears to be incompatible with the configured Aspire SDK. Run 'aspire update' to align the CLI and SDK and try again. ℹ Run 'aspire update' to align the installed Aspire CLI with the configured SDK version, then retry. ℹ Run with '--debug' for full diagnostic details. ``` Tip Prior to this improvement, a CLI/SDK version mismatch produced an empty `System.TypeLoadException` message followed by a 60-second timeout. If you saw that behaviour, update the CLI with `aspire update`. ### Getting full diagnostic details [Section titled “Getting full diagnostic details”](#getting-full-diagnostic-details) Pass `--debug` to `aspire run` for detailed diagnostic output: Aspire CLI ```bash aspire run --debug ``` In addition to the standard failure message, the CLI prints a diagnostic block: ```text 🔬 Diagnostic details: Exception: System.TypeLoadException Type: Aspire.Hosting.SomeType Runtime Aspire.Hosting: + • Aspire.Hosting.CodeGeneration.TypeScript + • Aspire.TypeSystem + ``` The same information is always written to the CLI log file at `~/.aspire/logs/cli_*.log`, even when `--debug` is not passed. ### Resolving the mismatch [Section titled “Resolving the mismatch”](#resolving-the-mismatch) Run `aspire update` to update the packages in `aspire.config.json` to match the installed CLI version, or reinstall the CLI to match the SDK version already in use: Aspire CLI ```bash aspire update ``` ## See also [Section titled “See also”](#see-also) * [Build your first app](/get-started/first-app/?lang=typescript) * [AppHost overview](/get-started/app-host/) * [Multi-language architecture](/architecture/multi-language-architecture/) * [aspire doctor command](/reference/cli/commands/aspire-doctor/) * [aspire update command](/reference/cli/commands/aspire-update/) * [aspire certs trust command](/reference/cli/commands/aspire-certs-trust/) * [Certificate configuration](/app-host/certificate-configuration/) # Test TUI and shell apps using WithTerminal > Use WithTerminal to expose an interactive terminal for a resource in your Aspire app model, then attach to it from the dashboard or the aspire terminal CLI to drive TUI and shell-based experiences. If you have a terminal user interface (TUI) application or a shell-based experience that you want to exercise while it runs under Aspire, add `WithTerminal(...)` to the resource. Aspire then exposes an interactive terminal session that you can attach to from the [Aspire dashboard](/dashboard/overview/) or from the [`aspire terminal`](/reference/cli/commands/aspire-terminal/) CLI command. * TypeScript apphost.ts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const agent = await builder.addExecutable("agent", "my-agent", ".") .withTerminal(); await builder.build().run(); ``` * C# AppHost.cs ```csharp #pragma warning disable ASPIRETERMINAL001 var builder = DistributedApplication.CreateBuilder(args); var agent = builder.AddExecutable("agent", "my-agent", ".") .WithTerminal(); builder.Build().Run(); ``` Once the app is running, open the resource’s terminal page in the dashboard—or run `aspire terminal attach agent`—to interact with the process just as you would in a local shell. Experimental `WithTerminal` is an experimental API. Calling it in C# produces the `ASPIRETERMINAL001` diagnostic, which you must acknowledge for your AppHost to build. Suppress it inline with `#pragma warning disable ASPIRETERMINAL001` (as shown in the C# example above), or add `ASPIRETERMINAL001` to `` in your project file. The shape of the API and its options may change in a future release. ## When to use WithTerminal [Section titled “When to use WithTerminal”](#when-to-use-withterminal) Reach for `WithTerminal` when a resource is interactive rather than a plain background service: * A **TUI application**—for example, an agent, a diagnostics console, or a curses-style tool—that draws a full-screen interface you want to see and drive. * A **shell-based experience** where you want an interactive prompt inside a container or executable while it runs as part of your app model. * Any resource you want to **poke at live** during development without leaving the Aspire dashboard or CLI. ## The debugger is not attached automatically [Section titled “The debugger is not attached automatically”](#the-debugger-is-not-attached-automatically) When you apply `WithTerminal`, Aspire runs the resource as a plain process and **does not automatically attach the debugger**. If you need to debug the resource, attach the debugger manually to the running process from your IDE. Note This is a temporary limitation while the implementation is completed. The orchestrator (DCP) cannot yet run a process under the debugger and a pseudo-terminal (PTY) at the same time, so for now Aspire favors a working interactive terminal over automatic IDE execution. Once both can run together, the debugger will attach automatically as usual. ## Attach from multiple places at once [Section titled “Attach from multiple places at once”](#attach-from-multiple-places-at-once) Terminal sessions support multiple simultaneous viewers. You can open **two browser tabs pointing at the same terminal**—or a browser tab and the CLI together—and both stay responsive: input and output are mirrored to every attached peer. One peer holds the **primary** role and drives the terminal’s dimensions, while the others attach as **viewers**. From the CLI you can join as a passive viewer with `aspire terminal attach --viewer`, and take control later with the `Ctrl+B T` hotkey. ## Configure the terminal [Section titled “Configure the terminal”](#configure-the-terminal) The terminal session is described by a set of options with sensible defaults: | Option | Default | Description | | ------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Columns` | `120` | The initial number of columns for the terminal grid. | | `Rows` | `30` | The initial number of rows for the terminal grid. | | `ShowTerminalHost` | `false` | Whether the hidden per-replica terminal host resources appear in the dashboard and CLI resource lists. Set to `true` to diagnose terminal-host startup or connectivity issues. | Tip `Columns` and `Rows` must each be `1` or greater. Configuring either with zero or a negative value in the `WithTerminal(...)` callback throws an `ArgumentOutOfRangeException` immediately instead of failing later during terminal-host startup. Note The resource being run is always the terminal program: for executables that’s the process itself, and for containers it’s the container’s own process. There’s no way to select a different shell to launch for the session—an earlier `Shell` option that appeared to do this was removed because it was never wired up to the underlying pseudo-terminal and had no effect. * TypeScript apphost.ts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const agent = await builder.addExecutable("agent", "my-agent", ".") .withTerminal(); await builder.build().run(); ``` Note In TypeScript AppHosts, `withTerminal()` currently applies the default options shown above. Configurable options are coming to the non-C# API as `WithTerminal` is finalized (tracked by [microsoft/aspire#18105](https://github.com/microsoft/aspire/issues/18105)). * C# In C#, pass a callback to override any of these options: AppHost.cs ```csharp #pragma warning disable ASPIRETERMINAL001 var builder = DistributedApplication.CreateBuilder(args); var agent = builder.AddExecutable("agent", "my-agent", ".") .WithTerminal(options => { options.Columns = 200; options.Rows = 50; }); builder.Build().Run(); ``` ## Terminals and replicas [Section titled “Terminals and replicas”](#terminals-and-replicas) Each replica of a resource gets its own independent terminal session. Aspire creates one terminal host per parent replica, so requesting three replicas yields three separate terminals. The order of `WithReplicas` and `WithTerminal` does not matter—the final replica count is always honored: * TypeScript apphost.ts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const agent = await builder.addExecutable("agent", "my-agent", ".") .withReplicas(3) .withTerminal(); await builder.build().run(); ``` * C# AppHost.cs ```csharp #pragma warning disable ASPIRETERMINAL001 var builder = DistributedApplication.CreateBuilder(args); // Three replicas, each with its own interactive terminal. var agent = builder.AddExecutable("agent", "my-agent", ".") .WithReplicas(3) .WithTerminal(); builder.Build().Run(); ``` When a resource has more than one replica, choose which one to attach to with `aspire terminal attach --replica ` (indices are 0-based), or pick interactively when prompted. Note Add a terminal to a resource only once. Calling `WithTerminal()` more than once on the same resource throws an exception. ## View terminals in the dashboard [Section titled “View terminals in the dashboard”](#view-terminals-in-the-dashboard) When a resource has `WithTerminal` applied, its **Console Logs** page in the [Aspire dashboard](/dashboard/overview/) gains a live terminal session alongside the usual console log stream. You can drive the running process directly in the browser without leaving the dashboard. For example, you can type commands, scroll the scrollback buffer, and switch between replicas. Each replica appears as its own entry (for example, `agent-r0`, `agent-r1`, `agent-r2`) with an independent session. The page picks a default view based on the resource’s state at the moment you navigate to it: * **Running** (the PTY is live) → the page defaults to the **Terminal** view, so the interactive session is the first thing you see. * **Waiting**/**Starting** or already **Exited**/**Finished**/**FailedToStart** → the page defaults to the **Console logs** view, so hosting messages—such as “Waiting for resource X to become healthy…” or a startup failure—and post-exit output remain visible immediately. Open the toolbar’s options (⋯) menu and choose **Terminal** or **Console logs** to switch views manually at any time. Both views stay live while you’re on the page: switching between them never tears down the terminal session or loses console log scrollback, and a later state transition (for example, `Waiting` → `Running`) doesn’t auto-switch the view once you’ve navigated to the page. Selecting a different resource re-evaluates the default for that resource. Note The view you pick only affects what’s currently displayed—it doesn’t change what’s captured. Console logs keep streaming, and the terminal session keeps running, regardless of which view is active. ## Access terminals from VS Code [Section titled “Access terminals from VS Code”](#access-terminals-from-vs-code) In the [Aspire VS Code extension](/get-started/aspire-vscode-extension/), any resource configured with `WithTerminal()` gains an **Open terminal** entry in its right-click context menu. Selecting **Open terminal** runs `aspire terminal attach ` and opens the session as an editor-style terminal tab directly in VS Code—no separate shell window needed. For multi-replica resources, the extension automatically passes `--replica ` so the correct instance is attached. The `--apphost` flag is included when the current AppHost connection information is available, so the command connects without requiring you to specify the AppHost separately. Note The **Open terminal** context-menu item is only visible for resources that have `terminal.enabled` set, which is the case for any resource where `WithTerminal()` was called in the AppHost. Resources without `WithTerminal()` do not show this menu entry. ## Work with terminals from the CLI [Section titled “Work with terminals from the CLI”](#work-with-terminals-from-the-cli) The `aspire terminal` command group lets you list and attach to terminal sessions from your shell. Because `WithTerminal` is experimental, these commands are hidden behind a feature flag. Enable them with: Enable the aspire terminal commands ```bash aspire config set features.terminalCommandsEnabled true ``` Then: * [`aspire terminal ps`](/reference/cli/commands/aspire-terminal-ps/) lists every terminal-enabled resource in the running AppHost, with grid size, attached-peer count, and per-replica health. * [`aspire terminal attach`](/reference/cli/commands/aspire-terminal-attach/) attaches your local terminal to a resource’s interactive PTY session. ## See also [Section titled “See also”](#see-also) * [aspire terminal command](/reference/cli/commands/aspire-terminal/) * [Executable resources](/app-host/executable-resources/) * [Aspire dashboard overview](/dashboard/overview/) # Add Dockerfiles to your app model > Add Dockerfiles to your Aspire app model with WithDockerfile — build local container images, layer arguments, and pin context paths for project and executable resources. With Aspire it’s possible to specify a *Dockerfile* to build when the [AppHost](/get-started/app-host/) is started using either the `AddDockerfile` or `WithDockerfile` extension methods. These two methods serve different purposes: * **`AddDockerfile`**: Creates a new container resource from an existing Dockerfile. Use this when you want to add a custom containerized service to your app model. * **`WithDockerfile`**: Customizes an existing container resource (like a database or cache) to use a different Dockerfile. Use this when you want to modify the default container image for an Aspire component. Both methods expect an existing Dockerfile in the specified context path—neither method creates a Dockerfile for you. To generate a Dockerfile from AppHost code instead, use the [Dockerfile builder APIs](#generate-a-dockerfile-programmatically) or the [Dockerfile factory APIs](#generate-a-dockerfile-with-a-factory-function). ## When to use AddDockerfile vs WithDockerfile [Section titled “When to use AddDockerfile vs WithDockerfile”](#when-to-use-adddockerfile-vs-withdockerfile) Choose the appropriate method based on your scenario: **Use `AddDockerfile` when:** * You want to add a custom containerized service to your app model. * You have an existing Dockerfile for a custom application or service. * You need to create a new container resource that isn’t provided by Aspire components. **Use `WithDockerfile` when:** * You want to customize an existing Aspire component (like PostgreSQL, Redis, etc.). * You need to replace the default container image with a custom one. * You want to maintain the strongly typed resource builder and its extension methods. * You have specific requirements that the default container image doesn’t meet. ## Add a Dockerfile to the app model [Section titled “Add a Dockerfile to the app model”](#add-a-dockerfile-to-the-app-model) In the following example the `AddDockerfile` extension method is used to specify a container by referencing the context path for the container build. * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const container = await builder.addDockerfile( "mycontainer", "relative/context/path"); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var container = builder.AddDockerfile( "mycontainer", "relative/context/path"); ``` Unless the context path argument is a rooted path the context path is interpreted as being relative to the AppHost project directory. By default the name of the *Dockerfile* which is used is `Dockerfile` and is expected to be within the context path directory. It’s possible to explicitly specify the *Dockerfile* name either as an absolute path or a relative path to the context path. This is useful if you wish to modify the specific *Dockerfile* being used when running locally or when the AppHost is deploying. * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const container = (await builder.executionContext.isRunMode()) ? await builder.addDockerfile( "mycontainer", "relative/context/path", "Dockerfile.debug") : await builder.addDockerfile( "mycontainer", "relative/context/path", "Dockerfile.release"); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var container = builder.ExecutionContext.IsRunMode ? builder.AddDockerfile( "mycontainer", "relative/context/path", "Dockerfile.debug") : builder.AddDockerfile( "mycontainer", "relative/context/path", "Dockerfile.release"); ``` ## Customize existing container resources [Section titled “Customize existing container resources”](#customize-existing-container-resources) When using `AddDockerfile` the return value is an `IResourceBuilder`. Aspire includes many custom resource types that are derived from `ContainerResource`. Using the `WithDockerfile` extension method it’s possible to take an existing Aspire component (like PostgreSQL, Redis, or SQL Server) and replace its default container image with a custom one built from your own Dockerfile. This allows you to continue using the strongly typed resource types and their specific extension methods while customizing the underlying container. * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const pgsql = await builder.addPostgres("pgsql"); // This replaces the default PostgreSQL container image with a custom one // built from your Dockerfile, while keeping PostgreSQL-specific functionality. await pgsql.withDockerfile("path/to/context"); await pgsql.withPgAdmin(); // Still works because it's still a PostgreSQL resource. ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); // This replaces the default PostgreSQL container image with a custom one // built from your Dockerfile, while keeping PostgreSQL-specific functionality var pgsql = builder.AddPostgres("pgsql") .WithDockerfile("path/to/context") .WithPgAdmin(); // Still works because it's still a PostgreSQL resource ``` ## Generate a Dockerfile programmatically [Section titled “Generate a Dockerfile programmatically”](#generate-a-dockerfile-programmatically) Use `AddDockerfileBuilder` or `WithDockerfileBuilder` when you need Aspire to generate a Dockerfile from AppHost code. These APIs are useful when the Dockerfile depends on AppHost configuration, when you want to compose Dockerfile fragments, or when you want to keep multi-stage image build logic near the resource definition. Caution The Dockerfile builder APIs are experimental and may change in future releases. In C# AppHosts, suppress diagnostic [`ASPIREDOCKERFILEBUILDER001`](/diagnostics/aspiredockerfilebuilder001/) when you choose to use them. `AddDockerfileBuilder` creates a new container resource and configures the generated Dockerfile in one step: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; import type { DockerfileBuilderCallbackContext } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const configureDockerfile = async (context: DockerfileBuilderCallbackContext) => { const dockerfile = await context.builder(); await dockerfile .from("node:22-alpine", { stageName: "build" }) .workDir("/app") .copy("package*.json", "./") .run("npm ci") .copy(".", ".") .run("npm run build"); await dockerfile .from("nginx:alpine", { stageName: "runtime" }) .copyFrom("build", "/app/dist", "/usr/share/nginx/html") .expose(80); }; await builder.addDockerfileBuilder("frontend", "../frontend", configureDockerfile, { stage: "runtime", }); await builder.build().run(); ``` * C# AppHost.cs ```csharp using Aspire.Hosting.ApplicationModel.Docker; var builder = DistributedApplication.CreateBuilder(args); #pragma warning disable ASPIREDOCKERFILEBUILDER001 builder.AddDockerfileBuilder("frontend", "../frontend", context => { var build = context.Builder.From("node:22-alpine", "build"); build.WorkDir("/app") .Copy("package*.json", "./") .Run("npm ci") .Copy(".", ".") .Run("npm run build"); var runtime = context.Builder.From("nginx:alpine", "runtime"); runtime.CopyFrom("build", "/app/dist", "/usr/share/nginx/html") .Expose(80); return Task.CompletedTask; }, stage: "runtime"); #pragma warning restore ASPIREDOCKERFILEBUILDER001 builder.Build().Run(); ``` `WithDockerfileBuilder` applies a generated Dockerfile to an existing container resource. The image name provided when the resource is created is replaced by the generated Dockerfile build during publish: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; import type { DockerfileBuilderCallbackContext } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const configureDockerfile = async (context: DockerfileBuilderCallbackContext) => { const dockerfile = await context.builder(); await dockerfile .from("nginx:alpine", { stageName: "runtime" }) .copy(".", "/usr/share/nginx/html") .expose(80); }; await builder .addContainer("frontend", "nginx:alpine") .withDockerfileBuilder("../frontend", configureDockerfile, { stage: "runtime", }); await builder.build().run(); ``` * C# AppHost.cs ```csharp using Aspire.Hosting.ApplicationModel.Docker; var builder = DistributedApplication.CreateBuilder(args); #pragma warning disable ASPIREDOCKERFILEBUILDER001 builder.AddContainer("frontend", "nginx:alpine") .WithDockerfileBuilder("../frontend", context => { var stage = context.Builder.From("nginx:alpine", "runtime"); stage.Copy(".", "/usr/share/nginx/html") .Expose(80); return Task.CompletedTask; }, stage: "runtime"); #pragma warning restore ASPIREDOCKERFILEBUILDER001 builder.Build().Run(); ``` ## Generate a Dockerfile with a factory function [Section titled “Generate a Dockerfile with a factory function”](#generate-a-dockerfile-with-a-factory-function) Use `AddDockerfileFactory` or `WithDockerfileFactory` when you need to generate a Dockerfile as a string from AppHost code. Unlike the [Dockerfile builder APIs](#generate-a-dockerfile-programmatically) that use a fluent API to compose Dockerfile instructions, the factory APIs let you return Dockerfile content directly as a string — useful when you already have string-based Dockerfile generation logic or want to construct content conditionally. The factory callback receives a `DockerfileFactoryContext` parameter that provides access to the resource and DI services when needed. `AddDockerfileFactory` creates a new container resource and configures the generated Dockerfile in one step: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const container = await builder.addDockerfileFactory("myapp", "../myapp", async () => ` FROM node:22-alpine WORKDIR /app COPY . . RUN npm ci EXPOSE 3000 CMD ["node", "server.js"] `); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var container = builder.AddDockerfileFactory("myapp", "../myapp", async context => { // Return Dockerfile content as a string. return """ FROM node:22-alpine WORKDIR /app COPY . . RUN npm ci EXPOSE 3000 CMD ["node", "server.js"] """; }); builder.Build().Run(); ``` `WithDockerfileFactory` applies a factory-generated Dockerfile to an existing container resource: * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); await builder.addContainer("myapp", { image: "placeholder", tag: "latest" }) .withDockerfileFactory("../myapp", async () => ` FROM nginx:alpine COPY dist/ /usr/share/nginx/html EXPOSE 80 `); await builder.build().run(); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddContainer("myapp", "placeholder") .WithDockerfileFactory("../myapp", async context => { return """ FROM nginx:alpine COPY dist/ /usr/share/nginx/html EXPOSE 80 """; }); builder.Build().Run(); ``` ## Pass build arguments [Section titled “Pass build arguments”](#pass-build-arguments) The `WithBuildArg` method can be used to pass arguments into the container image build. * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const container = await builder.addDockerfile("mygoapp", "relative/context/path"); await container.withBuildArg("GO_VERSION", "1.22"); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var container = builder.AddDockerfile("mygoapp", "relative/context/path") .WithBuildArg("GO_VERSION", "1.22"); ``` The value parameter on the `WithBuildArg` method can be a literal value (`boolean`, `string`, `int`) or it can be a resource builder for a [parameter resource](/fundamentals/external-parameters/). The following code replaces the `GO_VERSION` with a parameter value that can be specified at deployment time. * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const goVersion = await builder.addParameter("goversion"); const container = await builder.addDockerfile("mygoapp", "relative/context/path"); await container.withBuildArg("GO_VERSION", goVersion); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var goVersion = builder.AddParameter("goversion"); var container = builder.AddDockerfile("mygoapp", "relative/context/path") .WithBuildArg("GO_VERSION", goVersion); ``` Build arguments correspond to the [`ARG` command](https://docs.docker.com/build/guide/build-args/) in *Dockerfiles*. Expanding the preceding example, this is a multi-stage *Dockerfile* which specifies specific container image version to use as a parameter. Dockerfile ```dockerfile # Stage 1: Build the Go program ARG GO_VERSION=1.22 FROM golang:${GO_VERSION} AS builder WORKDIR /build COPY . . RUN go build mygoapp.go # Stage 2: Run the Go program FROM mcr.microsoft.com/cbl-mariner/base/core:2.0 WORKDIR /app COPY --from=builder /build/mygoapp . CMD ["./mygoapp"] ``` Note Instead of hardcoding values into the container image, it’s recommended to use environment variables for values that frequently change. This avoids the need to rebuild the container image whenever a change is required. ## Pass build secrets [Section titled “Pass build secrets”](#pass-build-secrets) In addition to build arguments it’s possible to specify build secrets using `WithBuildSecret` which are made selectively available to individual commands in the *Dockerfile* using the `--mount=type=secret` syntax on `RUN` commands. * TypeScript apphost.mts ```typescript import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const accessToken = await builder.addParameter("accesstoken", { secret: true }); const container = await builder.addDockerfile("myapp", "relative/context/path"); await container.withBuildSecret("ACCESS_TOKEN", accessToken); ``` * C# AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var accessToken = builder.AddParameter("accesstoken", secret: true); var container = builder.AddDockerfile("myapp", "relative/context/path") .WithBuildSecret("ACCESS_TOKEN", accessToken); ``` For example, consider the `RUN` command in a *Dockerfile* which exposes the specified secret to the specific command: Dockerfile ```dockerfile # The helloworld command can read the secret from /run/secrets/ACCESS_TOKEN RUN --mount=type=secret,id=ACCESS_TOKEN helloworld ``` Caution Caution should be exercised when passing secrets in build environments. This is often done when using a token to retrieve dependencies from private repositories or feeds before a build. It is important to ensure that the injected secrets are not copied into the final or intermediate images. # Multi-language architecture > Understand how Aspire uses a guest/host architecture, ATS, SDK generation, and local token-based auth to support TypeScript AppHosts. Aspire supports writing AppHosts in multiple languages. While the orchestration engine is built on .NET, a guest/host architecture allows AppHosts written in TypeScript today, and additional languages in the future, to access Aspire integrations, service discovery, and the dashboard. ## Why a shared backend [Section titled “Why a shared backend”](#why-a-shared-backend) Aspire’s hosting integrations and deployment publishers are written in .NET and have already accumulated the runtime behavior needed for local orchestration, diagnostics, and publishing. Rewriting those integrations for every guest language would duplicate a large amount of behavior and significantly increase maintenance cost. Instead, guest languages only declare resources such as `addRedis`, `addPostgres`, and `withReference`, while the .NET host handles orchestration concerns such as starting containers, wiring service discovery, running health checks, and generating deployment artifacts. The trade-off is a local IPC hop, which is much cheaper than maintaining the same integration surface independently in multiple languages. ## Guest/host model [Section titled “Guest/host model”](#guesthost-model) When you run a TypeScript AppHost, the Aspire CLI orchestrates two processes: * **Guest**: your `apphost.mts` process, running in Node.js * **Host**: the Aspire orchestration server, running on .NET The guest communicates with the host via JSON-RPC over a local transport: Unix sockets on macOS and Linux, and named pipes on Windows. Your code calls methods such as `addRedis()` or `withReference()`, and the generated SDK translates those calls into RPC requests. ``` flowchart TB subgraph Guest["Guest Process (Node.js)"] TS["apphost.mts"] SDK["Generated SDK (.aspire/modules/)"] Client["JSON-RPC Client"] TS --> SDK --> Client end subgraph Host["Host Process (Aspire Server)"] RPC["JSON-RPC Server"] Dispatcher["Capability Dispatcher"] Integrations["Hosting Integrations"] RPC --> Dispatcher --> Integrations end subgraph Managed["Managed Resources"] Dashboard["Dashboard"] Containers["Containers"] Discovery["Service Discovery"] end Client <-->|"Local transport"| RPC Integrations --> Dashboard & Containers & Discovery ``` ## Startup sequence [Section titled “Startup sequence”](#startup-sequence) 1. The CLI prepares the host process with the required hosting packages. 2. The ATS scanner inspects assemblies for exports and generates the TypeScript SDK into `.aspire/modules/`. 3. The CLI starts the host process and creates the local socket or pipe endpoint. 4. The CLI starts the guest process and passes connection details through environment variables. 5. The guest connects and invokes capabilities such as `createBuilder`, `addRedis`, `build`, and `run`. 6. The host orchestrates resources, starts the dashboard, and manages the application lifecycle. ## Token-based authentication [Section titled “Token-based authentication”](#token-based-authentication) The guest process authenticates to the host with a one-time token generated for that session and passed through environment variables at startup. The local transport is also protected by operating system file permissions, so only processes running as the same user can connect. There are no public network ports involved in guest-to-host communication. ## Aspire Type System [Section titled “Aspire Type System”](#aspire-type-system) The Aspire Type System, or ATS, is the contract that bridges .NET and guest languages. Every exported type that crosses the boundary gets a portable type identity derived from its assembly and type name. ### Type categories [Section titled “Type categories”](#type-categories) | Category | Description | Serialization | | --------------------- | ----------------------------------------------------------- | ------------------------------------------- | | **Primitive** | `string`, `int`, `bool`, `double`, and similar scalar types | JSON native values | | **Enum** | .NET enum types | String member names | | **Handle** | Opaque references to host-side objects | JSON handle envelopes | | **DTO** | Data transfer objects marked for export | JSON objects | | **Callback** | Guest-provided delegate functions | Callback identifiers | | **Array** | Immutable collections | JSON arrays | | **List / dictionary** | Mutable collections | Handles for properties, JSON for parameters | ### How ATS maps to TypeScript [Section titled “How ATS maps to TypeScript”](#how-ats-maps-to-typescript) | .NET type | TypeScript representation | | -------------- | ---------------------------------------- | | Primitives | Native TypeScript primitives | | Enums | String literal unions | | Resource types | Typed handle objects with fluent methods | | DTOs | Interfaces serialized as JSON | | Collections | Arrays and `Record` | | Delegates | Async callback functions | Resource types are passed by handle. The actual instance remains in the host process, while the TypeScript SDK keeps a reference and dispatches method calls as JSON-RPC requests. ## Polymorphism flattening [Section titled “Polymorphism flattening”](#polymorphism-flattening) .NET APIs rely on inheritance, interfaces, and generics. Guest SDKs do not need to expose that full shape directly. During scanning, ATS flattens the exported type system so the generated guest API is easier to consume: * Concrete types receive the full set of applicable capabilities. * Interface relationships are expanded into directly callable members. * Generic constraints are resolved into exportable concrete surfaces. That flattening means a resource such as `RedisResource` can expose fluent methods from shared interfaces alongside Redis-specific APIs without requiring guest languages to model the original inheritance tree. ## SDK generation [Section titled “SDK generation”](#sdk-generation) The TypeScript SDK is generated from hosting integration assemblies. When you add an integration with `aspire add`, the CLI: 1. Loads the integration assembly. 2. Scans exported methods and types. 3. Applies ATS rules, including polymorphism flattening. 4. Emits typed TypeScript wrappers into `.aspire/modules/`. This keeps the SDK in sync with the .NET implementation. Integration authors do not hand-write TypeScript bindings; they export their .NET APIs and the CLI generates the guest surface automatically. If you’re building a hosting integration and want it to work with TypeScript AppHosts, see [Multi-language integrations](/extensibility/multi-language-integration-authoring/). ## Same model, different syntax [Section titled “Same model, different syntax”](#same-model-different-syntax) The AppHost model is the same regardless of language. A TypeScript AppHost defines the same resources, references, and dependency graph as a C# AppHost. The difference is the authoring syntax. | Concept | C# | TypeScript | | -------------- | -------------------------------------------- | --------------------------------- | | Create builder | `DistributedApplication.CreateBuilder(args)` | `await createBuilder()` | | Add resource | `builder.AddRedis("cache")` | `await builder.addRedis("cache")` | | Reference | `.WithReference(db)` | `.withReference(db)` | | Wait for | `.WaitFor(api)` | `.waitFor(api)` | | Build and run | `builder.Build().Run()` | `await builder.build().run()` | The resulting dashboard, service discovery behavior, health checks, and deployment artifacts come from the same host-side orchestration engine. ## See also [Section titled “See also”](#see-also) * [Build your first app](/get-started/first-app/?lang=typescript) — get started with a TypeScript AppHost * [Resource model](/architecture/resource-model/) — understand how Aspire models resources and relationships * [Multi-language integrations](/extensibility/multi-language-integration-authoring/) — make your integration work with TypeScript AppHosts # Aspire architecture overview > Learn the overall architecture of Aspire — the AppHost, resource model, orchestration, networking, dashboard, and how multi-language integrations fit together. Aspire brings together a powerful suite of tools and libraries, designed to deliver a seamless and intuitive experience for developers. Its modular and extensible architecture empowers you to define your application model with precision, orchestrating intricate systems composed of services, containers, and executables. Whether your components span different programming languages, platforms, stacks, or operating systems, Aspire ensures they work harmoniously, simplifying the complexity of modern cloud-native app development. ## App model architecture [Section titled “App model architecture”](#app-model-architecture) Resources are the building blocks of your app model. They’re used to represent abstract concepts like services, containers, executables, and external integrations. Specific resources enable developers to define dependencies on concrete implementations of these concepts. For example, a `Redis` resource can be used to represent a Redis cache, while a `Postgres` resource can represent a PostgreSQL database. While the app model is often synonymous with a collection of resources, it’s also a high level representation of your entire application topology. This is important, as it’s architected for lowering. In this way, Aspire can be thought of as a compiler for application topology. ### Lowering the model [Section titled “Lowering the model”](#lowering-the-model) In a traditional compiler, the process of “lowering” involves translating a high-level programming language into progressively simpler representations: * **Intermediate Representation (IR):** The first step abstracts away language-specific features, creating a platform-neutral representation. * **Machine Code:** The IR is then transformed into machine-specific instructions tailored to a specific CPU architecture. Similarly, Aspire applies this concept to applications, treating the app model as the high-level language: * **Intermediate constructs:** The app model is first lowered into intermediate constructs, such as cloud development kit (CDK)-style object graphs. These constructs might be platform-agnostic or partially tailored to specific targets. * **Target runtime representation:** Finally, a publisher generates the deployment-ready artifacts—YAML, HCL, JSON, or other formats—required by the target platform. This layered approach unlocks several key benefits: * **Validation and enrichment:** Models can be validated and enriched during the transformation process, ensuring correctness and completeness. * **Multi-target support:** Aspire supports multiple deployment targets, enabling flexibility across diverse environments. * **Customizable workflow:** Developers can hook into each phase of the process to customize behavior, tailoring the output to specific needs. * **Clean and portable models:** The high-level app model remains expressive, portable, and free from platform-specific concerns. Most importantly, the translation process itself is highly extensible. You can define custom transformations, enrichments, and output formats, allowing Aspire to seamlessly adapt to your unique infrastructure and deployment requirements. This extensibility ensures that Aspire remains a powerful and versatile tool, capable of evolving alongside your application’s needs. ### Modality and extensibility [Section titled “Modality and extensibility”](#modality-and-extensibility) Aspire operates in two primary modes, each tailored to streamline your specific needs—detailed in the following section. Both modes use a robust set of familiar APIs and a rich ecosystem of [integrations](/integrations/gallery/). Each integration simplifies working with a common service, framework, or platform, such as Redis, PostgreSQL, Azure services, or Orleans, for example. These integrations work together like puzzle pieces, enabling you to define resources, express dependencies, and configure behavior effortlessly—whether you’re running locally or deploying to production. Why is modality important when it comes to the AppHost’s execution context? This is because it allows you to define your app model once and with the appropriate APIs, specify how resources operate in each mode. Consider the following collection of resources: * Database: PostgreSQL * Cache: Redis * AI service: Ollama or OpenAI * Backend: ASP.NET Core minimal API * Frontend: React app Depending on the mode, the AppHost might treat these resources differently. For example, in run mode, the AppHost might use a local PostgreSQL database and Redis cache—using containers, while in publish mode, it might generate deployment artifacts for Azure PostgreSQL and Redis Cache. #### Run mode [Section titled “Run mode”](#run-mode) The default mode is run mode, which is ideal for local development and testing. In this mode, the Aspire AppHost orchestrates your application model, including processes, containers, and cloud emulators, to facilitate fast and iterative development. Resources behave like real runtime entities with lifecycles that mirror production. With a simple F5`F5`F5`F5`F5`F5`, the AppHost launches everything in your app model—storage, databases, caches, messaging, jobs, APIs, frontends—all fully configured and ready to debug locally. Let’s consider the app model from the previous section—where AppHost would orchestrate the following resources locally: ![Local app topology for dev-time orchestration](/_astro/local-app-topology.B_sGoIdM_Z9F0ET.svg) For more information on how run mode works, see [Dev-time orchestration](#dev-time-orchestration). #### Publish mode [Section titled “Publish mode”](#publish-mode) The publish mode generates deployment-ready artifacts tailored to your target environment. The Aspire AppHost compiles your app model into outputs like Kubernetes manifests, Terraform configs, Bicep/ARM templates, Docker Compose files, or CDK constructs—ready for integration into any deployment pipeline. The output format depends on the chosen publisher, giving you flexibility across deployment scenarios. When you consider the app model from the previous section, the AppHost doesn’t orchestrate anything—instead, it emits publish artifacts that can be used to deploy your application to a cloud provider. For example, let’s assume you want to deploy to Azure—the AppHost would emit Bicep templates that define the following resources: ![Published app topology](/_astro/publish-app-topology.MEx6uFC3_Z17AXwr.svg) ## Dev-time orchestration [Section titled “Dev-time orchestration”](#dev-time-orchestration) In run mode, [the AppHost orchestrates](/get-started/app-host/) all resources defined in your app model. But how does it achieve this? Caution The AppHost isn’t a production runtime. It’s a development-time orchestration tool that simplifies the process of running and debugging your application locally. In this section, several key questions are answered to help you understand how the AppHost orchestrates your app model: * **What powers the orchestration?** Orchestration is delegated to the [Microsoft Developer Control Plane](#developer-control-plane) (DCP), which manages resource lifecycles, startup order, dependencies, and network configurations across your app topology. * **How is the app model used?** The app model defines all resources via implementations of `IResource`, including containers, processes, databases, and external services—forming the blueprint for orchestration. * **What role does the AppHost play?** The AppHost provides a high-level declaration of the desired application state. It delegates execution to DCP, which interprets the app model and performs orchestration accordingly. * **What resources are monitored?** All declared resources—including containers, executables, and integrations—are monitored to ensure correct behavior and to support a fast and reliable development workflow. * **How are containers and executables managed?** Containers and processes are initialized with their configurations and launched concurrently, respecting the dependency graph defined in the app model. DCP ensures their readiness and connectivity during orchestration, starting resources as quickly as possible while maintaining the correct order dictated by their dependencies. * **How are resource dependencies handled?** Dependencies are defined in the app model and evaluated by DCP to determine correct startup sequencing, ensuring resources are available before dependents start. * **How is networking configured?** Networking—such as port bindings—is autoconfigured unless explicitly defined. DCP resolves conflicts and ensures availability, enabling seamless communication between services. The orchestration process follows a layered architecture. At its core, the AppHost represents the developer’s desired view of the distributed application’s resources. DCP ensures that this desired state is realized by orchestrating the resources and maintaining consistency. The [app model](/get-started/app-host/) serves as a blueprint for DCP to orchestrate your application. Under the hood, the AppHost is a .NET console application powered by the [`📦 Aspire.Hosting.AppHost`](https://www.nuget.org/packages/Aspire.Hosting.AppHost) NuGet package. This package includes build targets that register orchestration dependencies, enabling seamless dev-time orchestration. DCP is a Kubernetes-compatible API server, meaning it uses the same network protocols and conventions as Kubernetes. This compatibility allows the Aspire AppHost to leverage existing Kubernetes libraries for communication. Specifically, the AppHost contains an implementation of the `k8s.KubernetesClient` (from the [📦 KubernetesClient](https://www.nuget.org/packages/KubernetesClient) NuGet package), which is a .NET client for Kubernetes. This client is used to communicate with the DCP API server, enabling the AppHost to delegate orchestration tasks to DCP. When you run the AppHost, it performs the first step of “lowering” by translating the general-purpose Aspire app model into a DCP-specific model tailored for local execution in run mode. This DCP model is then handed off to DCP, which evaluates it and orchestrates the resources accordingly. This separation ensures that the AppHost focuses on adapting the Aspire app model for local execution, while DCP specializes in executing the tailored model. The following diagram helps to visualize this orchestration process: ![Flow diagram showing AppHost delegating to DCP](/_astro/app-host-dcp-flow.BZQuKk3r_1GhuRb.svg) ### Developer control plane [Section titled “Developer control plane”](#developer-control-plane) DCP is at the core of the Aspire AppHost orchestration functionality. It’s responsible for orchestrating all resources defined in your app model, starting the developer dashboard, ensuring that everything is set up correctly for local development and testing. DCP manages the lifecycle of resources, applies network configurations, and resolves dependencies. DCP is written in Go, aligning with Kubernetes and its ecosystem, which are also Go-based. This choice enables deep, native integration with Kubernetes APIs, efficient concurrency, and access to mature tooling like Kubebuilder. DCP is delivered as two executables: * `dcp.exe`: API server that exposes a Kubernetes-like API endpoint for the AppHost to communicate with. Additionally, it exposes log streaming to the AppHost, which ultimately streams logs to the developer dashboard. * `dcpctrl.exe`: Controller that monitors the API server for new objects and changes, ensuring that the real-world environment matches the specified model. Note DCP operates on the principle of “eventual consistency,” meaning that changes to the model and the real-world environment are applied asynchronously. While this approach may introduce noticeable delays, DCP is designed to diligently synchronize both states. Unlike a “strongly consistent” system that might fail immediately on encountering issues, DCP persistently retries until the desired state is achieved or an error is conclusively determined, often resulting in a more robust alignment between the model and the real world. When the AppHost runs, it uses Kubernetes client libraries to communicate with DCP. It translates the app model into a format DCP can process by converting the model’s resources into specifications. Specifically, this involves generating Kubernetes Custom Resource Definitions (CRDs) that represent the application’s desired state. DCP performs the following tasks: * Prepares the resources for execution: * Configures service endpoints. * Assigns names and ports dynamically, unless explicitly set (DCP ensures that the ports are available and not in use by other processes). * Initializes container networks. * Pulls container images based on their applied `ImagePullPolicy`. * Creates and starts containers. * Runs executables with the required arguments and environment variables. * Monitors resources: * Provides change notifications about objects managed within DCP, including process IDs, running status, and exit codes (the AppHost subscribes to these changes to manage the [application’s lifecycle](/app-host/eventing/) effectively). * Starts the developer dashboard. Continuing from the diagram in the previous section, consider the following diagram that helps to visualize the responsibilities of DCP: ![Architecture diagram showing DCP components](/_astro/dcp-arch.9Tyj5BPk_14dpUB.svg) DCP logs are streamed back to the AppHost, which then forwards them to the developer dashboard. While the developer dashboard exposes commands such as start, stop, and restart, these commands are not part of DCP itself. Instead, they are implemented by the app model runtime, specifically within its “dashboard service” component. These commands operate by manipulating DCP objects—creating new ones, deleting old ones, or updating their properties. For example, restarting a .NET project involves stopping and deleting the existing `ExecutableResource` representing the project and creating a new one with the same specifications. ## Developer dashboard [Section titled “Developer dashboard”](#developer-dashboard) The Aspire developer dashboard is a powerful tool designed to simplify local development and resource management. It also supports a standalone mode and integrates seamlessly when publishing to Azure Container Apps. With its intuitive interface, the dashboard empowers developers to monitor, manage, and interact with application resources effortlessly. ### Monitor and manage resources [Section titled “Monitor and manage resources”](#monitor-and-manage-resources) The dashboard provides a user-friendly interface for inspecting resource states, viewing logs, and executing commands. Whether you’re debugging locally or deploying to the cloud, the dashboard ensures you have full visibility into your application’s behavior. ### Built-in and custom commands [Section titled “Built-in and custom commands”](#built-in-and-custom-commands) The dashboard provides a set of commands for managing resources, such as start, stop, and restart. While commands appear as intuitive actions in the dashboard UI, under the hood, they operate by manipulating DCP objects. For more information, see *Stop or Start a resource*. In addition to these built-in commands, you can define custom commands tailored to your application’s needs. These custom commands are registered in the app model and seamlessly integrated into the dashboard, providing enhanced flexibility and control. ### Real-time log streaming [Section titled “Real-time log streaming”](#real-time-log-streaming) Stay informed with the dashboard’s real-time log streaming feature. Logs from all resources in your app model are streamed from DCP to the AppHost and displayed in the dashboard. With advanced filtering options—by resource type, severity, and more—you can quickly pinpoint relevant information and troubleshoot effectively. The developer dashboard is more than just a tool—it’s your command center for building, debugging, and managing Aspire applications with confidence and ease. # Resource API Patterns > Discover common API resource patterns in Aspire, including how to add and configure resources, use annotations, and compose hosting and client integrations. Aspire’s resource model allows you to define and configure resources in a structured way, enabling seamless integration and management of your application’s components. This guide provides details the common patterns for adding and configuring resources in Aspire. ## API patterns [Section titled “API patterns”](#api-patterns) Aspire separates **resource data models** from **behavior** using **fluent extension methods**. * **Resource classes** define only constructors and properties. * **Extension methods** implement resource creation, configuration, and runtime wiring. This guide describes each pattern and shows a **verbatim Redis example** at the end. It also covers how to publish manifests via custom resources. ## Adding resources with `AddX(...)` [Section titled “Adding resources with AddX(...)”](#adding-resources-with-addx) An `AddX(...)` method executes: 1. **Validate inputs** (`builder`, `name`, required arguments). 2. **Instantiate** the data-only resource (`new TResource(...)`). 3. **Register** it with `builder.AddResource(resource)`. 4. **Optional wiring** of endpoints, health checks, container settings, environment variables, command-line arguments, and event subscriptions. ### Signature pattern [Section titled “Signature pattern”](#signature-pattern) * TypeScript ```typescript // Resources are added via async builder methods: const resource = await builder.addRedis("name" /*, optional params */); // The builder handles validation, instantiation, // and registration internally. Optional wiring: // .withEndpoint(...) // .withHealthCheck(...) // .withImage(...) // .withEnvironment(...) // .withArgs(...) // builder.addEventingSubscriber(...) ``` * C# ```csharp public static IResourceBuilder AddX( this IDistributedApplicationBuilder builder, [ResourceName] string name, /* optional parameters */) { // 1. Validate inputs // 2. Instantiate resource // 3. builder.AddResource(resource) // 4. Optional wiring: // .WithEndpoint(...) // .WithHealthCheck(...) // .WithImage(...) // .WithEnvironment(...) // .WithArgs(...) // Eventing.Subscribe<...>(...) } ``` ### Optional wiring examples [Section titled “Optional wiring examples”](#optional-wiring-examples) **Endpoints**: * TypeScript ```typescript resource.withEndpoint({ port: hostPort, targetPort: containerPort, name: endpointName }); ``` * C# ```csharp .WithEndpoint(port: hostPort, targetPort: containerPort, name: endpointName) ``` **Health checks**: * TypeScript ```typescript resource.withHealthCheck(healthCheckKey); ``` * C# ```csharp .WithHealthCheck(healthCheckKey) ``` **Container images / registries**: * TypeScript ```typescript resource.withImage(imageName, imageTag); resource.withImageRegistry(registryUrl); ``` * C# ```csharp .WithImage(imageName, imageTag) .WithImageRegistry(registryUrl) ``` **Entrypoint and args**: * TypeScript ```typescript resource.withEntrypoint("/bin/sh"); resource.withArgs(["--flag", "value"]); ``` * C# ```csharp .WithEntrypoint("/bin/sh") .WithArgs(context => { /* build args */ return Task.CompletedTask; }) ``` **Environment variables**: * TypeScript ```typescript resource.withEnvironment("ENV_VAR", value); ``` * C# ```csharp .WithEnvironment(context => new("ENV_VAR", valueProvider)) ``` **Event subscriptions**: * TypeScript ```typescript builder.addEventingSubscriber(async (context) => { context.onBeforeStart(async (event) => { // Handle event }); }); ``` * C# ```csharp builder.Eventing.Subscribe(resource, handler); ``` ### Summary table [Section titled “Summary table”](#summary-table) | Step | Call/Method | Purpose | | --------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | Validate | `ArgumentNullException.ThrowIfNull(...)` | Ensure non-null builder, name, and args | | Instantiate | `new TResource(name, …)` | Create data-only instance | | Register | `builder.AddResource(resource)` | Add resource to the application model | | Optional wiring | `.WithEndpoint(…)`, `.WithHealthCheck(…)`, `.WithImage(…)`, `.WithEnvironment(…)`, `.WithArgs(…)`, `Eventing.Subscribe(…)` | Configure container details, wiring, and runtime hooks | ## Configuring resources with `WithX(...)` [Section titled “Configuring resources with WithX(...)”](#configuring-resources-with-withx) `WithX(...)` methods **attach annotations** to resource builders. ### Signature pattern [Section titled “Signature pattern”](#signature-pattern-1) * TypeScript Note The TypeScript SDK doesn’t expose `withAnnotation(...)` directly. Configuration methods like `withEndpoint(...)` and `withEnvironment(...)` handle annotation attachment internally. * C# ```csharp public static IResourceBuilder WithX( this IResourceBuilder builder, FooOptions options) => builder.WithAnnotation(new FooAnnotation(options)); ``` - **Target**: `IResourceBuilder`. - **Action**: `WithAnnotation(...)`. - **Returns**: `IResourceBuilder`. ### Summary table [Section titled “Summary table”](#summary-table-1) | Method | Target | Action | | ------------ | ----------------------------- | ------------------------------------------------------ | | `WithX(...)` | `IResourceBuilder` | Attaches `XAnnotation` using the `WithAnnotation` API. | | Returns | `IResourceBuilder` | Enables fluent chaining . | ## Annotations [Section titled “Annotations”](#annotations) Annotations are **public** metadata types implementing `IResourceAnnotation`. They can be added or removed dynamically at runtime via hooks or events. Consumers can query annotations using `TryGetLastAnnotation()` when necessary. ### Definition and attachment [Section titled “Definition and attachment”](#definition-and-attachment) * TypeScript Note The TypeScript SDK doesn’t expose annotations as a user-facing pattern. Resource metadata is managed internally by the SDK’s configuration methods. * C# ```csharp public sealed record PersistenceAnnotation( TimeSpan? Interval, int KeysChangedThreshold) : IResourceAnnotation; builder.WithAnnotation(new PersistenceAnnotation( TimeSpan.FromSeconds(60), 100)); ``` ### Summary table [Section titled “Summary table”](#summary-table-2) | Concept | Pattern | Notes | | --------------- | ------------------------------------------------------- | ---------------------------------------- | | Annotation Type | `public record XAnnotation(...) : IResourceAnnotation` | Public to support dynamic runtime use. | | Attach | `builder.WithAnnotation(new XAnnotation(...))` | Adds metadata to resource builder. | | Query | `resource.TryGetLastAnnotation(out var a)` | Consumers inspect annotations as needed. | ## Custom value objects [Section titled “Custom value objects”](#custom-value-objects) Custom value objects defer evaluation and allow the framework to discover dependencies between resources. ### Core interfaces [Section titled “Core interfaces”](#core-interfaces) | Interface | Member | Mode | Purpose | | ------------------------------- | ----------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------- | | `IValueProvider` | `ValueTask GetValueAsync(CancellationToken)` | Run | Resolve live values at runtime | | `IManifestExpressionProvider` | `string ValueExpression { get; }` | Publish | Emit structured expressions in manifests | | `IExpressionValue` | Inherits `IValueProvider` and `IManifestExpressionProvider` | Run and publish | Mark a value object as usable wherever an expression-backed value is accepted | | `IValueWithReferences` *(opt.)* | `IEnumerable References { get; }` | Both (if needed) | Declare dependencies on other resources | * **Implement** `IValueProvider` and `IManifestExpressionProvider` on all structured value types. * **Implement** `IExpressionValue` when a structured value type should be accepted by APIs such as `WithEnvironment(...)`. * **Implement** `IValueWithReferences` only when your type holds resource references. ### Attaching to resources [Section titled “Attaching to resources”](#attaching-to-resources) * TypeScript ```typescript resource.withEnvironment("REDIS_CONNECTION_STRING", redis); ``` * C# ```csharp builder.WithEnvironment(context => new("REDIS_CONNECTION_STRING", redis.GetConnectionStringAsync)); ``` - TypeScript Note In the TypeScript SDK, `BicepOutputReference` and the value provider interfaces exist as read-only types. Users consume these types (e.g., via `resource.getOutput("name")`) but don’t implement custom value objects. - C# Example: BicepOutputReference ```csharp public sealed partial class BicepOutputReference : IManifestExpressionProvider, IValueProvider, IValueWithReferences { public string ValueExpression { get; } public ValueTask GetValueAsync(CancellationToken cancellationToken = default); IEnumerable IValueWithReferences.References { get; } } ``` * TypeScript Note In the TypeScript SDK, `withEnvironment()` handles value binding directly without requiring custom annotation types. * C# ```csharp public static IResourceBuilder WithEnvironment( this IResourceBuilder builder, string name, BicepOutputReference bicepOutputReference) where T : IResourceWithEnvironment { return builder.WithAnnotation( new EnvironmentVariableAnnotation(name, bicepOutputReference)); } ``` ### Summary table [Section titled “Summary table”](#summary-table-3) | Concept | Pattern | Purpose | | ------------------------------- | ------------------------------------------------ | ------------------------------------ | | `IValueProvider` | `GetValueAsync(...)` | Deferred runtime resolution | | `IManifestExpressionProvider` | `ValueExpression` | Structured publish-time expression | | `IExpressionValue` | `IValueProvider` + `IManifestExpressionProvider` | Reusable expression-backed value | | `IValueWithReferences` *(opt.)* | `References` | Declare resource dependencies | | `WithEnvironment(...)` | `new("NAME", valueProvider)` | Attach structured values unflattened | # Examples > Explore complete, runnable examples that demonstrate Aspire's resource model — custom container resources, connection strings, health checks, and event subscriptions. Aspire provides a flexible resource model that allows you to define and configure resources in a structured way. This guide explores common patterns for adding and configuring resources, including examples of custom resources and how to implement them. ## Example: Derived Container Resource (Redis) [Section titled “Example: Derived Container Resource (Redis)”](#example-derived-container-resource-redis) This example shows how to create a custom resource (`RedisResource`) that derives from `ContainerResource` and implements `IResourceWithConnectionString`. It demonstrates: * Defining a data-only resource class. * Implementing `IResourceWithConnectionString` with deferred evaluation using `ReferenceExpression`. * Creating an `AddRedis` extension method that handles parameter validation, password management, event subscription, health checks, and container configuration using fluent APIs. RedisResourceExtensions.cs ```csharp public static class RedisResourceExtensions { // This extension method provides a convenient way to add a Redis resource to the Aspire application model. public static IResourceBuilder AddRedis( this IDistributedApplicationBuilder builder, // Extends the main application builder interface. [ResourceName] string name, // The unique name for this Redis resource. int? port = null, // Optional host port mapping. IResourceBuilder? password = null) // Optional parameter resource for the password. { // 1. Validate inputs before any side effects // Ensure the builder and name are not null to prevent downstream errors. ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(name); // 2. Preserve or generate the password ParameterResource (deferred evaluation) // If a password parameter is provided, use it. Otherwise, create a default one. // ParameterResource allows the actual password value to be resolved later (e.g., from secrets). var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter( builder, $"{name}-password", special: false); // Creates a default password parameter if none is supplied. // 3. Instantiate the data-only RedisResource with its password parameter // Create the RedisResource instance, passing the name and the (potentially deferred) password parameter. var redis = new RedisResource(name, passwordParameter); // Variable to hold the resolved connection string at runtime. string? connectionString = null; // 4. Use OnConnectionStringAvailable to capture the connection string at runtime. // This event hook allows capturing the connection string *after* it has been resolved // by the Aspire runtime, including potentially allocated ports and resolved parameter values. var redisBuilder = builder.AddResource(redis) .OnConnectionStringAvailable(async (resource, @event, ct) => { // Resolve the connection string using the resource's method. connectionString = await resource.GetConnectionStringAsync(ct).ConfigureAwait(false); // Ensure the connection string was actually resolved. if (connectionString == null) { throw new DistributedApplicationException( $"Connection string for '{resource.Name}' was unexpectedly null."); } }); // 5. Register a health check that uses the connection string once it becomes available // Define a unique key for the health check. var healthCheckKey = $"{name}_check"; // Add a Redis-specific health check to the application's health check services. // The lambda `_ => connectionString ?? ...` ensures the health check uses the // connection string *after* it has been resolved by the event handler above. builder.Services .AddHealthChecks() .AddRedis(_ => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"), // Throw if accessed too early. name: healthCheckKey); // Name the health check for identification. // 6. Configure the container using the fluent builder pattern. // Continue configuring the RedisResource through its existing builder. return redisBuilder // 6.a Expose the Redis TCP endpoint // Map the host port (if provided) to the container's default Redis port (6379). // Name the endpoint "tcp" for reference. .WithEndpoint( port: port, // Optional host port. targetPort: 6379, // Default Redis port inside the container. name: RedisResource.PrimaryEndpointName) // Use the constant defined in RedisResource. // 6.b Specify container image and tag // Define the Docker image to use for the Redis container. .WithImage(RedisContainerImageTags.Image, RedisContainerImageTags.Tag) // 6.c Configure container registry if needed // Specify a container registry if the image is not on Docker Hub. .WithImageRegistry(RedisContainerImageTags.Registry) // 6.d Wire the health check into the resource // Associate the previously defined health check with this resource. // Aspire uses this for dashboard status and orchestration. .WithHealthCheck(healthCheckKey) // 6.e Define the container's entrypoint // Override the default container entrypoint if necessary. Here, it's set to use shell. .WithEntrypoint("/bin/sh") // 6.f Pass the password ParameterResource into an environment variable // Set environment variables for the container. This uses a callback to access // the resource instance (`redis`) and its properties. .WithEnvironment(context => { // If a password parameter exists, expose it as the REDIS_PASSWORD environment variable. // The actual value resolution happens later via the ParameterResource. if (redis.PasswordParameter is { } pwd) { context.EnvironmentVariables["REDIS_PASSWORD"] = pwd; } }) // 6.g Build the container arguments lazily, preserving annotations // Define the command-line arguments for the container. This also uses a callback // to allow dynamic argument construction based on resource state or annotations. .WithArgs(context => { // Start with the basic command to run the Redis server. var cmd = new List { "redis-server" }; // If a password parameter is set, add the necessary Redis CLI arguments. // Note: It uses the environment variable name set earlier ($REDIS_PASSWORD). if (redis.PasswordParameter is not null) { cmd.Add("--requirepass"); cmd.Add("$REDIS_PASSWORD"); // Reference the environment variable. } // Check if a PersistenceAnnotation has been added to the resource. // Annotations allow adding optional configuration or behavior. if (redis.TryGetLastAnnotation(out var pa)) { // If persistence is configured, add the corresponding Redis CLI arguments. var interval = (pa.Interval ?? TimeSpan.FromSeconds(60)) .TotalSeconds .ToString(CultureInfo.InvariantCulture); cmd.Add("--save"); cmd.Add(interval); // Save interval in seconds. cmd.Add(pa.KeysChangedThreshold.ToString(CultureInfo.InvariantCulture)); // Number of key changes threshold. } // Finalize the arguments for the shell entrypoint. context.Args.Add("-c"); // Argument for /bin/sh to execute a command string. context.Args.Add(string.Join(' ', cmd)); // Join all parts into a single command string. return Task.CompletedTask; // Return a completed task as the callback is synchronous. }); } } ``` RedisResource.cs ```csharp // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Aspire.Hosting.ApplicationModel; // Data-only Redis resource derived from ContainerResource. // It implements IResourceWithConnectionString to provide connection details. public class RedisResource(string name) // Inherits common container properties and behaviors from ContainerResource. : ContainerResource(name), // Implements this interface to indicate it can provide a connection string. IResourceWithConnectionString { // Constant for the primary endpoint name, used for consistency. internal const string PrimaryEndpointName = "tcp"; // Backing field for the lazy-initialized primary endpoint reference. private EndpointReference? _primaryEndpoint; // Public property to get the EndpointReference for the primary "tcp" endpoint. // EndpointReference allows deferred access to endpoint details (host, port, URL). // It's lazy-initialized on first access. public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); // Property to hold the ParameterResource representing the Redis password. // ParameterResource allows the password value to be resolved later (e.g., from secrets). public ParameterResource? PasswordParameter { get; private set; } // Constructor that accepts a password ParameterResource. public RedisResource(string name, ParameterResource password) : this(name) // Call the base constructor. { PasswordParameter = password; // Store the provided password parameter. } // Helper method to build the ReferenceExpression for the connection string. // ReferenceExpression captures the structure of the connection string, including // references to endpoints and parameters, allowing deferred resolution. private ReferenceExpression BuildConnectionString() { // Use a builder to construct the expression piece by piece. var builder = new ReferenceExpressionBuilder(); // Append the host and port part, referencing the PrimaryEndpoint properties. // .Property() ensures deferred resolution suitable for both run and publish modes. builder.Append($"{PrimaryEndpoint.Property(EndpointProperty.HostAndPort)}"); // If a password parameter exists, append it to the connection string format. if (PasswordParameter is not null) { // Append the password parameter directly; ReferenceExpression handles its deferred resolution. builder.Append($",password={PasswordParameter}"); } // Build and return the final ReferenceExpression. return builder.Build(); } // Implementation of IResourceWithConnectionString.ConnectionStringExpression. // Provides the connection string as a ReferenceExpression, suitable for publish mode // where concrete values aren't available yet. public ReferenceExpression ConnectionStringExpression => BuildConnectionString(); } ``` ## Example: Custom Resource - Talking Clock [Section titled “Example: Custom Resource - Talking Clock”](#example-custom-resource---talking-clock) This example demonstrates creating a completely custom resource (`TalkingClockResource`) that doesn’t derive from built-in types. It shows: * Defining a simple resource class. * Implementing a custom eventing subscriber (`TalkingClockEventingSubscriber`) to manage the resource’s behavior (starting, logging, state updates). * Using `ResourceLoggerService` for per-resource logging. * Using `ResourceNotificationService` to publish state updates. * Creating an `AddTalkingClock` extension method to register the resource and its eventing subscriber. TalkingClockResource.cs ```csharp // Define the custom resource type. It inherits from the base Aspire 'Resource' class. // This class is primarily a data container; Aspire behavior is added via eventing subscribers and extension methods. public sealed class TalkingClockResource(string name) : Resource(name); ``` TalkingClockEventingSubscriber.cs ```csharp // Define an Aspire eventing subscriber that implements the behavior for the TalkingClockResource. // Eventing subscribers allow plugging into the application's startup and lifecycle events. public sealed class TalkingClockEventingSubscriber( // Aspire service for publishing resource state updates (e.g., Running, Starting). ResourceNotificationService notification, // Aspire service for getting a logger scoped to a specific resource. ResourceLoggerService loggerSvc, // General service provider for dependency injection if needed. IServiceProvider services) : IDistributedApplicationEventingSubscriber // Implement the Aspire eventing subscriber interface. { // This method is called by Aspire to allow subscription to lifecycle events. public Task SubscribeAsync( IDistributedApplicationEventing eventing, // The eventing service to subscribe to. DistributedApplicationExecutionContext context, // Execution context with model and environment info. CancellationToken cancellationToken) // Cancellation token for graceful shutdown. { // Subscribe to the AfterResourcesCreatedEvent to start the clock behavior. eventing.Subscribe(async (@event, ct) => { // Find all instances of TalkingClockResource in the Aspire application model. foreach (var clock in context.Model.Resources.OfType()) { // Get an Aspire logger specifically for this clock instance. // Logs will be associated with this resource in the dashboard. var log = loggerSvc.GetLogger(clock); // Start a background task to manage the clock's lifecycle and behavior. _ = Task.Run(async () => { // Publish an Aspire event indicating that this resource is about to start. // Other components could subscribe to this event for pre-start actions. await eventing.PublishAsync( new BeforeResourceStartedEvent(clock, services), ct); // Log an informational message associated with the resource. log.LogInformation("Starting Talking Clock..."); // Publish an initial state update to the Aspire notification service. // This sets the resource's state to 'Running' and records the start time. // The Aspire dashboard and other orchestrators observe these state updates. await notification.PublishUpdateAsync(clock, s => s with { StartTimeStamp = DateTime.UtcNow, State = KnownResourceStates.Running // Use an Aspire well-known state. }); // Enter the main loop that runs as long as cancellation is not requested. while (!ct.IsCancellationRequested) { // Log the current time, associated with the resource. log.LogInformation("The time is {time}", DateTime.UtcNow); // Publish a custom state update "Tick" using Aspire's ResourceStateSnapshot. // This demonstrates using custom state strings and styles in the Aspire dashboard. await notification.PublishUpdateAsync(clock, s => s with { State = new ResourceStateSnapshot("Tick", KnownResourceStateStyles.Info) }); await Task.Delay(1000, ct); // Publish another custom state update "Tock" using Aspire's ResourceStateSnapshot. await notification.PublishUpdateAsync(clock, s => s with { State = new ResourceStateSnapshot("Tock", KnownResourceStateStyles.Success) }); await Task.Delay(1000, ct); } }, ct); } }); return Task.CompletedTask; } } ``` TalkingClockExtensions.cs ```csharp // Define Aspire extension methods for adding the TalkingClockResource to the application builder. // This provides a fluent API for users to add the custom resource. public static class TalkingClockExtensions { // The main Aspire extension method to add a TalkingClockResource. public static IResourceBuilder AddTalkingClock( this IDistributedApplicationBuilder builder, // Extends the Aspire application builder. string name) // The name for this resource instance. { // Register the TalkingClockEventingSubscriber with the DI container using Aspire's helper method. // The Aspire hosting infrastructure will automatically discover and run registered eventing subscribers. builder.Services.TryAddEventingSubscriber(); // Create a new instance of the TalkingClockResource. var clockResource = new TalkingClockResource(name); // Add the resource instance to the Aspire application builder and configure it using fluent APIs. return builder.AddResource(clockResource) // Use Aspire's ExcludeFromManifest to prevent this resource from being included in deployment manifests. .ExcludeFromManifest() // Use Aspire's WithInitialState to set an initial state snapshot for the resource. // This provides initial metadata visible in the Aspire dashboard. .WithInitialState(new CustomResourceSnapshot // Aspire type for custom resource state. { ResourceType = "TalkingClock", // A string identifying the type of resource for Aspire. CreationTimeStamp = DateTime.UtcNow, State = KnownResourceStates.NotStarted, // Use an Aspire well-known state. // Add custom properties displayed in the Aspire dashboard's resource details. Properties = [ // Use Aspire's known property key for source information. new(CustomResourceKnownProperties.Source, "Talking Clock") ], // Add URLs associated with the resource, displayed as links in the Aspire dashboard. Urls = [ // Define a URL using Aspire's UrlSnapshot type. new("Speaking Clock", "https://www.speaking-clock.com/", isInternal: false) ] }); } } ``` # Resource Hierarchies > Model parent-child resource relationships in Aspire to express ownership, lifecycle containment, and dashboard grouping for distributed application topologies. Aspire supports modeling **parent-child relationships** between resources to express ownership, containment, and grouping. Parent-child relationships serve two purposes: * **Lifecycle Containment**: The child’s execution is tied to the parent’s — starting, stopping, and failures cascade from parent to child automatically. * **Dashboard Visualization**: The child appears **nested beneath** the parent in dashboards and visualizations, improving readability. ## Lifecycle containment [Section titled “Lifecycle containment”](#lifecycle-containment) When a resource implements the `IResourceWithParent` interface, it declares **true containment** — meaning its lifecycle is controlled by its parent: * **Startup**: The child resource will only start after its parent starts (though readiness is independent). * **Shutdown**: If the parent is stopped or removed, the child is also stopped automatically. * **Failure Propagation**: If a parent enters a terminal failure state (`FailedToStart`, etc.), dependent children are stopped. Note A logging sidecar container is tied to the lifecycle of a main application container — if the main app stops, the logging sidecar is also terminated. ## Visual grouping (without lifecycle impact) [Section titled “Visual grouping (without lifecycle impact)”](#visual-grouping-without-lifecycle-impact) Aspire also supports **visual-only parent-child relationships** using the `WithParentRelationship()` method during resource construction. Visual relationships: * Affect **only the dashboard layout**. * **Do not affect lifecycle** — the resources are independent operationally. * Improve **clarity** by logically grouping related components. Tip A Redis database container and a Redis Commander admin UI container can be grouped visually, even though they start independently. ## Manual Relationships — No Inference [Section titled “Manual Relationships — No Inference”](#manual-relationships--no-inference) Aspire **does not infer** parent-child relationships automatically based on names, dependencies, or network links. You must **explicitly declare** relationships by either: * **Implementing `IResourceWithParent`**: Creates lifecycle dependency and visual nesting. * **Using `.WithParentRelationship()`**: Creates visual nesting only. This explicitness ensures developers have full control over resource containment and presentation. ## Real-world scenarios [Section titled “Real-world scenarios”](#real-world-scenarios) The following scenarios illustrate how Aspire models parent-child relationships: | Scenario | Parent | Child | | ----------------------------------------------- | ------------------ | ---------------------- | | Main application container with logging sidecar | App container | Fluentd container | | Database with admin dashboard | Database container | Admin UI container | | Microservice with associated health monitor | API container | Health probe container | ## Values and references [Section titled “Values and references”](#values-and-references) In Aspire, configuration, connectivity details, and dependencies between distributed resources are modeled using **structured values**. These values capture relationships explicitly—not just as simple strings—making the application graph **portable, inspectable, and evolvable**. Aspire represents these relationships through a **heterogeneous Directed Acyclic Graph (DAG)**. This graph tracks not only dependency ordering but also how **structured values** are passed between resources at multiple abstraction levels: configuration, connection, and runtime behavior. AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var db = builder.AddPostgres("pg"); var api = builder.AddProject("api").WithReference(db); var web = builder.AddNpmApp("web").WithReference(api); builder.Build().Run(); ``` ``` architecture-beta service db(logos:postgresql)[pg] service epr(iconoir:server-connection)[Endpoint Reference] service api(logos:dotnet)[api] service ctr(iconoir:server-connection)[Connection String Reference] service frontend(logos:react)[web] db:L <-- R:ctr ctr:L <-- R:api api:L <-- R:epr epr:L <-- R:frontend ``` ### Special case: Endpoints [Section titled “Special case: Endpoints”](#special-case-endpoints) Normally, resource references form an acyclic graph — **no cycles allowed**. However, **endpoint references are treated specially** and **may form cycles** intentionally. Endpoints are modeled as **external entities**: * They are **not edges** in the resource dependency graph. * They enable realistic mutual references like: * A frontend app and an OIDC server mutually referencing each other’s URLs (redirects, login callbacks). * A backend exposing CORS settings that reference the frontend URL. Tip Endpoints are managed separately from strict dependency edges to allow flexible, real-world service wiring. ### How the DAG forms [Section titled “How the DAG forms”](#how-the-dag-forms) Resources connect to each other through: * **`WithReference()` calls**: Direct resource-to-resource dependencies. * **Environment variables and CLI arguments**: Configuration values containing structured references. * **Other configuration sources**: Settings populated with structured value references. Each reference **adds an edge** to the graph, allowing Aspire to: * Track dependency ordering. * Propagate structured values cleanly between services. * Validate application integrity before execution. Note Aspire **never infers references automatically** — all value flows must be explicitly authored by developers. ### Structured vs literal values [Section titled “Structured vs literal values”](#structured-vs-literal-values) Aspire distinguishes between **structured values** and **literal values**. * **Structured values** preserve meaning (e.g., “this is a service URL” vs. “this is a raw string”). * **Literal values** are inert — they are carried unchanged across modes. At publish time and run time: * Structured values are either **resolved** (if possible) or **translated into target artifacts** (e.g., environment variables, argument values, etc.). * Literal values are simply copied. Caution **Flattening values too early destroys portability, environment substitution, and cross-platform compatibility.** Aspire delays flattening as long as possible to maintain graph fidelity. ### Value providers and deferred evaluation [Section titled “Value providers and deferred evaluation”](#value-providers-and-deferred-evaluation) Every structured value type in Aspire implements two fundamental interfaces: | Interface | When Used | Purpose | | ----------------------------- | ------------ | --------------------------------------------------------------------------------- | | `IValueProvider` | Run mode | Resolves live values when the application starts. | | `IManifestExpressionProvider` | Publish mode | Emits structured expressions (like `{pg.outputs.url}`) into deployment artifacts. | This dual-interface model enables **deferred evaluation**: * During **publish**, structured placeholders are emitted — no runtime values are resolved yet. * During **run**, structured references are resolved to live values like URLs, ports, or connection strings. Internally, value providers are attached to environment variables, CLI arguments, configuration fields, and other structured outputs during application graph construction. Note Deferred evaluation guarantees that Aspire applications can be **published safely**, **deployed flexibly**, and **run consistently** across environments. ### Core value types (expanded) [Section titled “Core value types (expanded)”](#core-value-types-expanded) | Type | Represents | Run Mode | Publish Mode | | ----------------------------- | ----------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------- | | `string` | A literal string value. | Same literal. | Same literal. | | `EndpointReference` | A link to a named endpoint on another resource. | Concrete URL (`http://localhost:5000`). | Target-specific endpoint translation (DNS, ingress, etc.). | | `EndpointReferenceExpression` | A property of an endpoint (`Host`, `Port`, `Scheme`). | Concrete value. | Platform-specific translation. | | `ConnectionStringReference` | A symbolic pointer to a resource’s connection string. | Concrete string. | Token or externalized secret. | | `ParameterResource` | An external input, secret, or setting. | Local dev value or environment lookup. | Placeholder `${PARAM}` for substitution. | | `ReferenceExpression` | A composite string with embedded references. | Concrete formatted string. | Format string preserved for substitution. | ## `ReferenceExpression` [Section titled “ReferenceExpression”](#referenceexpression) `ReferenceExpression` preserves **structured value objects**—endpoints, parameters, connection strings, etc.—inside an interpolated string and defers evaluation until it is safe. Aspire evaluates the model in **two distinct modes**: | Phase | `ReferenceExpression` yields | | ----------- | ----------------------------------------------------------------------- | | **Publish** | Publisher-specific placeholder text (e.g., `{api.bindings.http.host}`). | | **Run** | Concrete value such as `localhost`. | **Example — Using `ReferenceExpression`:** AppHost.cs ```csharp var ep = api.GetEndpoint("http"); builder.WithEnvironment("HEALTH_URL", ReferenceExpression.Create( $"https://{ep.Property(EndpointProperty.Host)}:{ep.Property(EndpointProperty.Port)}/health" ) ); ``` *Publish manifest excerpt:* ```ini HEALTH_URL=https://{api.bindings.http.host}:{api.bindings.http.port}/health ``` *Run-time value:* ```ini HEALTH_URL=https://localhost:5000/health ``` Tip **Avoid resolving values directly** — build the string inside `ReferenceExpression.Create()` to preserve structure. ### Alternate pattern using `ExecutionContext` [Section titled “Alternate pattern using ExecutionContext”](#alternate-pattern-using-executioncontext) AppHost.cs ```csharp var ep = api.GetEndpoint("http"); if (builder.ExecutionContext.IsRunMode) { builder.WithEnvironment("HEALTH_URL", $"{ep.Url}/health"); // concrete } else { builder.WithEnvironment("HEALTH_URL", ReferenceExpression.Create($"{ep}/health")); // structured } ``` ### Pattern used by `IResourceWithConnectionString` [Section titled “Pattern used by IResourceWithConnectionString”](#pattern-used-by-iresourcewithconnectionstring) A common implementation builds the connection string with `ReferenceExpression`, mixing any value objects (endpoint properties, parameters, other references): ```csharp private static ReferenceExpression BuildConnectionString( EndpointReference endpoint, ParameterResource passwordParameter) { var host = endpoint.Property(EndpointProperty.IPV4Host); var port = endpoint.Property(EndpointProperty.Port); var pwd = passwordParameter; return ReferenceExpression.Create( $"Server={host},{port};User ID=sa;Password={pwd};TrustServerCertificate=true"); } ``` ### Common errors [Section titled “Common errors”](#common-errors) The following patterns are common mistakes when using `ReferenceExpression`: | Error | Correct approach | | -------------------------------------- | ------------------------------------------------------- | | Build the string first, wrap later. | Build **inside** `ReferenceExpression.Create(...)`. | | Access `Endpoint.Url` during publish. | Use `Endpoint.Property(...)` in the expression. | | Mix resolved strings and placeholders. | Keep the entire value inside one `ReferenceExpression`. | ## Endpoint primitives [Section titled “Endpoint primitives”](#endpoint-primitives) The `EndpointReference` is the fundamental type used to interact with another resource’s endpoint. It provides properties such as: * `Url`: The full URL of the endpoint, e.g., `http://localhost:6379`. * `Host`: The hostname or IP address of the endpoint. * `Port`: The port number of the endpoint. These properties are dynamically resolved during the application’s startup sequence. Accessing them before the endpoints are allocated results in an exception. ### `IResourceWithEndpoints` [Section titled “IResourceWithEndpoints”](#iresourcewithendpoints) Resources supporting endpoints should implement `IResourceWithEndpoints`, enabling the use of `GetEndpoint(name)` to retrieve an `EndpointReference`. This is implemented on the built-in `ProjectResource`, `ContainerResource` and `ExecutableResource`. It allows endpoints to be programmatically accessed and passed between resources. Example — Endpoint Access and Resolution ```csharp var builder = DistributedApplication.CreateBuilder(args); var redis = builder.AddContainer("redis", "redis") .WithEndpoint(name: "tcp", targetPort: 6379); // Get a reference to the "tcp" endpoint by name var endpoint = redis.GetEndpoint("tcp"); builder.Build().Run(); ``` ### What does “allocated” mean? [Section titled “What does “allocated” mean?”](#what-does-allocated-mean) An endpoint is **allocated** when Aspire resolves its runtime values (e.g., `Host`, `Port`, `Url`) during **run mode**. Allocation happens as part of the **startup sequence**, ensuring endpoints are ready for use in local development. In **publish mode**, endpoints are not allocated with concrete values. Instead, their values are represented as **manifest expressions** or bindings (e.g., `{redis.bindings.tcp.host}:{redis.bindings.tcp.port}`) that are resolved by the deployment infrastructure. #### Comparison: Run Mode vs Publish Mode [Section titled “Comparison: Run Mode vs Publish Mode”](#comparison-run-mode-vs-publish-mode) | **Context** | **Run Mode** | **Publish Mode** | | ------------------- | ---------------------------------------- | ----------------------------------------------------------------- | | **Endpoint Values** | Fully resolved (`tcp://localhost:6379`). | Represented by manifest expressions (`{redis.bindings.tcp.url}`). | | **Use Case** | Local development and debugging. | Deployed environments (e.g., Kubernetes, Azure, AWS, etc.). | | **Behavior** | Endpoints are allocated dynamically. | Endpoint placeholders resolve at runtime. | Use the `IsAllocated` property on an `EndpointReference` to check whether an endpoint has been allocated before accessing its runtime values. ### Accessing Allocated Endpoints Safely [Section titled “Accessing Allocated Endpoints Safely”](#accessing-allocated-endpoints-safely) Endpoint resolution happens during the startup sequence of the `DistributedApplication`. To safely access endpoint values (e.g., `Url`, `Host`, `Port`), you must wait until endpoints are allocated. Aspire provides eventing APIs, such as `AfterEndpointsAllocatedEvent`, to access endpoints after allocation. These APIs ensure code executes only when endpoints are ready. Example — Checking Allocation and Using Eventing ```csharp var builder = DistributedApplication.CreateBuilder(args); // Add a Redis container with a TCP endpoint var redis = builder.AddContainer("redis", "redis") .WithEndpoint(name: "tcp", targetPort: 6379); // Retrieve the EndpointReference var endpoint = redis.GetEndpoint("tcp"); // Check allocation status and access Url Console.WriteLine($"IsAllocated: {endpoint.IsAllocated}"); try { Console.WriteLine($"Url: {endpoint.Url}"); } catch (Exception ex) { Console.WriteLine($"Error accessing Url: {ex.Message}"); } // Subscribe to AfterEndpointsAllocatedEvent for resolved properties builder.Eventing.Subscribe( (@event, cancellationToken) => { Console.WriteLine($"Endpoint allocated: {endpoint.IsAllocated}"); Console.WriteLine($"Resolved Url: {endpoint.Url}"); return Task.CompletedTask; }); // Start the application builder.Build().Run(); ``` The preceding code will output different results depending on whether the application is running in **run mode** or **publish mode**: **Run Mode**: Run Mode — Console Output ```bash IsAllocated: True Resolved Url: http://localhost:6379 ``` **Publish Mode**: Publish Mode — Console Output ```bash IsAllocated: False Error accessing Url: Endpoint has not been allocated. ``` Tip The overloads of `WithEnvironment` that take a callback run after endpoints have been allocated. ## Referencing Endpoints from Other Resources [Section titled “Referencing Endpoints from Other Resources”](#referencing-endpoints-from-other-resources) This section covers how to reference endpoints from other resources in Aspire, allowing you to wire up dependencies and configurations effectively. ### Using `WithReference` [Section titled “Using WithReference”](#using-withreference) The `WithReference` API allows you to pass an endpoint reference directly to a target resource. AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var redis = builder.AddContainer("redis", "redis") .WithEndpoint(name: "tcp", targetPort: 6379); builder.AddProject("worker") .WithReference(redis.GetEndpoint("tcp")); builder.Build().Run(); ``` `WithReference` is optimized for applications that use service discovery. ### Using `WithEnvironment` [Section titled “Using WithEnvironment”](#using-withenvironment) The `WithEnvironment` API exposes endpoint details as environment variables, enabling runtime configuration. Example — Passing Redis Endpoint as Environment Variable ```csharp var builder = DistributedApplication.CreateBuilder(args); var redis = builder.AddContainer("redis", "redis") .WithEndpoint(name: "tcp", targetPort: 6379); builder.AddProject("worker") .WithEnvironment("RedisUrl", redis.GetEndpoint("tcp")); builder.Build().Run(); ``` `WithEnvironment` gives full control over the configuration names injected into the target resource. ## `EndpointReferenceExpression` — Accessing Endpoint Parts [Section titled “EndpointReferenceExpression — Accessing Endpoint Parts”](#endpointreferenceexpression--accessing-endpoint-parts) `EndpointReferenceExpression` represents **one field** of an endpoint (`Host`, `Port`, `Scheme`, etc.). In C#, call `endpoint.Property(...)` to get that field. In TypeScript AppHosts, call `await endpoint.property(...)`. The result is still a structured value and stays deferred until publish/run time. | Need | Pattern | | --------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | Only one part (e.g., host) | C#: `endpoint.Property(EndpointProperty.Host)` TypeScript: `await endpoint.property(EndpointProperty.Host)` | | Compose multiple parts into one setting | Build a `ReferenceExpression` (see dedicated section). | * TypeScript apphost.mts ```typescript import { createBuilder, EndpointProperty } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const redis = await builder.addContainer('redis', 'redis'); await redis.withEndpoint({ name: 'tcp', targetPort: 6379 }); const ep = await redis.getEndpoint('tcp'); const redisHost = await ep.property(EndpointProperty.Host); const redisPort = await ep.property(EndpointProperty.Port); await builder .addProject('worker', '../Worker/Worker.csproj') .withEnvironment('REDIS_HOST', redisHost) .withEnvironment('REDIS_PORT', redisPort); await builder.build().run(); ``` * C# AppHost.cs ```csharp var redis = builder.AddContainer("redis", "redis") .WithEndpoint("tcp", 6379); builder.AddProject("worker") .WithEnvironment(ctx => { var ep = redis.GetEndpoint("tcp"); ctx.EnvironmentVariables["REDIS_HOST"] = ep.Property(EndpointProperty.Host); ctx.EnvironmentVariables["REDIS_PORT"] = ep.Property(EndpointProperty.Port); }); ``` In this pattern, the endpoint property call returns an `EndpointReferenceExpression`, which is a structured value that will be resolved at runtime. Example — Build a Full Redis URL ```csharp var ep = redis.GetEndpoint("tcp"); builder.WithEnvironment("REDIS_URL", ReferenceExpression.Create( $"redis://{ep.Property(EndpointProperty.HostAndPort)}" ) ); ``` This pattern avoids resolving endpoint values prematurely and works in both publish and run modes. ### `EndpointProperty` API Surface [Section titled “EndpointProperty API Surface”](#endpointproperty-api-surface) | Property | Meaning | | ---------------------- | ------------------------------------------------ | | `Url` | Fully qualified URL (`scheme://host:port`). | | `Host` or `IPV4Host` | Host name or IPv4 literal. | | `Port` or `TargetPort` | Allocated host port vs. container-internal port. | | `Scheme` | `http`, `tcp`, etc. | | `HostAndPort` | Convenience composite (`host:port`). | The `EndpointReference` type exposes live or placeholder values for an endpoint and provides `.Property(...)` to create an `EndpointReferenceExpression`. Key members: | Member | Description | | -------------------------------------------------------- | ------------------------------------------------------ | | `Url`, `Host`, `Port`, `Scheme`, `TargetPort` | Concrete in run mode; undefined in publish mode. | | `bool IsAllocated` | Indicates if concrete values are available (run mode). | | `EndpointReferenceExpression Property(EndpointProperty)` | Creates a deferred expression for one field. | `EndpointReferenceExpression` implements the same `IManifestExpressionProvider` / `IValueProvider` pair, so it can be embedded in a `ReferenceExpression` or resolved directly with `GetValueAsync()`. ## Context-based endpoint resolution [Section titled “Context-based endpoint resolution”](#context-based-endpoint-resolution) Aspire resolves endpoints differently based on the relationship between the source and target resources. This ensures proper communication across all environments. ### Resolution Rules [Section titled “Resolution Rules”](#resolution-rules) | **Source** | **Target** | **Resolution** | **Example URL** | | -------------------- | -------------------- | ------------------------------------------- | --------------------------- | | Container | Container | Container network (`resource name:port`). | `redis:6379` | | Executable / Project | Container | Host network (`localhost:port`). | `localhost:6379` | | Container | Executable / Project | Host network (`host.docker.internal:port`). | `host.docker.internal:5000` | ### Advanced Scenario: Overriding default endpoint resolution [Section titled “Advanced Scenario: Overriding default endpoint resolution”](#advanced-scenario-overriding-default-endpoint-resolution) Aspire resolves endpoints differently based on the execution context (e.g., run mode vs. publish mode, container vs. executable). Sometimes you need to override that resolution behavior to get an endpoint from a different perspective than the default. Consider a scenario where a project resource needs to configure Grafana and Keycloak containers. The project must provide container-to-container URLs for these services to communicate with each other, even though the project itself would normally receive host-based URLs. ### Explicit context resolution with ValueProviderContext [Section titled “Explicit context resolution with ValueProviderContext”](#explicit-context-resolution-with-valueprovidercontext) Starting with Aspire 13.2, you can explicitly control endpoint resolution context using `ValueProviderContext`. This provides a cleaner alternative to manually constructing URLs when you need endpoints resolved from a specific resource’s perspective or network context. #### Resolve from a specific resource (Caller) [Section titled “Resolve from a specific resource (Caller)”](#resolve-from-a-specific-resource-caller) Use the `Caller` property to resolve an endpoint from the perspective of a specific calling resource: Resolve endpoint from a resource's perspective ```csharp var builder = DistributedApplication.CreateBuilder(args); var redis = builder.AddRedis("cache"); var containerApp = builder.AddContainer("worker", "myimage"); // Get the endpoint reference var endpoint = redis.GetEndpoint("tcp"); // Resolve the URL from the container's perspective var url = await endpoint.GetValueAsync(new ValueProviderContext { Caller = containerApp.Resource, }); // The URL will be appropriate for container-to-container communication // e.g., "cache:6379" using the resource name as the hostname ``` This is particularly useful when you need to pass connection information between resources that may be running in different contexts (containers vs. host processes). #### Resolve from a specific network [Section titled “Resolve from a specific network”](#resolve-from-a-specific-network) Use the `Network` property to resolve an endpoint from the perspective of a specific network: Resolve endpoint from a network's perspective ```csharp var builder = DistributedApplication.CreateBuilder(args); var redis = builder.AddRedis("cache"); // Get the endpoint reference var endpoint = redis.GetEndpoint("tcp"); // Resolve the URL for the default Aspire container network var url = await endpoint.GetValueAsync(new ValueProviderContext { Network = KnownNetworkIdentifiers.DefaultAspireContainerNetwork }); // The URL will be appropriate for the container network // e.g., "cache:6379" using the resource name as the hostname ``` The `KnownNetworkIdentifiers` class provides predefined network identifiers: * `LocalhostNetwork`: Resolves to localhost-based URLs * `DefaultAspireContainerNetwork`: Resolves to container network URLs using resource names * `PublicInternet`: Resolves to externally accessible URLs Note These APIs existed in Aspire 13.1 but did not behave as expected. In Aspire 13.2, they now correctly resolve endpoints based on the specified context. For more details, see [What’s new in Aspire 13.2](/whats-new/aspire-13-2/#contextual-endpoint-resolution). ### Cross-Context Communication Example [Section titled “Cross-Context Communication Example”](#cross-context-communication-example) The following code demonstrates how to set environment variables for a project that needs to communicate with other resources like Grafana and Keycloak. It ensures that the URLs are correctly resolved based on the execution context (run mode vs. publish mode). With Aspire 13.2, you can simplify this using `ValueProviderContext`: Simplified with ValueProviderContext (Aspire 13.2+) ```csharp var builder = DistributedApplication.CreateBuilder(args); var keycloak = builder.AddKeycloak("keycloak", 8080); var grafana = builder.AddContainer("grafana", "grafana/grafana"); var api = builder.AddProject("api") .WithEnvironment(async ctx => { var keyCloakEndpoint = keycloak.GetEndpoint("http"); var grafanaEndpoint = grafana.GetEndpoint("http"); ctx.EnvironmentVariables["Grafana__Url"] = grafanaEndpoint; if (ctx.ExecutionContext.IsRunMode) { // Resolve from container network perspective var keycloakUrl = await keyCloakEndpoint.GetValueAsync(new ValueProviderContext { Network = KnownNetworkIdentifiers.DefaultAspireContainerNetwork }); ctx.EnvironmentVariables["Keycloak__AuthServerUrl"] = keycloakUrl; } else { // In publish mode let the endpoint resolver handle the URL ctx.EnvironmentVariables["Keycloak__AuthServerUrl"] = keyCloakEndpoint; } }); builder.Build().Run(); ``` **Before Aspire 13.2**, you would need to manually construct the URL: Manual URL construction (pre-13.2) ```csharp var builder = DistributedApplication.CreateBuilder(args); var keycloak = builder.AddKeycloak("keycloak", 8080); var grafana = builder.AddContainer("grafana", "grafana/grafana"); var api = builder.AddProject("api") .WithEnvironment(ctx => { var keyCloakEndpoint = keycloak.GetEndpoint("http"); var grafanaEndpoint = grafana.GetEndpoint("http"); ctx.EnvironmentVariables["Grafana__Url"] = grafanaEndpoint; if (ctx.ExecutionContext.IsRunMode) { // Manual URL construction required in pre-13.2 var keycloakUrl = new UriBuilder(keyCloakEndpoint.Url) { Host = keycloak.Resource.Name, Port = keyCloakEndpoint.TargetPort ?? keyCloakEndpoint.Port, }; ctx.EnvironmentVariables["Keycloak__AuthServerUrl"] = keycloakUrl.ToString(); } else { // In publish mode let the endpoint resolver handle the URL ctx.EnvironmentVariables["Keycloak__AuthServerUrl"] = keyCloakEndpoint; } }); builder.Build().Run(); ``` # Resource Model > Learn how Aspire's resource model represents distributed apps as a directed acyclic graph of services, containers, executables, and integrations in the AppHost. Aspire’s AppHost represents a collection of resources, known as the “resource model”. This model allows developers to define and manage the various components and services that make up their applications, providing a unified way to interact with these resources throughout the development lifecycle. The resource model is a **directed acyclic graph (DAG)**, where resources are nodes and dependencies are edges. This structure allows Aspire to manage complex relationships between resources, ensuring that they can be started, stopped, and monitored in a predictable manner. ## Basic example [Section titled “Basic example”](#basic-example) A quick example that shows the basic usage: AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var db = builder.AddPostgres("pg").AddDatabase("appdata"); var api = builder.AddProject("api").WithReference(db); var web = builder.AddNpmApp("web", "../web").WithReference(api); builder.Build().Run(); ``` The preceding `AppHost` code defines an architecture with three resources: ``` architecture-beta service db(logos:postgresql)[pq] service api(logos:dotnet)[api] service frontend(logos:react)[web] api:R --> L:db frontend:R --> L:api ``` 1. Use `.AddXyz(...)` helper methods to add and declare resources (e.g., `.AddPostgres(...)`, `.AddProject(...)`). 2. Use `.WithReference(...)` (or similar) to represent explicit dependencies between resources. 3. Call `Build().Run()` - Aspire builds the application model (graph) and executes it handling: * Port allocation * Environment variables * Startup order Prefer writing your AppHost in TypeScript? Start with [Build your first Aspire app](/get-started/first-app/?lang=typescript). ## Resource basics [Section titled “Resource basics”](#resource-basics) In Aspire, a **resource** is the fundamental unit for modeling your distributed applications. Resources represent services, infrastructure elements, or supporting components that together compose a distributed system. Resources in Aspire implement the `IResource` interface, with most built-in resources deriving from the base `Resource` class. * Resources are **inert by default** — they are **pure data objects** that describe capabilities, configuration, and relationships. They **do not manage their own lifecycle** (e.g., starting, stopping, checking health). Resource lifecycle is coordinated externally by orchestrators and lifecycle hooks. * Resources are identified by a **unique name** within the application graph. This name forms the basis for referencing, wiring, and visualizing resources. ### Annotations [Section titled “Annotations”](#annotations) Resource metadata is expressed through **annotations**, which are strongly-typed objects implementing the `IResourceAnnotation` interface. Annotations allow attaching additional structured information to a resource without modifying its core class. They are the **primary extensibility mechanism** in Aspire, enabling: * Core system behaviors (e.g., service discovery, connection strings, health probes). * Custom extensions and third-party integrations. * Layering of optional capabilities without inheritance or tight coupling. Tip Resources might have annotations for environment variables, endpoint information, or service discovery metadata that other resources require. For additional information on annotations, see [Resource API Patterns: Annotations](/architecture/resource-api-patterns/#annotations). ### Fluent extension methods [Section titled “Fluent extension methods”](#fluent-extension-methods) Resources are typically added using fluent **extension methods** such as `.AddRedis(...)`, `.AddProject(...)`, or `.AddPostgres(...)`. Extension methods encapsulate: * **Construction** of the resource object. * **Attachment of annotations** that describe defaults, discovery hints, or runtime behavior. * **Relationships** like wiring up dependencies (e.g., via `.WithReference(...)`). This pattern improves the developer experience by: * Setting **sane defaults** automatically. * Making **required configuration obvious and discoverable**. * Providing a **product-like feel** to adding infrastructure. Note Without extension methods, adding a resource manually would require constructing it directly, setting annotations manually, and remembering to wire relationships by hand. ### Adding resources and wiring dependencies [Section titled “Adding resources and wiring dependencies”](#adding-resources-and-wiring-dependencies) To continue from the previous example, here’s how you can add resources and wire them together in the `AppHost`: AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); var db = builder.AddPostgres("pg").AddDatabase("appdata"); var api = builder.AddProject("api").WithReference(db); var web = builder.AddNpmApp("web", "../web").WithReference(api); builder.Build().Run(); ``` The preceding example: * A PostgreSQL server (`pg`) is created and configured with a database named `appdata`. * A backend service (`api`) is created and connected to the database. * A frontend app (`web`) is created and reverse-proxies traffic to the backend. Each resource participates in the application graph passively, with dependencies expressed through references. ### Key takeaways [Section titled “Key takeaways”](#key-takeaways) Resources **describe** capabilities rather than controlling them directly. **Annotations** provide a rich, extensible metadata system for resources, while **fluent extension methods** guide developers toward correct and complete configurations. **Names** serve as the identity anchors for wiring and dependency resolution throughout the application graph. ## Built-in resources and lifecycle [Section titled “Built-in resources and lifecycle”](#built-in-resources-and-lifecycle) In Aspire, many common infrastructure and application patterns are available as **built-in resource types**. Built-in resources simplify modeling real-world systems by providing ready-made building blocks that automatically integrate with the Aspire runtime, lifecycle management, health tracking, and dashboard visualization. Built-in resources: * Handle **lifecycle transitions** automatically. * Raise **lifecycle events** (like startup and readiness signals). * Push **status updates** to the system for real-time orchestration and monitoring. * Expose **endpoints, environment variables, and metadata** needed for dependent resources. They help developers express distributed applications **consistently** without needing to manually orchestrate startup, shutdown, and dependency wiring. ### Known resource states [Section titled “Known resource states”](#known-resource-states) All resources in Aspire begin in an `Unknown` state when added to the application graph. This ensures that the **resource graph can be fully constructed** before any execution, dependency resolution, or publishing occurs. | State | Meaning | | ------------------ | ---------------------------------------------------------------------------------------------------- | | `Exited` | Completed execution (typically for short-lived jobs, migrations, one-shot tasks). | | `FailedToStart` | Failed during startup initialization. | | `Finished` | Ran to successful completion (used for batch workloads or scripts). | | `Hidden` | Present in the model but intentionally hidden from dashboard UI (e.g., infrastructure helpers). | | `NotStarted` | Defined but not yet scheduled to start. | | `Running` | Successfully started; may have separate application-level health probing. | | `RuntimeUnhealthy` | The container or host runtime environment (e.g., Docker daemon) is unavailable, preventing start-up. | | `Starting` | Actively starting; readiness not yet confirmed. | | `Stopping` | Resource is shutting down gracefully. | | `Unknown` | Default state when first added to the graph. No execution planned yet. | | `TerminalStates` | List of terminal states (e.g., `Finished`, `Exited`, `FailedToStart`). | | `Waiting` | Awaiting dependencies to become ready (e.g., using `.WaitFor(...)`). | Resource states drive: * **Readiness checks** to unblock dependent resources. * **Dashboard visualization** and state coloring. * **Orchestration sequencing** for startup and shutdown. * **Health monitoring** at runtime. ### Built-in types [Section titled “Built-in types”](#built-in-types) Aspire provides a set of fundamental built-in resource types that serve as the foundation for modeling execution units: | Type | Purpose | | -------------------- | ------------------------------------------------------- | | `ContainerResource` | Runs Docker containers as resources. | | `ExecutableResource` | Launches arbitrary executables or scripts as resources. | | `ProjectResource` | Runs a .NET project directly (build + launch workflow). | | `ParameterResource` | Represents a parameter or configuration value. | These types are **infrastructure-oriented primitives**. They model how code and applications are packaged and executed. Note Specialized services like Redis, Postgres, or RabbitMQ are **not** true “built-in” resource types in Aspire core — they are typically provided through external packages or extensions that build on `ContainerResource` or custom resource types. Built-in types: * Automatically participate in resource orchestration. * Raise standard lifecycle events without manual intervention. * Report health and readiness status. * Expose connection endpoints for dependent services. Custom resources must **opt-in manually** to these behaviors. ### Well-known lifecycle events [Section titled “Well-known lifecycle events”](#well-known-lifecycle-events) Aspire defines standard events to orchestrate resource lifecycles, in the following order: 1. `InitializeResourceEvent`: Fired when a resource is first created to kick off the resource’s lifecycle. 2. `ConnectionStringAvailableEvent`: Fired when a connection string is ready, enabling dependent resources to wire themselves dynamically based on the resource’s outputs. 3. `ResourceEndpointsAllocatedEvent`: Fired when endpoints have been allocated and can be evaluated successfully. 4. `BeforeResourceStartedEvent`: Fired just before the resource starts executing as a last-chance dynamic setup or validation point. 5. `ResourceReadyEvent`: Fired when the resource is considered “ready,” unblocking any dependents waiting for the resource. Lifecycle events allow: * Dynamic reconfiguration just before startup. * Dependent resource activation based on readiness. * Wiring services together based on runtime-generated outputs. Caution Event publishing is **synchronous and blocking** — event handlers can delay further execution. ### Status reporting [Section titled “Status reporting”](#status-reporting) Beyond events, Aspire uses **asynchronous state snapshots** to report resource status continuously. * `ResourceNotificationService` handles snapshot updates. * Status updates involve: 1. Receiving the previous immutable snapshot. 2. Mutating to a new snapshot representing the updated state. 3. Publishing the new snapshot to the dashboard and orchestrators. Snapshots: * Always reflect the **latest known status**. * Are **non-blocking** and do not delay orchestration. * Drive **dashboard visualization** and orchestration decisions. Note Events represent **moment-in-time actions**. Snapshots represent **ongoing state**. ### Resource health [Section titled “Resource health”](#resource-health) Aspire integrates with .NET health checks to monitor the status of resources after they have started. The health check mechanism is tied into the resource lifecycle: 1. When a resource transitions to the `Running` state, Aspire checks if it has any associated health check annotations (typically added via `.WithHealthCheck(...)`). 2. **If health checks are configured:** Aspire begins executing these checks periodically. The resource is considered fully “ready” only after its health checks pass successfully. Once healthy, Aspire automatically publishes the `ResourceReadyEvent`. 3. **If no health checks are configured:** The resource is considered “ready” as soon as it enters the `Running` state. Aspire automatically publishes the `ResourceReadyEvent` immediately in this case. This automatic handling ensures that dependent resources (using mechanisms like `.WaitFor(...)`) only proceed when the target resource is truly ready, either by simply running or by passing its defined health checks. Danger Developers should **not** manually publish the `ResourceReadyEvent`. Aspire manages the transition to the ready state based on the presence and outcome of health checks. Manually firing this event can interfere with the orchestration logic. ### Resource logging [Section titled “Resource logging”](#resource-logging) Aspire supports logging output on a per-resource basis, which is displayed in the console window and can be surfaced in the dashboard. This log stream is especially useful for monitoring what a resource is doing in real time. For built-in resources, Aspire captures and forwards output from: * `stdout` and `stderr` of containers (e.g., Docker). * Process output from executables or .NET projects. For custom resources, developers can write directly to a resource’s log using the `ResourceLoggerService`. This service provides an `ILogger` scoped to the individual resource instance, enabling human-readable, contextual logging. ```csharp var logger = resourceLoggerService.GetLogger(myResource); logger.LogInformation("Starting provisioning…"); ``` Note A full example demonstrating custom resource logging with the Talking Clock resource can be found in the [Full Examples](/architecture/resource-examples/) section. #### Common resource logging APIs [Section titled “Common resource logging APIs”](#common-resource-logging-apis) The following APIs are likely to be used when working with resource logging: | API | Description | | -------------------------------------------- | --------------------------- | | `ResourceLoggerService.GetLogger(IResource)` | Returns a scoped `ILogger`. | | `ResourceLoggerService.WatchAsync` | Stream log lines. | Resource logs are designed to be human-readable and provide insights into the resource’s behavior, state changes, and any issues encountered during execution. Use the `ResourceNotificationService` to publish structured state changes. ## Standard interfaces [Section titled “Standard interfaces”](#standard-interfaces) Aspire defines a set of **optional standard interfaces** that allow resources to declare their capabilities in a structured, discoverable way. Implementing these interfaces enables **dynamic wiring, publishing, service discovery, and orchestration** without hardcoded type knowledge. These interfaces are the foundation for Aspire’s polymorphic behaviors — enabling tools, publishers, and the runtime to treat resources uniformly based on what they can do, rather than what they are. ### Benefits of standard interfaces [Section titled “Benefits of standard interfaces”](#benefits-of-standard-interfaces) * **Dynamic discovery:** Tooling and runtime systems can automatically adapt based on resource capabilities. * **Loose coupling:** Behaviors (like environment wiring, service discovery, or connection sharing) are opt-in. * **Extensibility:** New resource types can integrate seamlessly into the Aspire ecosystem by implementing one or more interfaces. ### Common interfaces [Section titled “Common interfaces”](#common-interfaces) | Interface | Purpose | | ------------------------------- | ----------------------------------------------------------------------------- | | `IResourceWithArgs` | Supplies additional CLI arguments when launching a project or executable. | | `IResourceWithConnectionString` | Provides a connection string output for consumers to connect to the resource. | | `IResourceWithEndpoints` | Exposes ports, URLs, or connection points that other resources can consume. | | `IResourceWithEnvironment` | Supports setting environment variables for the resource. | | `IResourceWithServiceDiscovery` | Registers a service hostname and metadata for discovery by other resources. | | `IResourceWithWaitSupport` | This resource can wait for other resources. | | `IResourceWithWithoutLifetime` | This resource does not have a lifecycle. (e.g. connection string, parameter) | ### Examples per interface [Section titled “Examples per interface”](#examples-per-interface) **`IResourceWithEnvironment`** ```csharp builder.WithEnvironment("MY_SETTING", "value"); ``` Allows setting environment variables that are passed to the resource when it starts. **`IResourceWithServiceDiscovery`** ```csharp builder.WithReference(myResourceWithDiscovery); ``` Exposes the resource via DNS-style service discovery. Downstream resources can refer to it by logical name. **`IResourceWithEndpoints`** ```csharp builder.GetEndpoint("http"); ``` When a resource implements `IResourceWithEndpoints`, it allows referencing specific endpoints (e.g., `http`, `tcp`) for reverse proxies or connection targets. **`IResourceWithConnectionString`** ```csharp builder.WithReference(myDatabaseResource); ``` Allows wiring a database connection string into environment variables, configurations, or CLI arguments. **`IResourceWithArgs`** ```csharp builder.WithArgs("2", "--url", endpoint); ``` Allows setting command-line arguments on the resource. **`IResourceWithWaitSupport`** ```csharp builder.WaitFor(otherResource) ``` This resource can wait on other resources. A `ParameterResource` is an example of resources that cannot wait. Note These APIs and behaviors are defined in the [📦 Aspire.Hosting](https://www.nuget.org/packages/Aspire.Hosting) package. ### Importance of polymorphism [Section titled “Importance of polymorphism”](#importance-of-polymorphism) By modeling behaviors through interfaces rather than concrete types, Aspire enables: * **Tooling flexibility**: Publishers can wire environment variables, endpoints, and arguments generically. * **Runtime uniformity**: Dashboards and orchestrators treat resources based on capabilities, not type-specific logic. * **Ecosystem extensibility**: New resource types can plug into the system without modifying core code. Interfaces allow Aspire to remain **open, flexible, and adaptable** as new types of services, platforms, and deployment targets emerge. # Resource Publishing > Learn how Aspire publishes resource manifests as JSON for deployment tooling, including manifest annotations, value providers, and structured field expressions. Aspire provides a flexible mechanism for publishing resource manifests, enabling seamless integration with various deployment environments. Resources are serialized into JSON format, which can be consumed by deployment tools. Custom resources that publish JSON manifest entries must: 1. **Register a callback** using `ManifestPublishingCallbackAnnotation` in the constructor. 2. **Implement the callback** to write JSON via `ManifestPublishingContext.Writer`. 3. **Use value objects** (`IManifestExpressionProvider`) for structured fields. Resources can opt-out of being included in the publishing manifest entirely by calling the `ExcludeFromManifest()` extension method on the `IResourceBuilder`. Resources marked this way will be omitted when generating publishing assets like Docker Compose files or Kubernetes manifests. ## Registering the callback [Section titled “Registering the callback”](#registering-the-callback) Consider the following example of a custom Azure Bicep resource that publishes its parameters to a manifest: AzureBicepResource.cs ```csharp public class AzureBicepResource : Resource, IAzureResource { public AzureBicepResource(string name, ...) : base(name) { Annotations.Add(new ManifestPublishingCallbackAnnotation(WriteToManifest)); } } ``` ## Writing to the manifest [Section titled “Writing to the manifest”](#writing-to-the-manifest) As an example, the `WriteToManifest` method serializes the resource’s parameters into a JSON object. This method is invoked during the manifest publishing phase: ```csharp public virtual void WriteToManifest(ManifestPublishingContext context) { context.Writer.WriteString("type", "azure.bicep.v0"); context.Writer.WriteString("path", context.GetManifestRelativePath(path)); context.Writer.WriteStartObject("params"); foreach (var kv in Parameters) { context.Writer.WritePropertyName(kv.Key); var v = kv.Value is IManifestExpressionProvider p ? p.ValueExpression : kv.Value?.ToString(); context.Writer.WriteString(kv.Key, v ?? ""); context.TryAddDependentResources(kv.Value); } context.Writer.WriteEndObject(); } ``` ## Summary table [Section titled “Summary table”](#summary-table) The following table summarizes the key steps and conventions for publishing resources: | Step | API / Call | Purpose | | --------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------- | | Register callback | `Annotations.Add(new ManifestPublishingCallbackAnnotation(WriteToManifest))` | Hook custom JSON writer | | Implement `WriteToManifest` | Use `context.Writer` to emit JSON properties | Define resource manifest representation | | Structured fields | `IManifestExpressionProvider.ValueExpression` | Ensure publish-time placeholders are preserved | ## Key conventions [Section titled “Key conventions”](#key-conventions) | Convention | Rationale | | -------------------------------- | ---------------------------------------------- | | Data-only resource classes | Separates data model from behavior | | `*BuilderExtensions` classes | Groups all API methods per integration | | Public annotations | Allow dynamic runtime addition/removal | | `[ResourceName]` attribute | Enforces valid resource naming at compile time | | Preserve parameter/value objects | Ensures deferred evaluation of secrets/outputs | # That's a wrap! > Aspire Conf

Thank you for joining us and celebrating the release of Aspire 13.3. Watch the replay on our YouTube playlist.

[Replay the Full Aspire Conf 2026 Livestream](https://www.youtube-nocookie.com/embed/6j61K9Sna2M?rel=0\&modestbranding=1\&playsinline=1) ## Connect with us The event may be over, but the conversation isn't! Join us on [Discord](https://discord.com/invite/raNPcaaSj8) to chat with the team and community members, ask questions, and share feedback. Follow us on [X](https://x.com/aspiredotdev) and [BlueSky](https://bsky.app/profile/aspire.dev) for announcements, clips, and quick updates. See you there! Search sessions or speakers… Show local time24h 9:00 AM PT1 session * 9:00 AM PT60 min ### Keynote: Come Meet the New Aspire Come meet the new Aspire. Discover how Aspire can transform the way you build and deploy your distributed apps and agents. With code-centric control, orchestrate and observe simple or complex systems with no rewrites, and deploy anywhere. ![Maddy Montaquila](/_astro/maddy.R23kbbR5_Z1IhXVd.webp) Maddy MontaquilaPrincipal Product Manager · Aspire @ Microsoft ![Damian Edwards](/_astro/damian._RJzLLTc_Z1nLKyz.webp) Damian EdwardsPrincipal Architect · Aspire @ Microsoft ![David Fowler](/_astro/fowler.BUeY7dbZ_Z1UQrpH.webp) David FowlerDistinguished Engineer · Aspire @ Microsoft 10:00 AM PT1 session * 10:00 AM PT45 min ### From Localhost to Liftoff: Aspire for Newbies What happens when you learn Aspire alongside someone seeing it for the first time? David walks Claudia through a live, beginner-friendly tour — from localhost to the cloud. Expect real questions, clear explanations, and a fresh look at how Aspire handles orchestration, observability, and service composition today. Whether you’re new or catching up, you’ll leave with a simple mental model and a clear way to explain Aspire to your team. ![David Pine](/_astro/pine.pNqi-i4I_2655Tl.webp) David PineSenior Software Engineer · Aspire @ Microsoft ![Claudia Regio](/_astro/claudia.kx-UQAz7_ZzJd13.webp) Claudia RegioSenior Product Manager · Dev Tools @ Microsoft 11:00 AM PT1 session * 11:00 AM PT30 min ### Aspire to Be Agentic: Designing Distributed Agentic Systems Without the Chaos Agentic systems are inherently distributed: multiple agents, multiple services, multiple languages. And right from the start, things get messy fast. Silent failures. Unclear execution paths. Too many “What tool did the agent call?” moments. Aspire’s polyglot support and built-in observability help you bring order to the chaos. You’ll see how much easier your life becomes when building agentic applications with Aspire. ![Tommaso Stocchi](/_astro/tommaso.C79OdxN__Z2qFNaX.webp) Tommaso StocchiCloud Solution Architect · Microsoft ![Seth Juarez](/_astro/seth.BO47DQgv_Z1QIIHT.webp) Seth JuarezPrincipal Product Manager · DevRel @ Microsoft 11:30 AM PT1 session * 11:30 AM PT30 min ### Beyond Telemetry: Supercharging DevEx with the Aspire Dashboard Aspire is often positioned as a way to model and run distributed applications, but its biggest impact is how it improves the everyday developer experience. In this talk, we'll focus on small, practical features in Aspire that reduce friction during local development and testing. Rather than adding complexity, these capabilities help you move faster with fewer context switches. Get some concrete ideas for using Aspire to make your app dev more approachable, safer to operate, and easier to work on day to day. Aspire doesn’t just help you run distributed apps—it helps you think less while building them. ![Michael Cummings](/_astro/michaelcummings.BAdmlyZb_1i7zMz.webp) Michael CummingsPrincipal Software Engineer · NuGet + VS Marketplace @ Microsoft 12:00 PM PT1 session * 12:00 PM PT30 min ### Coding Agents Need Aspire Too Your coding agents are only as good as the context they can access. Aspire hands them the keys — your entire app topology, real-time logs and traces, and resource commands like stop and restart — all with zero setup. You'll see how Aspire turns your agents from helpful assistants into full-stack collaborators you can actually trust. ![Pierce Boggan](/_astro/pierce.BvFBoA-x_1l7s16.webp) Pierce BogganPM Lead · VS Code + GitHub Copilot @ Microsoft 12:30 PM PT1 session * 12:30 PM PT30 min ### From Microservices to Water Sensors: End-to-End Testing with Aspire In this session, Andres will walk through how he uses Aspire to test an end-to-end pipeline that spans from cloud microservices to physical IoT devices — specifically an automated irrigation system powered by Arduino sensors and water pumps. You'll learn how Aspire's orchestration and integration testing capabilities can verify not just your web APIs and databases, but the full journey of data from a soil moisture sensor through HTTP endpoints to a real-time dashboard. Whether you're building traditional web apps or pushing Aspire into unconventional territory, you'll walk away with practical patterns for reliable end-to-end testing. ![Andres Rodriguez](/_astro/andres.Du6EO1yW_2e6Tzg.webp) Andres Rodriguez 1:00 PM PT1 session * 1:00 PM PT30 min ### One AppHost, Many Languages Aspire makes polyglot systems feel like one product by letting you run and wire everything through a single AppHost. Chris will show some popular patterns - like a Go backend + Vite frontend, Python API + JS frontend, Spring Boot with PostgreSQL, and C# API with CosmosDB. You'll see the same repeatable workflow for local dev, service discovery, and config across Python, TypeScript, Go, Java, and .NET, without the usual chaotic repo setup and onboarding. ![Chris Ayers](/_astro/chrisayers.ElZ8SppO_2hH6YJ.webp) Chris AyersPrincipal Software Engineer · Azure @ Microsoft 1:30 PM PT1 session * 1:30 PM PT30 min ### TypeScript and Aspire: Type Safety for Your Dev Experience Types aren't just for your server-side C#: they're a huge benefit in frontend and full-stack logic too! Let's dive into all the wonderfully fully-typed libraries and utilities in a freshly installed Aspire app. We'll cover the basics of how types work in TypeScript compared to traditional languages like C#, how they simultaneously catch bugs and help you write features in your code, and uncover some seriously nifty features of the TypeScript type system along the way. ![Josh Goldberg](/_astro/joshg.CgDUwA6e_5UPl4.webp) Josh GoldbergSenior Frontend Developer · Sentry 2:00 PM PT1 session * 2:00 PM PT30 min ### Customer Spotlight: Aspire for Windows 365 - Reliability, Extensibility, and Multi-Repo Rollout with AI We'll share how Windows 365 doubled Aspire adoption while improving reliability by driving E2E CloudTest success and systematically removing the top onboarding blockers, including key reliability fixes for Azure Functions and Cosmos DB. We'll cover the concrete work that made runs consistently “green,” plus the repeatable onboarding patterns used to move services onto Aspire and CloudTest at scale. Finally, we'll demo and explain how Aspire acts as the agent orchestrator, with Aspire extensibility and the GitHub Copilot SDK enabling AI-driven multi-repo rollout via an agent team—using analysis, remediation, and evolution agents to generate ready-to-merge pull requests that standardize repositories to a quality-gated, green baseline, without manual repo-by-repo effort. ![Chuanbo Zhang](/_astro/chuanbo.DCrLgxx2_Z2vwBw6.webp) Chuanbo ZhangPrincipal Software Engineer · Windows 365 @ Microsoft ![Yongyu Chen](/_astro/yongyuchen.Dw8fVr-J_Z1sqCHJ.webp) Yongyu ChenSenior Software Engineer Manager · Windows 365 @ Microsoft ![Jisheng Xing](/_astro/jisheng.qfgXa6dO_Z1t4n6k.webp) Jisheng XingSenior Software Engineer · Windows 365 @ Microsoft 2:30 PM PT1 session * 2:30 PM PT30 min ### Aspire Escapes the Inner Loop and Does Deployment Everyone knows that Aspire makes your development inner loop awesome, but did you know that it can also be used to streamline your deployments - all the way to production! Recent releases of Aspire have significantly improved Aspire's end-to-end deployment capabilities and now is a great time to get across how they work and how it can be adapted to suit your specific environment. ![Mitch Denny](/_astro/mitch.D1LjWTa-_Z1K3Xqy.webp) Mitch DennyPrincipal Software Engineer · Aspire @ Microsoft 3:00 PM PT1 session * 3:00 PM PT30 min ### Building and Deploying with Aspire and AWS Learn how to use Aspire with Amazon Web Services to streamline both local development and cloud deployment. This session demonstrates the new support for running and debugging AWS Lambda functions locally within Aspire, enabling a fast inner development loop. Then see how Aspire applications can be deployed to AWS by combining Aspire’s orchestration model with AWS Cloud Development Kit (CDK). ![Norm Johanson](/_astro/norm.7CL7npL8_Z1P1uHA.webp) Norm JohansonPrincipal Developer Engineer · AWS 3:30 PM PT1 session * 3:30 PM PT30 min ### Aspire at OpenCode OpenCode and Aspire make a strong pair for practical, observable agent workflows. In this demo-driven session, I’ll show how Aspire gives both me and an OpenCode agent access to the same OpenTelemetry data, so we can inspect traces, metrics, and runtime behavior from the same human-readable view instead of relying on hidden magic. You’ll learn how to use Aspire as a local observability surface for agent-assisted development, how to ground an agent in the same evidence a human would use, and why that leads to more trustworthy debugging. Surprisingly, OpenCode itself is a distributed application, which makes it an especially compelling system to explore this way. ![Luke Parker](/_astro/lukeParker.B5QKDhl9_Z1P73M0.webp) Luke ParkerCooking · OpenCode 4:00 PM PT1 session * 4:00 PM PT30 min ### Contributing to Aspire Aspire is open-source, and our community is the best in the game. Getting involved is easier than you think — whether that's filing an issue, contributing code to the core repo, helping build out aspire.dev, or shipping integrations in the Community Toolkit. In this session, Jose (Aspire's engineering manager) and Adam (one of the devs on the team) will break down all the ways you can contribute and pull back the curtain on how our code gets reviewed, tested, and released. ![Jose Perez Rodriguez](/_astro/jose.RQn8W6XC_Z2jbdYb.webp) Jose Perez RodriguezPrincipal Engineering Lead · Aspire @ Microsoft ![Adam Ratzman](/_astro/adam.Bw_Ms1Rw_Z2n4Xiq.webp) Adam RatzmanSenior Software Engineer · Aspire @ Microsoft 4:30 PM PT1 session * 4:30 PM PT15 min ### Closing Wrap-up the first ever Aspire Conf! ![Maddy Montaquila](/_astro/maddy.R23kbbR5_Z1IhXVd.webp) Maddy MontaquilaPrincipal Product Manager · Aspire @ Microsoft ![Damian Edwards](/_astro/damian._RJzLLTc_Z1nLKyz.webp) Damian EdwardsPrincipal Architect · Aspire @ Microsoft ![David Fowler](/_astro/fowler.BUeY7dbZ_Z1UQrpH.webp) David FowlerDistinguished Engineer · Aspire @ Microsoft # Aspire community channels and links > Connect with the Aspire team and community across Discord, GitHub Discussions, livestreams, social channels, and contribution platforms for distributed apps. Built together, in the open. Join thousands of developers building, contributing, and learning together across social channels, live streams, and open-source repos. [Start contributing](/community/contributor-guide/)[Watch videos](/community/videos/) ## Connect with us [Section titled “Connect with us”](#connect-with-us) ### [Join us on Discord](https://discord.com/invite/raNPcaaSj8) [Chat with the team and community members, ask questions, and share feedback in real time.](https://discord.com/invite/raNPcaaSj8) [Learn more ](https://discord.com/invite/raNPcaaSj8) ### [Read our blog](https://devblogs.microsoft.com/aspire) [Get release updates, announcements, and deep dives from the Aspire engineering team.](https://devblogs.microsoft.com/aspire) [Learn more ](https://devblogs.microsoft.com/aspire) ### [Follow us on BlueSky](https://bsky.app/profile/aspire.dev) [See community highlights, quick tips, and follow the latest Aspire news and releases.](https://bsky.app/profile/aspire.dev) [Learn more ](https://bsky.app/profile/aspire.dev) ### [Follow us on X](https://x.com/aspiredotdev) [Keep up with announcements, clips, and quick updates from the team and community.](https://x.com/aspiredotdev) [Learn more ](https://x.com/aspiredotdev) ### [Watch us on YouTube](https://youtube.com/@aspiredotdev) [Catch Aspire sessions, deep-dive demos, conference talks, and livestream replays.](https://youtube.com/@aspiredotdev) [Learn more ](https://youtube.com/@aspiredotdev) ### [Watch us on Twitch](https://twitch.tv/aspiredotdev) [Join live community streams, pair-programming sessions, and interact with us in real time.](https://twitch.tv/aspiredotdev) [Learn more ](https://twitch.tv/aspiredotdev) ## Watch and learn [Section titled “Watch and learn”](#watch-and-learn) Catch up on the latest videos from the Aspire team and community. From deep-dive demos to conference talks, there’s a wealth of content to explore. * YouTube * Twitch ## Get involved [Section titled “Get involved”](#get-involved) Whether you’re fixing a typo, building an integration, or translating docs — every contribution makes Aspire better for everyone. ### [Contribute to Aspire](https://github.com/microsoft/aspire) [Help improve the core Aspire project with code, issues, and ideas. The runtime, integrations, and tooling are all open source.](https://github.com/microsoft/aspire) [View on GitHub](https://github.com/microsoft/aspire) ### [Contribute to aspire.dev](/community/contributor-guide/) [Improve docs, guides, and examples on this very site. Start with the contributor guide to set up your environment.](/community/contributor-guide/) [Contributor guide](/community/contributor-guide/) ### [Help translate](/community/translation-guide/) [Aspire docs are available in 16+ languages. Help make Aspire accessible to developers around the world.](/community/translation-guide/) [Translation guide](/community/translation-guide/) ### [Meet the contributors](/community/contributors/) [See who is building Aspire. Browse the contributor list and discover the people behind the project.](/community/contributors/) [View contributors](/community/contributors/) ## Ready to be part of it? Aspire is open source and community driven. Whether you code, write docs, or spread the word — there's a place for you. [Read the contributor guide](/community/contributor-guide/)[Join Discord](https://discord.com/invite/raNPcaaSj8) # Contributor guide for aspire.dev > Learn how to contribute to aspire.dev: clone the repo, run the docs site locally, write Starlight MDX, follow the style guide, and open a pull request. Thank you for your interest in contributing to `aspire.dev`! Whether you’re fixing typos, adding new content, or improving existing pages, this guide will help you get started and your contributions are greatly appreciated. ## 🚀 About this site [Section titled “🚀 About this site”](#-about-this-site) This documentation site is built using [Starlight](https://starlight.astro.build/), a full-featured documentation theme built on top of [Astro](https://astro.build/). Starlight provides a fast, accessible, and SEO-friendly foundation, while Astro’s component-based architecture makes it easy to create and maintain content. ## 🤔 Ways to contribute [Section titled “🤔 Ways to contribute”](#-ways-to-contribute) There are several ways you can contribute to `aspire.dev`: * **Small fixes** - Correct typos, grammar mistakes, or formatting issues. * **Content additions** - Add new documentation pages or sections to cover missing topics. * **Content improvements** - Enhance existing documentation with clearer explanations, updated information, or additional examples. * **Code contributions** - Improve the site’s codebase, fix bugs, or add new features. There’s also different methods for contributing, from selecting the **Edit page** button at the bottom of any documentation page, to using [GitHub Codespaces](/get-started/github-codespaces/) for a fully configured development environment, or setting up a local development environment on your machine. ### Click-edit [Section titled “Click-edit”](#click-edit) Selecting the **Edit page** button at the bottom of any documentation page will take you to the corresponding file in the GitHub repository. From there, you can make changes directly in the GitHub web interface and submit a pull request. This is best suited for small fixes or minor content additions. Tip If you haven’t noticed the **Edit page** button, scroll to the bottom of this page and you should see it there: ![Edit page button at the bottom of a documentation page](/_astro/edit-page.DKe4ror6_ZIxWJS.webp) ### Codespaces [Section titled “Codespaces”](#codespaces) To avoid local setup, you can use [GitHub Codespaces](/get-started/github-codespaces/) for a fully configured development environment in the cloud. This is ideal for larger contributions or if you prefer not to set up a local environment. [![Open microsoft\/aspire.dev in GitHub Codespaces](https://github.com/codespaces/badge.svg "Open microsoft\/aspire.dev in GitHub Codespaces")](https://codespaces.new/microsoft/aspire.dev) Note If you choose to use Codespaces, please still refer to the rest of this guide for information on writing style, code quality, and the contribution workflow. Continue reading at the [Writing style section below](#%EF%B8%8F-writing-style-guide). ### Local development [Section titled “Local development”](#local-development) If you prefer to work locally, you can set up a development environment on your machine. Follow the instructions below to get started. ## 📋 Prerequisites [Section titled “📋 Prerequisites”](#-prerequisites) Before you begin, ensure you have the following installed: * [Node.js](https://nodejs.org/en/download) (LTS version recommended) - For running the development server * [pnpm](https://pnpm.io/installation) - Fast, disk space efficient package manager * [Visual Studio Code](https://code.visualstudio.com/) - Recommended code editor * [Git](https://git-scm.com/downloads) - For version control ## ⚙️ Local dev setup [Section titled “⚙️ Local dev setup”](#️-local-dev-setup) 1. Clone the `aspire.dev` repository. ```bash git clone https://github.com/microsoft/aspire.dev.git ``` 2. Navigate to the `aspire.dev` directory. ```bash cd aspire.dev ``` 3. Install dependencies * pnpm ```bash pnpm install ``` * npm ```bash npm install ``` Stop using npm If you’re not already using `pnpm`, consider switching! [![pnpm logo](/_astro/pnpm.Bm2ieaYB_j3OI6.svg)](https://pnpm.io/installation "https://pnpm.io/installation") It’s faster and more efficient than `npm` or `yarn`. Install globally with: ```bash npm install -g pnpm ``` 4. Run the development server ```bash pnpm dev ``` This starts the Vite development server for the frontend and provide hot-reload capabilities. 5. View the site locally Open your browser to `http://localhost:4321` (or the port shown in your terminal) Tip During local development, the site search functionality is disabled. This is normal behavior as the search index is built during the production build process. To test search functionality, run a production build locally using `pnpm build` and then preview it with `pnpm preview`. ### Known formatting limitations [Section titled “Known formatting limitations”](#known-formatting-limitations) We expose `lint` and `format` scripts in the `package.json` to help maintain code quality and consistency. This isn’t something that you’re need to run manually. However, regardless of whether or not you run these scrips, be aware of the following known limitation when working with MDX files. Caution **Prettier and Steps components** The Prettier formatter has a known limitation when formatting MDX files containing the `Steps` component. Prettier incorrectly converts all ordered lists (`ol`) as a single line, which causes the steps to render malformed. **Workaround:** Always ensure there is a blank line between each step item in a `Steps` component. For example: ```md 1. First step with content 1. Second step with content 1. Third step with content ``` Without the blank lines between steps, the inner content won’t render correctly. If you notice steps appearing malformed after running `pnpm format`, manually add the blank lines back before committing. ## ➡️ Git workflow [Section titled “➡️ Git workflow”](#️-git-workflow) 1. Start from an issue (or a discussion that leads to an issue) 2. Fork the repository As mentioned in the local dev setup section, start by forking the `aspire.dev` repository to your own GitHub account 3. Create a new branch for your changes ```bash git checkout -b feature/your-feature-name ``` 4. Make your changes, considering the writing style guide 5. Commit with descriptive messages 6. Push to your fork 7. Create a pull request, and always follow the [Code of Conduct](https://github.com/microsoft/aspire.dev/blob/main/CODE_OF_CONDUCT.md) ## 🧩 Adding a new framework integration [Section titled “🧩 Adding a new framework integration”](#-adding-a-new-framework-integration) If you’ve built a new Community Toolkit hosting integration (e.g. `CommunityToolkit.Aspire.Hosting.`) and want to document it on `aspire.dev`, you’ll need to touch several files. Here’s a summary of the steps, using the Perl integration as an example: 1. **Create the documentation page** Add a new MDX file at `src/frontend/src/content/docs/integrations/frameworks/.mdx`. Use an existing framework page (such as `python.mdx` or `java.mdx`) as a template. Include frontmatter with a `title`, any required component imports, and a `Badge` indicating it’s a Community Toolkit integration. 2. **Add an icon asset** Place an SVG icon for the framework in `src/frontend/src/assets/icons/`. Reference it in your MDX page with an `Image` component import. 3. **Register the sidebar entry** In `src/frontend/config/sidebar/integrations.topics.ts`, add a new entry for your framework in the frameworks list (keep alphabetical order): ```ts { label: '', slug: 'integrations/frameworks/' }, ``` 4. **Add the NuGet package name** In `src/frontend/src/data/aspire-integration-names.json`, add the full NuGet package name for your integration (keep alphabetical order): ```json "CommunityToolkit.Aspire.Hosting.", ``` 5. **Add license attribution** If your integration uses assets or technologies with specific licenses, add entries in `src/frontend/src/data/thanks-license-titles.ts`: ```ts '': ': ', ``` After making these changes, run `pnpm dev` (or `aspire start`) locally to verify the page renders correctly, the sidebar navigation works, and the icon displays as expected. ## ✍️ Writing style guide [Section titled “✍️ Writing style guide”](#️-writing-style-guide) When contributing to `aspire.dev`, follow these writing guidelines to ensure consistency and clarity: * **Use clear and concise language** - Aim for simplicity. Avoid jargon unless necessary, and explain technical terms when they first appear. * **Be consistent** - Follow existing conventions in terminology, formatting, and structure. Refer to other documentation pages for examples. * **Use active voice** - Write in active voice to make instructions and explanations more direct and engaging. * **Use sentence case** - Capitalize only the first word and proper nouns in headings, sidebars, and table of contents. * **Be inclusive** - Use inclusive language that respects all readers. Avoid gendered terms and stereotypes. * **Provide examples** - Where applicable, include code snippets or examples to illustrate concepts. * **Use proper grammar and spelling** - Proofread your contributions to ensure they are free of errors and typos. * **Structure content logically** - Use headings, subheadings, and lists to organize information in a way that is easy to follow. * **Link to relevant resources** - When mentioning concepts, tools, or related documentation, provide links to help readers find more information. * **Follow formatting conventions** - Use consistent formatting for code snippets, commands, and technical terms. Refer to the examples in this guide for guidance. * **Review existing content** - Before adding new content, review existing documentation to avoid duplication and ensure coherence. ### Third-party links [Section titled “Third-party links”](#third-party-links) Third-party links are appropriate when they help readers complete a task or understand a topic. Apply these standards to both authored and generated content: * **Use links sparingly** - Include third-party links only when they’re directly relevant. Prefer Aspire documentation when it covers the same information. * **Present resources neutrally** - Describe third-party resources factually. Avoid endorsements, marketing claims, promotional comparisons, and calls to purchase or sign up. * **Avoid promotion** - Don’t add links or surrounding content whose primary purpose is to market a third-party offering or position it as a preferred replacement for Aspire. * **Choose substantive destinations** - Link to technical documentation or other substantive resources. Don’t link to product, pricing, lead-generation, or sales pages except as described in the following exceptions. * **Allow necessary exceptions** - Prerequisite download or account sign-up pages, pricing or billing references needed for cost planning, and project descriptions or links on attribution and acknowledgment pages are allowed. Keep each exception limited to the context that makes it necessary. * **Disclose affiliations** - In the **Third-party links and affiliations** section of the pull request description, disclose any material affiliation with a linked organization, such as employment, sponsorship, or ownership. An affiliation doesn’t automatically disqualify a link, but the link and surrounding content must meet the same editorial standards as any other contribution. * **Review generated content** - Automated updates and generated catalogs aren’t exempt. They may reproduce upstream metadata when that’s the catalog’s explicit purpose, but the reviewing maintainer must apply the same editorial standards to the surrounding content and linked destinations. ### AppHost-specific TypeScript and C# content [Section titled “AppHost-specific TypeScript and C# content”](#apphost-specific-typescript-and-c-content) When you are documenting AppHost-specific content that changes between TypeScript and C#, use synced `Tabs` and `TabItem` blocks with `syncKey='aspire-lang'`. Put TypeScript first so `apphost.mts` is the default for readers without a saved preference. Each code snippet should have its own TypeScript and C# selector, and the shared sync key keeps the selected language consistent across the page. Use `Tabs` for other choices such as package managers, deployment targets, IDEs, or CLI variants. If a feature is only available in the C# AppHost today, show the C# example by itself and add an explanatory note. Do not add a language selector for a single-language example. For the **On this page** table of contents to pick up a heading reliably, define the heading outside `Pivot` and `Tabs`. Headings inside those components are often missed or can produce incomplete results in the generated table of contents. ## 📝 Write Markdown [Section titled “📝 Write Markdown”](#-write-markdown) Here are some common Markdown formatting examples to help you write documentation: ### Frontmatter [Section titled “Frontmatter”](#frontmatter) You can customize individual pages in `aspire.dev` by setting values in their frontmatter. Frontmatter is set at the top of your files between `---` separators: src/content/docs/example.md ```md --- title: My page title --- Page content follows the second `---`. ``` Every page must include at least a `title`. See the [frontmatter reference](https://starlight.astro.build/reference/frontmatter/) for all available fields and how to add custom fields. #### Social card metadata (Open Graph) [Section titled “Social card metadata (Open Graph)”](#social-card-metadata-open-graph) Aspire automatically generates a per-page Open Graph image at build time. Each card uses the site-wide `og-image.png` as a full-bleed background and overlays a topic pill (with the same icon you see in the topics sidebar), the page `title`, and — if the page has one — its `description`. Long titles and descriptions are truncated with an ellipsis so previews stay readable at small social-card sizes. The generated image lives at `/og/.png` and is referenced by the page’s `og:image` and `twitter:image` meta tags so links shared on Discord, Slack, Twitter/X, LinkedIn, and other platforms render a unique preview. To customize the social card for a specific page, set one of the following optional frontmatter fields: src/content/docs/example.mdx ```md --- title: My page title description: A short summary rendered on the card and shown next to the social-card image on most platforms. # Use a custom image instead of the auto-generated one. Accepts an absolute URL # or a site-relative path. ogImage: /img/custom-card.png # Or opt out of dynamic image generation entirely and fall back to the # site-wide og-image.png. og: false --- ``` Pages that intentionally have no description (such as the home page or splash landing pages) fall back to the site-wide marketing description. Translated pages reuse the site-wide `og-image.png` so the same imagery is shown across locales. ### Headings [Section titled “Headings”](#headings) Use `#` symbols to create headings. More `#` symbols create smaller headings: src/content/docs/example.md ```md ## Heading 2 ### Heading 3 #### Heading 4 ``` Headings are automatically created as bookmarks (shareable deep links) for easy navigation. Note Avoid using Heading 1 (`#`) in your content, as it is reserved for the page title defined in frontmatter. Tip To configure which headings appear in the **On this page** sidebar, use the `tableOfContents` frontmatter field. See the [tableOfContents reference](https://starlight.astro.build/reference/frontmatter/#tableofcontents) for more details. ### Text formatting [Section titled “Text formatting”](#text-formatting) **Bold text** is created with double asterisks: src/content/docs/example.md ```md **Bold text** ``` *Italic text* is created with an `_` (or single asterisks `*`—while valid, for consistency we recommend using `_`): src/content/docs/example.md ```md _Italic text_ ``` `Inline code` is created with backticks: src/content/docs/example.md ```md `Inline code` ``` ### Links [Section titled “Links”](#links) Links are created with square brackets and parentheses: src/content/docs/example.md ```md [David Pine](https://davidpine.net) ``` Renders as: [David Pine](https://davidpine.net) Additionally, when linking to other pages within `aspire.dev`, use site relative paths: src/content/docs/example.md ```md [Build your first Aspire app](/get-started/first-app/) ``` Renders as: [Build your first Aspire app](/get-started/first-app/) Note Site relative links should always include a trailing slash `/` at the end to ensure proper navigation. This is confugred by default for `aspire.dev`, for more information see [Astro Docs: trailingSlash](https://docs.astro.build/en/reference/configuration-reference/#trailingslash). ### Lists [Section titled “Lists”](#lists) Unordered lists use `-` (or `*`—while valid, for consistency we recommend using `-`): src/content/docs/example.md ```md - First item - Second item - Third item ``` Renders as: * First item * Second item * Third item Ordered lists use numbers: src/content/docs/example.md ```md 1. First step 2. Second step 3. Third step ``` Renders as: 1. First step 2. Second step 3. Third step ### Code blocks [Section titled “Code blocks”](#code-blocks) Use triple backticks with a language identifier for syntax highlighting: src/content/docs/example.md ````md ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); builder.AddProject("apiservice"); ``` ```` Renders as: AppHost.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddProject("apiservice"); ``` To add a title to a code block, use this syntax: src/content/docs/example.md ````md ```csharp title="Program.cs" var builder = DistributedApplication.CreateBuilder(args); builder.AddProject("apiservice"); ``` ```` Renders as: Program.cs ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddProject("apiservice"); ``` ### Blockquotes [Section titled “Blockquotes”](#blockquotes) Use `>` to create blockquotes: src/content/docs/example.md ```md > This is a note or important callout. ``` Renders as: > This is a note or important callout. ### Tables [Section titled “Tables”](#tables) Create tables using pipes `|` and hyphens `-`. Keep a blank line before and after the table, include one separator cell for each heading, and use at least three hyphens in each separator cell: src/content/docs/example.md ```md | Feature | Description | Status | | ------- | ----------- | ------ | | Dashboard | Web-based monitoring | Available | | Telemetry | OpenTelemetry support | Available | | Deployment | Kubernetes deployment | Preview | ``` Renders as: | Feature | Description | Status | | ---------- | --------------------- | --------- | | Dashboard | Web-based monitoring | Available | | Telemetry | OpenTelemetry support | Available | | Deployment | Kubernetes deployment | Preview | Tip You can align columns using colons: `| :--- |` for left, `| :---: |` for center, and `| ---: |` for right alignment. ### Horizontal rules [Section titled “Horizontal rules”](#horizontal-rules) Create a horizontal rule with three or more hyphens, asterisks, or underscores: src/content/docs/example.md ```md --- ``` Renders as: *** ### Strikethrough [Section titled “Strikethrough”](#strikethrough) Use double tildes to create strikethrough text: src/content/docs/example.md ```md ~~This text is crossed out~~ ``` Renders as: ~~This text is crossed out~~ ### Task lists [Section titled “Task lists”](#task-lists) Create interactive task lists in Markdown: ```md - [x] Add Aspire to your project - [x] Configure service defaults - [ ] Deploy to Azure - [ ] Set up monitoring ``` Renders as: * [x] Add Aspire to your project * [x] Configure service defaults * [ ] Deploy to Azure * [ ] Set up monitoring ### Nested lists [Section titled “Nested lists”](#nested-lists) You can nest lists by indenting with two spaces: ```md - Aspire components - Databases - PostgreSQL - Redis - Messaging - RabbitMQ - Azure Service Bus ``` Renders as: * Aspire components * Databases * PostgreSQL * Redis * Messaging * RabbitMQ * Azure Service Bus ### Escaping characters [Section titled “Escaping characters”](#escaping-characters) Use a backslash `\` to escape special Markdown characters: ```md \*This text is not italic\* \[This is not a link\] ``` Renders as: \*This text is not italic\* \[This is not a link] ### Line breaks [Section titled “Line breaks”](#line-breaks) End a line with two or more spaces to create a line break: ```md First line with two spaces at the end Second line ``` Or use an empty line to create a paragraph break. ## ➕ Markdown extensions [Section titled “➕ Markdown extensions”](#-markdown-extensions) The `aspire.dev` site supports several Markdown extensions to enhance your documentation: ### Mermaid diagrams [Section titled “Mermaid diagrams”](#mermaid-diagrams) You can write mermaid diagrams as code blocks: src/content/docs/example.md ````md ```mermaid graph TD A[build-apiservice] --> C[push-apiservice] B[provision-container-registry] --> C C --> D[deploy-apiservice] E[provision-cosmosdb] --> D F[provision-identity] --> D ``` ```` Renders as: ``` graph TD A[build-apiservice] --> C[push-apiservice] B[provision-container-registry] --> C C --> D[deploy-apiservice] E[provision-cosmosdb] --> D F[provision-identity] --> D ``` ### Asides [Section titled “Asides”](#asides) [Asides](https://starlight.astro.build/components/asides/), “admonitions”, “callouts”, or “alerts” are special highlighted blocks used to draw attention to important information, tips, warnings, or notes. The `:::` syntax creates asides given a type of `note`, `tip`, `caution`, or `danger` in both Markdown and [MDX](#write-mdx): Use the `:::` syntax as the default pattern in `aspire.dev` docs, including `.mdx` pages. Prefer the `Aside` component only when you specifically need a JSX-only composition pattern that fenced callouts cannot express cleanly. src/content/docs/example.md ````md :::note Some content in an aside. ::: :::caution Some cautionary content. ::: :::tip Other content is also supported in asides. ```js // A code snippet, for example. ``` ::: :::danger Do not give your password to anyone. ::: ```` Renders as: Note Some content in an aside. Caution Some cautionary content. Tip Other content is also supported in asides. ```js // A code snippet, for example. ``` Danger Do not give your password to anyone. Additionally, `aspire.dev` supports [GitHub Alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) syntax with the [community plugin](https://starlight-github-alerts.netlify.app/getting-started/): For example, you can write: #### Note [Section titled “Note”](#note) src/content/docs/example.md ```md > [!NOTE] > Useful information that users should know, even when skimming content. ``` Renders as: Note Useful information that users should know, even when skimming content. See the full demo here: [Starlight: GitHub Alerts](https://starlight-github-alerts.netlify.app/demo/). If you need a JSX-only fallback, see the [Aside component](#aside-component) section below. ## ☑️ Write MDX [Section titled “☑️ Write MDX”](#️-write-mdx) MDX files use the `.mdx` extension and combine standard Markdown with the power of JSX. This means you can write content and seamlessly embed interactive components—all in one file. [Learn more about MDX](https://mdxjs.com/docs/what-is-mdx/). With the power of Astro components, you can enhance your documentation with interactive elements, custom layouts, and dynamic content. To use any of the built-in Starlight or custom components available in `aspire.dev`, simply import them at the top of your MDX file and use them like regular JSX components. ### LinkButton component [Section titled “LinkButton component”](#linkbutton-component) src/content/docs/example.mdx ```mdx --- title: Example MDX Page --- import { LinkButton } from '@astrojs/starlight/components'; Here's an example of an MDX page with a custom button: Visit aspire.dev ``` Renders as: Here’s an example of an MDX page with a custom button: [Visit aspire.dev](https://aspire.dev) For all available components, see: [Starlight: Components](https://starlight.astro.build/components/using-components/). ### FileTree component [Section titled “FileTree component”](#filetree-component) Use the `FileTree` component from `starlight-plugin-icons` so file and folder entries render with icons: src/content/docs/example.mdx ```mdx --- title: Example MDX Page --- import FileTree from 'starlight-plugin-icons/components/FileTree.astro'; - src/ - components/ - Example.astro - content/ - docs/ - example.mdx ``` ### Aside component [Section titled “Aside component”](#aside-component) Prefer `:::` callouts for new docs content. The `Aside` component from Starlight is the fallback option when you need JSX composition inside a callout: src/content/docs/example.mdx ````mdx --- title: Example MDX Page --- import { Aside } from '@astrojs/starlight/components'; ```` Renders as: Note Some content in an aside. Caution Some cautionary content. Tip Other content is also supported in asides. ```js // A code snippet, for example. ``` Danger Do not give your password to anyone. ### Using custom components [Section titled “Using custom components”](#using-custom-components) To use custom components available in `aspire.dev`, import them at the top of your MDX file. Custom component imports rely on configured aliases—have a look at the `tsconfig.json` file for more information: tsconfig.json ```json { "extends": "astro/tsconfigs/strict", "include": [".astro/types.d.ts", "**/*"], "exclude": ["dist"], "compilerOptions": { "paths": { "@assets/*": ["./src/assets/*"], "@components/*": ["./src/components/*"], "@data/*": ["./src/data/*"], "@scripts/*": ["./src/scripts/*"], "@tests/e2e/*": ["./tests/e2e/*"], "@tests/typecheck/*": ["./tests/typecheck/*"], "@tests/unit/*": ["./tests/unit/*"], "@tests/*": ["./tests/*"], "@utils/*": ["./src/utils/*"] } } } ``` By using the `@components` alias, you can easily import any custom component from the `frontend/src/components/` directory. For example, to import the `LearnMore` component used in this guide: src/content/docs/example.mdx ```mdx --- title: Example MDX Page --- import LearnMore from '@components/LearnMore.astro'; Here's an example of using the `LearnMore` component: Please give our [repository a star on GitHub! ⭐](https://github.com/microsoft/aspire.dev) ``` ### Aspire language selectors [Section titled “Aspire language selectors”](#aspire-language-selectors) Use synced `Tabs` and `TabItem` blocks for AppHost content that switches between TypeScript and C#. Put TypeScript first so the `apphost.mts` tab appears on the left and is selected by default. Each AppHost code snippet should have its own selector, and every AppHost language selector on the page should share `syncKey='aspire-lang'` so a reader’s language choice applies to the whole page. Keep shared section headings outside the tabs. For example, write `## Add a Redis resource` before the `Tabs` block, then place the language-specific code inside the `csharp` and `typescript` tab items. This helps the **On this page** navigation stay accurate. src/content/docs/example.mdx ````mdx --- title: Example MDX Page --- import { Tabs, TabItem } from '@astrojs/starlight/components'; ```typescript title="apphost.mts" twoslash import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const cache = await builder.addRedis("cache"); const api = await builder.addProject("api", "../Api/Api.csproj"); await api.withReference(cache); await builder.build().run(); ``` ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); var cache = builder.AddRedis("cache"); builder.AddProject("api") .WithReference(cache); builder.Build().Run(); ``` ```` If the difference is not about AppHost language, keep using regular `Tabs`. The same TOC rule applies to `Tabs`: if a heading should appear in **On this page**, keep that heading outside the component and only put the variant-specific body content inside. Renders as: Here’s an example of using the `LearnMore` component: Please give our [repository a star on GitHub! ⭐](https://github.com/microsoft/aspire.dev) ## 🆘 Getting help [Section titled “🆘 Getting help”](#-getting-help) * **Issues** - Report bugs or request features via [GitHub Issues](https://github.com/microsoft/aspire.dev/issues) * **Discussions** - Join conversations in [GitHub Discussions](https://github.com/microsoft/aspire.dev/discussions) * **Discord** - Connect with the community on the [Aspire Discord](https://discord.com/invite/raNPcaaSj8) # Contributors 🤝 > Meet the contributors who help make Aspire better every day. Explore the global community of engineers, writers, and translators powering the Aspire ecosystem. Aspire wouldn’t be what it is today without the amazing contributions from our community. From code contributions to documentation improvements, every bit helps us grow and improve. Your ideas have the potential to shape the future of the project, and issues you uncover help others be successful too. Interested in contributing? [Contribute to aspire.dev!](/community/contributor-guide/) ## 🙏 Aspire contributors [Section titled “🙏 Aspire contributors”](#-aspire-contributors) Thank you to all the community members who have contributed to Aspire! Your efforts help shape the future of this project. Collaborating on Aspire means joining a vibrant community that’s building the next evolution of cloud-native applications. Aspire is the successor to Microsoft’s experimental “Project Tye”—originally called “Astra”—and its ambitions are far reaching and extensible. By contributing, you’re helping to define new patterns, unlock extensibility, and drive innovation for developers everywhere. Jump in and help us push the boundaries of what’s possible! * [![JamesNK](https://avatars.githubusercontent.com/u/303201?s=64 "JamesNK")](https://github.com/JamesNK) * [![mitchdenny](https://avatars.githubusercontent.com/u/513398?s=64 "mitchdenny")](https://github.com/mitchdenny) * [![eerhardt](https://avatars.githubusercontent.com/u/8291187?s=64 "eerhardt")](https://github.com/eerhardt) * [![davidfowl](https://avatars.githubusercontent.com/u/95136?s=64 "davidfowl")](https://github.com/davidfowl) * [![adamint](https://avatars.githubusercontent.com/u/20359921?s=64 "adamint")](https://github.com/adamint) * [![sebastienros](https://avatars.githubusercontent.com/u/1165805?s=64 "sebastienros")](https://github.com/sebastienros) * [![joperezr](https://avatars.githubusercontent.com/u/13854455?s=64 "joperezr")](https://github.com/joperezr) * [![radical](https://avatars.githubusercontent.com/u/1472?s=64 "radical")](https://github.com/radical) * [![danegsta](https://avatars.githubusercontent.com/u/50252651?s=64 "danegsta")](https://github.com/danegsta) * [![DamianEdwards](https://avatars.githubusercontent.com/u/249088?s=64 "DamianEdwards")](https://github.com/DamianEdwards) * [![aspire-repo-bot\[bot\]](https://avatars.githubusercontent.com/u/268009190?s=64 "aspire-repo-bot\[bot\]")](https://github.com/aspire-repo-bot\[bot]) * [![IEvangelist](https://avatars.githubusercontent.com/u/7679720?s=64 "IEvangelist")](https://github.com/IEvangelist) * [![dotnet-bot](https://avatars.githubusercontent.com/u/9011267?s=64 "dotnet-bot")](https://github.com/dotnet-bot) * [![karolz-ms](https://avatars.githubusercontent.com/u/15271049?s=64 "karolz-ms")](https://github.com/karolz-ms) * [![tlmii](https://avatars.githubusercontent.com/u/9613109?s=64 "tlmii")](https://github.com/tlmii) * [![drewnoakes](https://avatars.githubusercontent.com/u/350947?s=64 "drewnoakes")](https://github.com/drewnoakes) * [![ReubenBond](https://avatars.githubusercontent.com/u/203839?s=64 "ReubenBond")](https://github.com/ReubenBond) * [![afscrome](https://avatars.githubusercontent.com/u/289860?s=64 "afscrome")](https://github.com/afscrome) * [![Alirexaa](https://avatars.githubusercontent.com/u/70141416?s=64 "Alirexaa")](https://github.com/Alirexaa) * [![RussKie](https://avatars.githubusercontent.com/u/4403806?s=64 "RussKie")](https://github.com/RussKie) * [![ellahathaway](https://avatars.githubusercontent.com/u/67609881?s=64 "ellahathaway")](https://github.com/ellahathaway) * [![smitpatel](https://avatars.githubusercontent.com/u/1528107?s=64 "smitpatel")](https://github.com/smitpatel) * [![maddymontaquila](https://avatars.githubusercontent.com/u/12660687?s=64 "maddymontaquila")](https://github.com/maddymontaquila) * [![davidebbo](https://avatars.githubusercontent.com/u/556238?s=64 "davidebbo")](https://github.com/davidebbo) * [![BrennanConroy](https://avatars.githubusercontent.com/u/7574801?s=64 "BrennanConroy")](https://github.com/BrennanConroy) * [![timheuer](https://avatars.githubusercontent.com/u/4821?s=64 "timheuer")](https://github.com/timheuer) * [![Zombach](https://avatars.githubusercontent.com/u/52016832?s=64 "Zombach")](https://github.com/Zombach) * [![vnbaaij](https://avatars.githubusercontent.com/u/1761079?s=64 "vnbaaij")](https://github.com/vnbaaij) * [![jfversluis](https://avatars.githubusercontent.com/u/939291?s=64 "jfversluis")](https://github.com/jfversluis) * [![martincostello](https://avatars.githubusercontent.com/u/1439341?s=64 "martincostello")](https://github.com/martincostello) * [![wtgodbe](https://avatars.githubusercontent.com/u/14283640?s=64 "wtgodbe")](https://github.com/wtgodbe) * [![aaronpowell](https://avatars.githubusercontent.com/u/434140?s=64 "aaronpowell")](https://github.com/aaronpowell) * [![benjaminpetit](https://avatars.githubusercontent.com/u/20427417?s=64 "benjaminpetit")](https://github.com/benjaminpetit) * [![mmitche](https://avatars.githubusercontent.com/u/8725170?s=64 "mmitche")](https://github.com/mmitche) * [![DeagleGross](https://avatars.githubusercontent.com/u/31598696?s=64 "DeagleGross")](https://github.com/DeagleGross) * [![MattKotsenas](https://avatars.githubusercontent.com/u/51421?s=64 "MattKotsenas")](https://github.com/MattKotsenas) * [![ShilpiRach](https://avatars.githubusercontent.com/u/233947509?s=64 "ShilpiRach")](https://github.com/ShilpiRach) * [![tommasodotNET](https://avatars.githubusercontent.com/u/12819039?s=64 "tommasodotNET")](https://github.com/tommasodotNET) * [![Youssef1313](https://avatars.githubusercontent.com/u/31348972?s=64 "Youssef1313")](https://github.com/Youssef1313) * [![bart-vmware](https://avatars.githubusercontent.com/u/104792814?s=64 "bart-vmware")](https://github.com/bart-vmware) * [![vicancy](https://avatars.githubusercontent.com/u/668244?s=64 "vicancy")](https://github.com/vicancy) * [![normj](https://avatars.githubusercontent.com/u/1653751?s=64 "normj")](https://github.com/normj) * [![WeihanLi](https://avatars.githubusercontent.com/u/7604648?s=64 "WeihanLi")](https://github.com/WeihanLi) * [![zhiyuanliang-ms](https://avatars.githubusercontent.com/u/141655842?s=64 "zhiyuanliang-ms")](https://github.com/zhiyuanliang-ms) * [![ArcturusZhang](https://avatars.githubusercontent.com/u/10554446?s=64 "ArcturusZhang")](https://github.com/ArcturusZhang) * [![spboyer](https://avatars.githubusercontent.com/u/7681382?s=64 "spboyer")](https://github.com/spboyer) * [![kiapanahi](https://avatars.githubusercontent.com/u/4063578?s=64 "kiapanahi")](https://github.com/kiapanahi) * [![phenning](https://avatars.githubusercontent.com/u/2433750?s=64 "phenning")](https://github.com/phenning) * [![captainsafia](https://avatars.githubusercontent.com/u/1857993?s=64 "captainsafia")](https://github.com/captainsafia) * [![bjorkstromm](https://avatars.githubusercontent.com/u/7863439?s=64 "bjorkstromm")](https://github.com/bjorkstromm) * [![MatsM16](https://avatars.githubusercontent.com/u/17270481?s=64 "MatsM16")](https://github.com/MatsM16) * [![prom3theu5](https://avatars.githubusercontent.com/u/1518610?s=64 "prom3theu5")](https://github.com/prom3theu5) * [![sliekens](https://avatars.githubusercontent.com/u/1583241?s=64 "sliekens")](https://github.com/sliekens) * [![oising](https://avatars.githubusercontent.com/u/1844001?s=64 "oising")](https://github.com/oising) * [![Meir017](https://avatars.githubusercontent.com/u/9786571?s=64 "Meir017")](https://github.com/Meir017) * [![matthebrown](https://avatars.githubusercontent.com/u/45107667?s=64 "matthebrown")](https://github.com/matthebrown) * [![hewe-saxo](https://avatars.githubusercontent.com/u/105797295?s=64 "hewe-saxo")](https://github.com/hewe-saxo) * [![akoeplinger](https://avatars.githubusercontent.com/u/1376924?s=64 "akoeplinger")](https://github.com/akoeplinger) * [![Kahbazi](https://avatars.githubusercontent.com/u/19396090?s=64 "Kahbazi")](https://github.com/Kahbazi) * [![bgrainger](https://avatars.githubusercontent.com/u/188129?s=64 "bgrainger")](https://github.com/bgrainger) * [![paulomorgado](https://avatars.githubusercontent.com/u/470455?s=64 "paulomorgado")](https://github.com/paulomorgado) * [![cqnguy23](https://avatars.githubusercontent.com/u/44353219?s=64 "cqnguy23")](https://github.com/cqnguy23) * [![kundadebdatta](https://avatars.githubusercontent.com/u/87335885?s=64 "kundadebdatta")](https://github.com/kundadebdatta) * [![mtmk](https://avatars.githubusercontent.com/u/386903?s=64 "mtmk")](https://github.com/mtmk) * [![VincentH-Net](https://avatars.githubusercontent.com/u/1872271?s=64 "VincentH-Net")](https://github.com/VincentH-Net) * [![philliphoff](https://avatars.githubusercontent.com/u/6402946?s=64 "philliphoff")](https://github.com/philliphoff) * [![samsp-msft](https://avatars.githubusercontent.com/u/54915162?s=64 "samsp-msft")](https://github.com/samsp-msft) * [![SankeerthNara](https://avatars.githubusercontent.com/u/215511671?s=64 "SankeerthNara")](https://github.com/SankeerthNara) * [![SimonCropp](https://avatars.githubusercontent.com/u/122666?s=64 "SimonCropp")](https://github.com/SimonCropp) * [![stbau04](https://avatars.githubusercontent.com/u/67107950?s=64 "stbau04")](https://github.com/stbau04) * [![Pilchie](https://avatars.githubusercontent.com/u/312835?s=64 "Pilchie")](https://github.com/Pilchie) * [![RafaelJCamara](https://avatars.githubusercontent.com/u/52082556?s=64 "RafaelJCamara")](https://github.com/RafaelJCamara) * [![nellshamrell](https://avatars.githubusercontent.com/u/813007?s=64 "nellshamrell")](https://github.com/nellshamrell) * [![g7ed6e](https://avatars.githubusercontent.com/u/681739?s=64 "g7ed6e")](https://github.com/g7ed6e) * [![baronfel](https://avatars.githubusercontent.com/u/573979?s=64 "baronfel")](https://github.com/baronfel) * [![adegeo](https://avatars.githubusercontent.com/u/67293991?s=64 "adegeo")](https://github.com/adegeo) * [![alirezafzali](https://avatars.githubusercontent.com/u/220716413?s=64 "alirezafzali")](https://github.com/alirezafzali) * [![vhvb1989](https://avatars.githubusercontent.com/u/24213737?s=64 "vhvb1989")](https://github.com/vhvb1989) * [![Varorbc](https://avatars.githubusercontent.com/u/5714438?s=64 "Varorbc")](https://github.com/Varorbc) * [![twsouthwick](https://avatars.githubusercontent.com/u/583206?s=64 "twsouthwick")](https://github.com/twsouthwick) * [![stephentoub](https://avatars.githubusercontent.com/u/2642209?s=64 "stephentoub")](https://github.com/stephentoub) * [![shauryalowkeygotaura](https://avatars.githubusercontent.com/u/173814476?s=64 "shauryalowkeygotaura")](https://github.com/shauryalowkeygotaura) * [![marshalhayes](https://avatars.githubusercontent.com/u/17213165?s=64 "marshalhayes")](https://github.com/marshalhayes) * [![MackinnonBuck](https://avatars.githubusercontent.com/u/10456961?s=64 "MackinnonBuck")](https://github.com/MackinnonBuck) * [![Kumima](https://avatars.githubusercontent.com/u/93973732?s=64 "Kumima")](https://github.com/Kumima) * [![JoshLove-msft](https://avatars.githubusercontent.com/u/54595583?s=64 "JoshLove-msft")](https://github.com/JoshLove-msft) * [![javiercn](https://avatars.githubusercontent.com/u/6995051?s=64 "javiercn")](https://github.com/javiercn) * [![Muckenbatscher](https://avatars.githubusercontent.com/u/47030488?s=64 "Muckenbatscher")](https://github.com/Muckenbatscher) * [![brendandburns](https://avatars.githubusercontent.com/u/5751682?s=64 "brendandburns")](https://github.com/brendandburns) * [![aradalvand](https://avatars.githubusercontent.com/u/26527405?s=64 "aradalvand")](https://github.com/aradalvand) * [![askpt](https://avatars.githubusercontent.com/u/2493377?s=64 "askpt")](https://github.com/askpt) * [![andrevlins](https://avatars.githubusercontent.com/u/5325718?s=64 "andrevlins")](https://github.com/andrevlins) * [![WhitWaldo](https://avatars.githubusercontent.com/u/2238529?s=64 "WhitWaldo")](https://github.com/WhitWaldo) * [![vivekjm](https://avatars.githubusercontent.com/u/24496671?s=64 "vivekjm")](https://github.com/vivekjm) * [![vlesierse](https://avatars.githubusercontent.com/u/270232?s=64 "vlesierse")](https://github.com/vlesierse) * [![vsantele](https://avatars.githubusercontent.com/u/26800140?s=64 "vsantele")](https://github.com/vsantele) * [![SteveSandersonMS](https://avatars.githubusercontent.com/u/1101362?s=64 "SteveSandersonMS")](https://github.com/SteveSandersonMS) * [![rzikm](https://avatars.githubusercontent.com/u/32671551?s=64 "rzikm")](https://github.com/rzikm) * [![missymessa](https://avatars.githubusercontent.com/u/47990216?s=64 "missymessa")](https://github.com/missymessa) * [![mfcollins3](https://avatars.githubusercontent.com/u/104274?s=64 "mfcollins3")](https://github.com/mfcollins3) * [![MermaidIsla](https://avatars.githubusercontent.com/u/155835512?s=64 "MermaidIsla")](https://github.com/MermaidIsla) * [![air-hand](https://avatars.githubusercontent.com/u/45233435?s=64 "air-hand")](https://github.com/air-hand) * [![edmondshtogu](https://avatars.githubusercontent.com/u/10067009?s=64 "edmondshtogu")](https://github.com/edmondshtogu) * [![nanookclaw](https://avatars.githubusercontent.com/u/258741235?s=64 "nanookclaw")](https://github.com/nanookclaw) * [![paule96](https://avatars.githubusercontent.com/u/11291885?s=64 "paule96")](https://github.com/paule96) * [![rickylabs](https://avatars.githubusercontent.com/u/129366361?s=64 "rickylabs")](https://github.com/rickylabs) * [![illay1994](https://avatars.githubusercontent.com/u/10198202?s=64 "illay1994")](https://github.com/illay1994) * [![Evangelink](https://avatars.githubusercontent.com/u/11340282?s=64 "Evangelink")](https://github.com/Evangelink) * [![yreynhout](https://avatars.githubusercontent.com/u/142834?s=64 "yreynhout")](https://github.com/yreynhout) * [![liammclennan](https://avatars.githubusercontent.com/u/271514?s=64 "liammclennan")](https://github.com/liammclennan) * [![abdallahsellem](https://avatars.githubusercontent.com/u/77124997?s=64 "abdallahsellem")](https://github.com/abdallahsellem) * [![adamsitnik](https://avatars.githubusercontent.com/u/6011991?s=64 "adamsitnik")](https://github.com/adamsitnik) * [![adityamandaleeka](https://avatars.githubusercontent.com/u/219224?s=64 "adityamandaleeka")](https://github.com/adityamandaleeka) * [![AndriySvyryd](https://avatars.githubusercontent.com/u/6539701?s=64 "AndriySvyryd")](https://github.com/AndriySvyryd) * [![bwateratmsft](https://avatars.githubusercontent.com/u/36966225?s=64 "bwateratmsft")](https://github.com/bwateratmsft) * [![cmeyertons](https://avatars.githubusercontent.com/u/19917677?s=64 "cmeyertons")](https://github.com/cmeyertons) * [![cmdkeen](https://avatars.githubusercontent.com/u/54735?s=64 "cmdkeen")](https://github.com/cmdkeen) * [![chrisdcmoore](https://avatars.githubusercontent.com/u/5628773?s=64 "chrisdcmoore")](https://github.com/chrisdcmoore) * [![danespinosa](https://avatars.githubusercontent.com/u/30415120?s=64 "danespinosa")](https://github.com/danespinosa) * [![maraf](https://avatars.githubusercontent.com/u/10020471?s=64 "maraf")](https://github.com/maraf) * [![krubenok](https://avatars.githubusercontent.com/u/2529120?s=64 "krubenok")](https://github.com/krubenok) * [![julioct](https://avatars.githubusercontent.com/u/2374270?s=64 "julioct")](https://github.com/julioct) * [![jeffhandley](https://avatars.githubusercontent.com/u/1031940?s=64 "jeffhandley")](https://github.com/jeffhandley) * [![Dona278](https://avatars.githubusercontent.com/u/10567243?s=64 "Dona278")](https://github.com/Dona278) * [![ericmutta](https://avatars.githubusercontent.com/u/20465797?s=64 "ericmutta")](https://github.com/ericmutta) * [![Daluur](https://avatars.githubusercontent.com/u/6771251?s=64 "Daluur")](https://github.com/Daluur) * [![james-gould](https://avatars.githubusercontent.com/u/15638113?s=64 "james-gould")](https://github.com/james-gould) * [![guanzhousongmicrosoft](https://avatars.githubusercontent.com/u/85122868?s=64 "guanzhousongmicrosoft")](https://github.com/guanzhousongmicrosoft) * [![ndhansen](https://avatars.githubusercontent.com/u/12735464?s=64 "ndhansen")](https://github.com/ndhansen) * [![GryBsh](https://avatars.githubusercontent.com/u/120699894?s=64 "GryBsh")](https://github.com/GryBsh) * [![nightt5879](https://avatars.githubusercontent.com/u/87569709?s=64 "nightt5879")](https://github.com/nightt5879) * [![NinoFloris](https://avatars.githubusercontent.com/u/4218809?s=64 "NinoFloris")](https://github.com/NinoFloris) * [![pvanbuijtene](https://avatars.githubusercontent.com/u/7116354?s=64 "pvanbuijtene")](https://github.com/pvanbuijtene) * [![msftph](https://avatars.githubusercontent.com/u/65667413?s=64 "msftph")](https://github.com/msftph) * [![pdekkers](https://avatars.githubusercontent.com/u/11230135?s=64 "pdekkers")](https://github.com/pdekkers) * [![peterwald](https://avatars.githubusercontent.com/u/5578?s=64 "peterwald")](https://github.com/peterwald) * [![neoGeneva](https://avatars.githubusercontent.com/u/804724?s=64 "neoGeneva")](https://github.com/neoGeneva) * [![epsitec](https://avatars.githubusercontent.com/u/3872435?s=64 "epsitec")](https://github.com/epsitec) * [![pierrebelin](https://avatars.githubusercontent.com/u/25244392?s=64 "pierrebelin")](https://github.com/pierrebelin) * [![rdeveen](https://avatars.githubusercontent.com/u/5731724?s=64 "rdeveen")](https://github.com/rdeveen) * [![Arasz](https://avatars.githubusercontent.com/u/9105216?s=64 "Arasz")](https://github.com/Arasz) * [![romansp](https://avatars.githubusercontent.com/u/3474842?s=64 "romansp")](https://github.com/romansp) * [![richlander](https://avatars.githubusercontent.com/u/2608468?s=64 "richlander")](https://github.com/richlander) * [![Formatted](https://avatars.githubusercontent.com/u/14853553?s=64 "Formatted")](https://github.com/Formatted) * [![Jah-yee](https://avatars.githubusercontent.com/u/166608075?s=64 "Jah-yee")](https://github.com/Jah-yee) * [![r0ss88](https://avatars.githubusercontent.com/u/35737434?s=64 "r0ss88")](https://github.com/r0ss88) * [![RudyCo](https://avatars.githubusercontent.com/u/3536775?s=64 "RudyCo")](https://github.com/RudyCo) * [![rynowak](https://avatars.githubusercontent.com/u/1430011?s=64 "rynowak")](https://github.com/rynowak) * [![sayedihashimi](https://avatars.githubusercontent.com/u/1283154?s=64 "sayedihashimi")](https://github.com/sayedihashimi) * [![mangeg](https://avatars.githubusercontent.com/u/803458?s=64 "mangeg")](https://github.com/mangeg) * [![marcfreiheit](https://avatars.githubusercontent.com/u/27914201?s=64 "marcfreiheit")](https://github.com/marcfreiheit) * [![MarcinJuraszek](https://avatars.githubusercontent.com/u/6841973?s=64 "MarcinJuraszek")](https://github.com/MarcinJuraszek) * [![Marcus-Kanon](https://avatars.githubusercontent.com/u/89650029?s=64 "Marcus-Kanon")](https://github.com/Marcus-Kanon) * [![markphillips100](https://avatars.githubusercontent.com/u/6239029?s=64 "markphillips100")](https://github.com/markphillips100) * [![MO2k4](https://avatars.githubusercontent.com/u/453360?s=64 "MO2k4")](https://github.com/MO2k4) * [![heintz06](https://avatars.githubusercontent.com/u/53596000?s=64 "heintz06")](https://github.com/heintz06) * [![wicksipedia](https://avatars.githubusercontent.com/u/600044?s=64 "wicksipedia")](https://github.com/wicksipedia) * [![mattchenderson](https://avatars.githubusercontent.com/u/5815695?s=64 "mattchenderson")](https://github.com/mattchenderson) * [![MatthewSteeples](https://avatars.githubusercontent.com/u/255971?s=64 "MatthewSteeples")](https://github.com/MatthewSteeples) * [![mbwilding](https://avatars.githubusercontent.com/u/8856912?s=64 "mbwilding")](https://github.com/mbwilding) * [![mehara-rothila](https://avatars.githubusercontent.com/u/175220241?s=64 "mehara-rothila")](https://github.com/mehara-rothila) * [![mcumming](https://avatars.githubusercontent.com/u/20541227?s=64 "mcumming")](https://github.com/mcumming) * [![MichaelSimons](https://avatars.githubusercontent.com/u/8290530?s=64 "MichaelSimons")](https://github.com/MichaelSimons) * [![mikeharder](https://avatars.githubusercontent.com/u/9459391?s=64 "mikeharder")](https://github.com/mikeharder) * [![mu88](https://avatars.githubusercontent.com/u/4560672?s=64 "mu88")](https://github.com/mu88) * [![mitchcapper](https://avatars.githubusercontent.com/u/1643324?s=64 "mitchcapper")](https://github.com/mitchcapper) * [![GMouaad](https://avatars.githubusercontent.com/u/22234383?s=64 "GMouaad")](https://github.com/GMouaad) * [![Mrxx99](https://avatars.githubusercontent.com/u/33566379?s=64 "Mrxx99")](https://github.com/Mrxx99) * [![OptimusPi](https://avatars.githubusercontent.com/u/16853743?s=64 "OptimusPi")](https://github.com/OptimusPi) * [![foxminchan](https://avatars.githubusercontent.com/u/56079798?s=64 "foxminchan")](https://github.com/foxminchan) * [![Costo](https://avatars.githubusercontent.com/u/46972?s=64 "Costo")](https://github.com/Costo) * [![Vladipz](https://avatars.githubusercontent.com/u/120122292?s=64 "Vladipz")](https://github.com/Vladipz) * [![vladimir-shirmanov](https://avatars.githubusercontent.com/u/11080463?s=64 "vladimir-shirmanov")](https://github.com/vladimir-shirmanov) * [![Waleed-KH](https://avatars.githubusercontent.com/u/6625879?s=64 "Waleed-KH")](https://github.com/Waleed-KH) * [![wmeints](https://avatars.githubusercontent.com/u/1550763?s=64 "wmeints")](https://github.com/wmeints) * [![LittleLittleCloud](https://avatars.githubusercontent.com/u/16876986?s=64 "LittleLittleCloud")](https://github.com/LittleLittleCloud) * [![amrali21](https://avatars.githubusercontent.com/u/18468966?s=64 "amrali21")](https://github.com/amrali21) * [![andi0b](https://avatars.githubusercontent.com/u/2432095?s=64 "andi0b")](https://github.com/andi0b) * [![andrei-ungureanu-uipath](https://avatars.githubusercontent.com/u/61829128?s=64 "andrei-ungureanu-uipath")](https://github.com/andrei-ungureanu-uipath) * [![danikishin](https://avatars.githubusercontent.com/u/68384137?s=64 "danikishin")](https://github.com/danikishin) * [![devsko](https://avatars.githubusercontent.com/u/12471105?s=64 "devsko")](https://github.com/devsko) * [![duskembayev](https://avatars.githubusercontent.com/u/14294244?s=64 "duskembayev")](https://github.com/duskembayev) * [![eso-cyber](https://avatars.githubusercontent.com/u/245611701?s=64 "eso-cyber")](https://github.com/eso-cyber) * [![kola-tm](https://avatars.githubusercontent.com/u/48262102?s=64 "kola-tm")](https://github.com/kola-tm) * [![AkosLukacs](https://avatars.githubusercontent.com/u/844331?s=64 "AkosLukacs")](https://github.com/AkosLukacs) * [![mturac](https://avatars.githubusercontent.com/u/345446?s=64 "mturac")](https://github.com/mturac) * [![microsoft-github-operations\[bot\]](https://avatars.githubusercontent.com/u/55726097?s=64 "microsoft-github-operations\[bot\]")](https://github.com/microsoft-github-operations\[bot]) * [![ojamte](https://avatars.githubusercontent.com/u/1688780?s=64 "ojamte")](https://github.com/ojamte) * [![AdrianCanadasSC](https://avatars.githubusercontent.com/u/4287940?s=64 "AdrianCanadasSC")](https://github.com/AdrianCanadasSC) * [![sharpSteff](https://avatars.githubusercontent.com/u/30927510?s=64 "sharpSteff")](https://github.com/sharpSteff) * [![tg-msft](https://avatars.githubusercontent.com/u/1179329?s=64 "tg-msft")](https://github.com/tg-msft) * [![scottaddie](https://avatars.githubusercontent.com/u/10702007?s=64 "scottaddie")](https://github.com/scottaddie) * [![coolcsh](https://avatars.githubusercontent.com/u/816505?s=64 "coolcsh")](https://github.com/coolcsh) * [![McDonaldSean](https://avatars.githubusercontent.com/u/19911059?s=64 "McDonaldSean")](https://github.com/McDonaldSean) * [![Shaurya2k06](https://avatars.githubusercontent.com/u/104617579?s=64 "Shaurya2k06")](https://github.com/Shaurya2k06) * [![ShilpiRachna1](https://avatars.githubusercontent.com/u/163410222?s=64 "ShilpiRachna1")](https://github.com/ShilpiRachna1) * [![shivamgoel008](https://avatars.githubusercontent.com/u/55030452?s=64 "shivamgoel008")](https://github.com/shivamgoel008) * [![ShreyasJejurkar](https://avatars.githubusercontent.com/u/17148381?s=64 "ShreyasJejurkar")](https://github.com/ShreyasJejurkar) * [![stefannikolei](https://avatars.githubusercontent.com/u/873751?s=64 "stefannikolei")](https://github.com/stefannikolei) * [![sunandabalu](https://avatars.githubusercontent.com/u/16616736?s=64 "sunandabalu")](https://github.com/sunandabalu) * [![tskimmett](https://avatars.githubusercontent.com/u/4603206?s=64 "tskimmett")](https://github.com/tskimmett) * [![thimok](https://avatars.githubusercontent.com/u/20819818?s=64 "thimok")](https://github.com/thimok) * [![Thovenaar](https://avatars.githubusercontent.com/u/11218403?s=64 "Thovenaar")](https://github.com/Thovenaar) * [![ikkentim](https://avatars.githubusercontent.com/u/2820647?s=64 "ikkentim")](https://github.com/ikkentim) * [![timritzer](https://avatars.githubusercontent.com/u/7315207?s=64 "timritzer")](https://github.com/timritzer) * [![thomhurst](https://avatars.githubusercontent.com/u/30480171?s=64 "thomhurst")](https://github.com/thomhurst) * [![T-Gro](https://avatars.githubusercontent.com/u/46543583?s=64 "T-Gro")](https://github.com/T-Gro) * [![tmat](https://avatars.githubusercontent.com/u/41759?s=64 "tmat")](https://github.com/tmat) * [![tranhoangtu-it](https://avatars.githubusercontent.com/u/175955013?s=64 "tranhoangtu-it")](https://github.com/tranhoangtu-it) * [![vha-schleupen](https://avatars.githubusercontent.com/u/88094233?s=64 "vha-schleupen")](https://github.com/vha-schleupen) * [![Victor-johnsson](https://avatars.githubusercontent.com/u/70903378?s=64 "Victor-johnsson")](https://github.com/Victor-johnsson) * [![ViktorHofer](https://avatars.githubusercontent.com/u/7412651?s=64 "ViktorHofer")](https://github.com/ViktorHofer) * [![aaron-hardin](https://avatars.githubusercontent.com/u/3603910?s=64 "aaron-hardin")](https://github.com/aaron-hardin) * [![FullStackChef](https://avatars.githubusercontent.com/u/28607748?s=64 "FullStackChef")](https://github.com/FullStackChef) * [![buvinghausen](https://avatars.githubusercontent.com/u/1130210?s=64 "buvinghausen")](https://github.com/buvinghausen) * [![bdukes](https://avatars.githubusercontent.com/u/59507?s=64 "bdukes")](https://github.com/bdukes) * [![CaitieM20](https://avatars.githubusercontent.com/u/3310141?s=64 "CaitieM20")](https://github.com/CaitieM20) * [![ChaseKnowlden](https://avatars.githubusercontent.com/u/46290258?s=64 "ChaseKnowlden")](https://github.com/ChaseKnowlden) * [![ChinoUkaegbu](https://avatars.githubusercontent.com/u/77782533?s=64 "ChinoUkaegbu")](https://github.com/ChinoUkaegbu) * [![Tratcher](https://avatars.githubusercontent.com/u/1821173?s=64 "Tratcher")](https://github.com/Tratcher) * [![jesuszarate](https://avatars.githubusercontent.com/u/6497386?s=64 "jesuszarate")](https://github.com/jesuszarate) * [![colinwilliams91](https://avatars.githubusercontent.com/u/92059005?s=64 "colinwilliams91")](https://github.com/colinwilliams91) * [![cyrusdargahi](https://avatars.githubusercontent.com/u/20536256?s=64 "cyrusdargahi")](https://github.com/cyrusdargahi) * [![danfiedler-msft](https://avatars.githubusercontent.com/u/151573964?s=64 "danfiedler-msft")](https://github.com/danfiedler-msft) * [![djonser](https://avatars.githubusercontent.com/u/8218022?s=64 "djonser")](https://github.com/djonser) * [![danspark](https://avatars.githubusercontent.com/u/23037278?s=64 "danspark")](https://github.com/danspark) * [![dkattan](https://avatars.githubusercontent.com/u/1424395?s=64 "dkattan")](https://github.com/dkattan) * [![rsd-darshan](https://avatars.githubusercontent.com/u/183583132?s=64 "rsd-darshan")](https://github.com/rsd-darshan) * [![Odonno](https://avatars.githubusercontent.com/u/6053067?s=64 "Odonno")](https://github.com/Odonno) * [![flcdrg](https://avatars.githubusercontent.com/u/384747?s=64 "flcdrg")](https://github.com/flcdrg) * [![CyberDNS](https://avatars.githubusercontent.com/u/34795406?s=64 "CyberDNS")](https://github.com/CyberDNS) * [![DavidZidar](https://avatars.githubusercontent.com/u/381720?s=64 "DavidZidar")](https://github.com/DavidZidar) * [![ideepakchauhan7](https://avatars.githubusercontent.com/u/91007260?s=64 "ideepakchauhan7")](https://github.com/ideepakchauhan7) * [![denisivan0v](https://avatars.githubusercontent.com/u/5635171?s=64 "denisivan0v")](https://github.com/denisivan0v) * [![Aaronontheweb](https://avatars.githubusercontent.com/u/326939?s=64 "Aaronontheweb")](https://github.com/Aaronontheweb) * [![wahab-cide](https://avatars.githubusercontent.com/u/124523882?s=64 "wahab-cide")](https://github.com/wahab-cide) * [![los93sol](https://avatars.githubusercontent.com/u/6626120?s=64 "los93sol")](https://github.com/los93sol) * [![adelinowona](https://avatars.githubusercontent.com/u/51498470?s=64 "adelinowona")](https://github.com/adelinowona) * [![ailtonguitar](https://avatars.githubusercontent.com/u/7503689?s=64 "ailtonguitar")](https://github.com/ailtonguitar) * [![alexander-kucherov](https://avatars.githubusercontent.com/u/50913857?s=64 "alexander-kucherov")](https://github.com/alexander-kucherov) * [![HofmeisterAn](https://avatars.githubusercontent.com/u/9199345?s=64 "HofmeisterAn")](https://github.com/HofmeisterAn) * [![annatisch](https://avatars.githubusercontent.com/u/8689453?s=64 "annatisch")](https://github.com/annatisch) * [![amilochau](https://avatars.githubusercontent.com/u/48644548?s=64 "amilochau")](https://github.com/amilochau) * [![arpitjain099](https://avatars.githubusercontent.com/u/3242828?s=64 "arpitjain099")](https://github.com/arpitjain099) * [![Asafima](https://avatars.githubusercontent.com/u/100016552?s=64 "Asafima")](https://github.com/Asafima) * [![attilah](https://avatars.githubusercontent.com/u/230432?s=64 "attilah")](https://github.com/attilah) * [![bbartels](https://avatars.githubusercontent.com/u/23058572?s=64 "bbartels")](https://github.com/bbartels) * [![gansb](https://avatars.githubusercontent.com/u/1140050?s=64 "gansb")](https://github.com/gansb) * [![Falco20019](https://avatars.githubusercontent.com/u/940619?s=64 "Falco20019")](https://github.com/Falco20019) * [![benwitmanmsft](https://avatars.githubusercontent.com/u/160950603?s=64 "benwitmanmsft")](https://github.com/benwitmanmsft) * [![berkansasmaz](https://avatars.githubusercontent.com/u/31216880?s=64 "berkansasmaz")](https://github.com/berkansasmaz) * [![BillHiebert](https://avatars.githubusercontent.com/u/1553785?s=64 "BillHiebert")](https://github.com/BillHiebert) * [![Cethric](https://avatars.githubusercontent.com/u/7356786?s=64 "Cethric")](https://github.com/Cethric) * [![willibrandon](https://avatars.githubusercontent.com/u/5017479?s=64 "willibrandon")](https://github.com/willibrandon) * [![brettcannon](https://avatars.githubusercontent.com/u/54418?s=64 "brettcannon")](https://github.com/brettcannon) * [![dvoituron](https://avatars.githubusercontent.com/u/8350694?s=64 "dvoituron")](https://github.com/dvoituron) * [![JakeRadMSFT](https://avatars.githubusercontent.com/u/31937616?s=64 "JakeRadMSFT")](https://github.com/JakeRadMSFT) * [![nohwnd](https://avatars.githubusercontent.com/u/5735905?s=64 "nohwnd")](https://github.com/nohwnd) * [![licon4812](https://avatars.githubusercontent.com/u/32421608?s=64 "licon4812")](https://github.com/licon4812) * [![jgbright](https://avatars.githubusercontent.com/u/1843336?s=64 "jgbright")](https://github.com/jgbright) * [![jeffl8n](https://avatars.githubusercontent.com/u/160512?s=64 "jeffl8n")](https://github.com/jeffl8n) * [![jomaxso](https://avatars.githubusercontent.com/u/55972873?s=64 "jomaxso")](https://github.com/jomaxso) * [![flojon](https://avatars.githubusercontent.com/u/52005?s=64 "flojon")](https://github.com/flojon) * [![jnyrup](https://avatars.githubusercontent.com/u/919634?s=64 "jnyrup")](https://github.com/jnyrup) * [![jguadagno](https://avatars.githubusercontent.com/u/3209610?s=64 "jguadagno")](https://github.com/jguadagno) * [![JoshuaKGoldberg](https://avatars.githubusercontent.com/u/3335181?s=64 "JoshuaKGoldberg")](https://github.com/JoshuaKGoldberg) * [![holystix04](https://avatars.githubusercontent.com/u/8400967?s=64 "holystix04")](https://github.com/holystix04) * [![Steinblock](https://avatars.githubusercontent.com/u/6850324?s=64 "Steinblock")](https://github.com/Steinblock) * [![vcsjones](https://avatars.githubusercontent.com/u/361677?s=64 "vcsjones")](https://github.com/vcsjones) * [![kieronlanning](https://avatars.githubusercontent.com/u/5364423?s=64 "kieronlanning")](https://github.com/kieronlanning) * [![KirillOsenkov](https://avatars.githubusercontent.com/u/679326?s=64 "KirillOsenkov")](https://github.com/KirillOsenkov) * [![KuraiAndras](https://avatars.githubusercontent.com/u/19240945?s=64 "KuraiAndras")](https://github.com/KuraiAndras) * [![larsfjerm](https://avatars.githubusercontent.com/u/6827807?s=64 "larsfjerm")](https://github.com/larsfjerm) * [![flensrocker](https://avatars.githubusercontent.com/u/673769?s=64 "flensrocker")](https://github.com/flensrocker) * [![laurentkempe](https://avatars.githubusercontent.com/u/272612?s=64 "laurentkempe")](https://github.com/laurentkempe) * [![GrabYourPitchforks](https://avatars.githubusercontent.com/u/1746272?s=64 "GrabYourPitchforks")](https://github.com/GrabYourPitchforks) * [![levimatheri](https://avatars.githubusercontent.com/u/16405269?s=64 "levimatheri")](https://github.com/levimatheri) * [![dbreshears](https://avatars.githubusercontent.com/u/3432571?s=64 "dbreshears")](https://github.com/dbreshears) * [![divyeshio](https://avatars.githubusercontent.com/u/79130336?s=64 "divyeshio")](https://github.com/divyeshio) * [![aelij](https://avatars.githubusercontent.com/u/496737?s=64 "aelij")](https://github.com/aelij) * [![emilienbev](https://avatars.githubusercontent.com/u/44171454?s=64 "emilienbev")](https://github.com/emilienbev) * [![EmmittJ](https://avatars.githubusercontent.com/u/7478125?s=64 "EmmittJ")](https://github.com/EmmittJ) * [![ericsuh](https://avatars.githubusercontent.com/u/382805?s=64 "ericsuh")](https://github.com/ericsuh) * [![ericstj](https://avatars.githubusercontent.com/u/8918108?s=64 "ericstj")](https://github.com/ericstj) * [![onionhammer](https://avatars.githubusercontent.com/u/969938?s=64 "onionhammer")](https://github.com/onionhammer) * [![Bertolossi](https://avatars.githubusercontent.com/u/3521936?s=64 "Bertolossi")](https://github.com/Bertolossi) * [![fabiocav](https://avatars.githubusercontent.com/u/2507935?s=64 "fabiocav")](https://github.com/fabiocav) * [![jeremy-vm](https://avatars.githubusercontent.com/u/42162085?s=64 "jeremy-vm")](https://github.com/jeremy-vm) * [![frankbuckley](https://avatars.githubusercontent.com/u/5655810?s=64 "frankbuckley")](https://github.com/frankbuckley) * [![GeertvanHorrik](https://avatars.githubusercontent.com/u/1246444?s=64 "GeertvanHorrik")](https://github.com/GeertvanHorrik) * [![glennc](https://avatars.githubusercontent.com/u/234688?s=64 "glennc")](https://github.com/glennc) * [![Depechie](https://avatars.githubusercontent.com/u/351693?s=64 "Depechie")](https://github.com/Depechie) * [![gkulin](https://avatars.githubusercontent.com/u/55554236?s=64 "gkulin")](https://github.com/gkulin) * [![inlineHamed](https://avatars.githubusercontent.com/u/11094468?s=64 "inlineHamed")](https://github.com/inlineHamed) * [![hansmbakker](https://avatars.githubusercontent.com/u/3463496?s=64 "hansmbakker")](https://github.com/hansmbakker) * [![0xharkirat](https://avatars.githubusercontent.com/u/65155920?s=64 "0xharkirat")](https://github.com/0xharkirat) * [![eltociear](https://avatars.githubusercontent.com/u/22633385?s=64 "eltociear")](https://github.com/eltociear) * [![Banovvv](https://avatars.githubusercontent.com/u/44908454?s=64 "Banovvv")](https://github.com/Banovvv) [microsoft/aspire](https://github.com/microsoft/aspire) [Stars 6,298](https://github.com/microsoft/aspire) [MIT License](https://github.com/microsoft/aspire/blob/main/LICENSE.TXT) ## 💜 Aspire samples contributors [Section titled “💜 Aspire samples contributors”](#-aspire-samples-contributors) Thank you to all the community members who have contributed to Aspire Samples! Your contributions help showcase the power and flexibility of Aspire in real-world scenarios. By sharing your solutions and examples, you empower others to learn, experiment, and build with confidence. Every sample you contribute can inspire new ideas and help fellow developers overcome challenges. Join us in collaborating and contributing—your real-world scenarios make a difference and help the entire community grow! * [![DamianEdwards](https://avatars.githubusercontent.com/u/249088?s=64 "DamianEdwards")](https://github.com/DamianEdwards) * [![IEvangelist](https://avatars.githubusercontent.com/u/7679720?s=64 "IEvangelist")](https://github.com/IEvangelist) * [![JamesNK](https://avatars.githubusercontent.com/u/303201?s=64 "JamesNK")](https://github.com/JamesNK) * [![joperezr](https://avatars.githubusercontent.com/u/13854455?s=64 "joperezr")](https://github.com/joperezr) * [![davidfowl](https://avatars.githubusercontent.com/u/95136?s=64 "davidfowl")](https://github.com/davidfowl) * [![eerhardt](https://avatars.githubusercontent.com/u/8291187?s=64 "eerhardt")](https://github.com/eerhardt) * [![sebastienros](https://avatars.githubusercontent.com/u/1165805?s=64 "sebastienros")](https://github.com/sebastienros) * [![bradygaster](https://avatars.githubusercontent.com/u/41929050?s=64 "bradygaster")](https://github.com/bradygaster) * [![jeffhandley](https://avatars.githubusercontent.com/u/1031940?s=64 "jeffhandley")](https://github.com/jeffhandley) * [![mitchdenny](https://avatars.githubusercontent.com/u/513398?s=64 "mitchdenny")](https://github.com/mitchdenny) * [![wtgodbe](https://avatars.githubusercontent.com/u/14283640?s=64 "wtgodbe")](https://github.com/wtgodbe) * [![AndriySvyryd](https://avatars.githubusercontent.com/u/6539701?s=64 "AndriySvyryd")](https://github.com/AndriySvyryd) * [![radical](https://avatars.githubusercontent.com/u/1472?s=64 "radical")](https://github.com/radical) * [![antonfirsov](https://avatars.githubusercontent.com/u/6835152?s=64 "antonfirsov")](https://github.com/antonfirsov) * [![asilverman](https://avatars.githubusercontent.com/u/9611108?s=64 "asilverman")](https://github.com/asilverman) * [![balachir](https://avatars.githubusercontent.com/u/8246794?s=64 "balachir")](https://github.com/balachir) * [![danfiedler-msft](https://avatars.githubusercontent.com/u/151573964?s=64 "danfiedler-msft")](https://github.com/danfiedler-msft) * [![Depechie](https://avatars.githubusercontent.com/u/351693?s=64 "Depechie")](https://github.com/Depechie) * [![hishamco](https://avatars.githubusercontent.com/u/3237266?s=64 "hishamco")](https://github.com/hishamco) * [![jongalloway](https://avatars.githubusercontent.com/u/68539?s=64 "jongalloway")](https://github.com/jongalloway) * [![Layla-P](https://avatars.githubusercontent.com/u/15874598?s=64 "Layla-P")](https://github.com/Layla-P) * [![prisecano](https://avatars.githubusercontent.com/u/53272907?s=64 "prisecano")](https://github.com/prisecano) * [![mmitche](https://avatars.githubusercontent.com/u/8725170?s=64 "mmitche")](https://github.com/mmitche) * [![michaelto20](https://avatars.githubusercontent.com/u/4887488?s=64 "michaelto20")](https://github.com/michaelto20) * [![ReubenBond](https://avatars.githubusercontent.com/u/203839?s=64 "ReubenBond")](https://github.com/ReubenBond) * [![Rick-Anderson](https://avatars.githubusercontent.com/u/3605364?s=64 "Rick-Anderson")](https://github.com/Rick-Anderson) * [![captainsafia](https://avatars.githubusercontent.com/u/1857993?s=64 "captainsafia")](https://github.com/captainsafia) * [![sammychinedu2ky](https://avatars.githubusercontent.com/u/36219292?s=64 "sammychinedu2ky")](https://github.com/sammychinedu2ky) * [![sayedihashimi](https://avatars.githubusercontent.com/u/1283154?s=64 "sayedihashimi")](https://github.com/sayedihashimi) * [![sgbj](https://avatars.githubusercontent.com/u/5178445?s=64 "sgbj")](https://github.com/sgbj) * [![SimonCropp](https://avatars.githubusercontent.com/u/122666?s=64 "SimonCropp")](https://github.com/SimonCropp) * [![sliekens](https://avatars.githubusercontent.com/u/1583241?s=64 "sliekens")](https://github.com/sliekens) * [![timdeschryver](https://avatars.githubusercontent.com/u/28659384?s=64 "timdeschryver")](https://github.com/timdeschryver) * [![vishipayyallore](https://avatars.githubusercontent.com/u/8255269?s=64 "vishipayyallore")](https://github.com/vishipayyallore) * [![alexwolfmsft](https://avatars.githubusercontent.com/u/93200798?s=64 "alexwolfmsft")](https://github.com/alexwolfmsft) * [![microsoft-github-operations\[bot\]](https://avatars.githubusercontent.com/u/55726097?s=64 "microsoft-github-operations\[bot\]")](https://github.com/microsoft-github-operations\[bot]) * [![moljac](https://avatars.githubusercontent.com/u/1768576?s=64 "moljac")](https://github.com/moljac) [microsoft/aspire-samples](https://github.com/microsoft/aspire-samples) [Stars 1,190](https://github.com/microsoft/aspire-samples) [MIT License](https://github.com/microsoft/aspire-samples/blob/main/LICENSE) ## 🧰 Aspire Community Toolkit contributors [Section titled “🧰 Aspire Community Toolkit contributors”](#-aspire-community-toolkit-contributors) For the community, by the community. The Aspire Community Toolkit grows through passionate contributors building extensions and enhancements for the ecosystem. Your contributions shape Aspire’s future and empower developers worldwide. Join the journey and help craft a toolkit reflecting diverse needs and our shared innovative spirit. * [![aaronpowell](https://avatars.githubusercontent.com/u/434140?s=64 "aaronpowell")](https://github.com/aaronpowell) * [![IEvangelist](https://avatars.githubusercontent.com/u/7679720?s=64 "IEvangelist")](https://github.com/IEvangelist) * [![tommasodotNET](https://avatars.githubusercontent.com/u/12819039?s=64 "tommasodotNET")](https://github.com/tommasodotNET) * [![Alirexaa](https://avatars.githubusercontent.com/u/70141416?s=64 "Alirexaa")](https://github.com/Alirexaa) * [![afscrome](https://avatars.githubusercontent.com/u/289860?s=64 "afscrome")](https://github.com/afscrome) * [![Odonno](https://avatars.githubusercontent.com/u/6053067?s=64 "Odonno")](https://github.com/Odonno) * [![fboucher](https://avatars.githubusercontent.com/u/2404846?s=64 "fboucher")](https://github.com/fboucher) * [![brian-guerrero](https://avatars.githubusercontent.com/u/5503800?s=64 "brian-guerrero")](https://github.com/brian-guerrero) * [![marshalhayes](https://avatars.githubusercontent.com/u/17213165?s=64 "marshalhayes")](https://github.com/marshalhayes) * [![gabynevada](https://avatars.githubusercontent.com/u/20828017?s=64 "gabynevada")](https://github.com/gabynevada) * [![ErikEJ](https://avatars.githubusercontent.com/u/4169187?s=64 "ErikEJ")](https://github.com/ErikEJ) * [![anoordover](https://avatars.githubusercontent.com/u/5289365?s=64 "anoordover")](https://github.com/anoordover) * [![dealloc](https://avatars.githubusercontent.com/u/2164354?s=64 "dealloc")](https://github.com/dealloc) * [![justinyoo](https://avatars.githubusercontent.com/u/1538528?s=64 "justinyoo")](https://github.com/justinyoo) * [![QuantumNightmare](https://avatars.githubusercontent.com/u/3094648?s=64 "QuantumNightmare")](https://github.com/QuantumNightmare) * [![FullStackChef](https://avatars.githubusercontent.com/u/28607748?s=64 "FullStackChef")](https://github.com/FullStackChef) * [![askpt](https://avatars.githubusercontent.com/u/2493377?s=64 "askpt")](https://github.com/askpt) * [![krubenok](https://avatars.githubusercontent.com/u/2529120?s=64 "krubenok")](https://github.com/krubenok) * [![thomhurst](https://avatars.githubusercontent.com/u/30480171?s=64 "thomhurst")](https://github.com/thomhurst) * [![fabio-marini](https://avatars.githubusercontent.com/u/10209472?s=64 "fabio-marini")](https://github.com/fabio-marini) * [![karl-sjogren](https://avatars.githubusercontent.com/u/875092?s=64 "karl-sjogren")](https://github.com/karl-sjogren) * [![jmezach](https://avatars.githubusercontent.com/u/1225489?s=64 "jmezach")](https://github.com/jmezach) * [![axies20](https://avatars.githubusercontent.com/u/19223232?s=64 "axies20")](https://github.com/axies20) * [![JerryNixon](https://avatars.githubusercontent.com/u/1749983?s=64 "JerryNixon")](https://github.com/JerryNixon) * [![tamirdresher](https://avatars.githubusercontent.com/u/342800?s=64 "tamirdresher")](https://github.com/tamirdresher) * [![Scooletz](https://avatars.githubusercontent.com/u/519707?s=64 "Scooletz")](https://github.com/Scooletz) * [![oising](https://avatars.githubusercontent.com/u/1844001?s=64 "oising")](https://github.com/oising) * [![DavidGarton8](https://avatars.githubusercontent.com/u/5149277?s=64 "DavidGarton8")](https://github.com/DavidGarton8) * [![MackinnonBuck](https://avatars.githubusercontent.com/u/10456961?s=64 "MackinnonBuck")](https://github.com/MackinnonBuck) * [![martinjt](https://avatars.githubusercontent.com/u/1699587?s=64 "martinjt")](https://github.com/martinjt) * [![timheuer](https://avatars.githubusercontent.com/u/4821?s=64 "timheuer")](https://github.com/timheuer) * [![sebastienros](https://avatars.githubusercontent.com/u/1165805?s=64 "sebastienros")](https://github.com/sebastienros) * [![Omnideth](https://avatars.githubusercontent.com/u/3496652?s=64 "Omnideth")](https://github.com/Omnideth) * [![maddymontaquila](https://avatars.githubusercontent.com/u/12660687?s=64 "maddymontaquila")](https://github.com/maddymontaquila) * [![poissoncorp](https://avatars.githubusercontent.com/u/25389585?s=64 "poissoncorp")](https://github.com/poissoncorp) * [![TheBlueSky](https://avatars.githubusercontent.com/u/807685?s=64 "TheBlueSky")](https://github.com/TheBlueSky) * [![Harold-Morgan](https://avatars.githubusercontent.com/u/6255074?s=64 "Harold-Morgan")](https://github.com/Harold-Morgan) * [![Chicoo](https://avatars.githubusercontent.com/u/566365?s=64 "Chicoo")](https://github.com/Chicoo) * [![sliekens](https://avatars.githubusercontent.com/u/1583241?s=64 "sliekens")](https://github.com/sliekens) * [![shiranshalom](https://avatars.githubusercontent.com/u/46426884?s=64 "shiranshalom")](https://github.com/shiranshalom) * [![edmondshtogu](https://avatars.githubusercontent.com/u/10067009?s=64 "edmondshtogu")](https://github.com/edmondshtogu) * [![paulomorgado](https://avatars.githubusercontent.com/u/470455?s=64 "paulomorgado")](https://github.com/paulomorgado) * [![foxminchan](https://avatars.githubusercontent.com/u/56079798?s=64 "foxminchan")](https://github.com/foxminchan) * [![TechWatching](https://avatars.githubusercontent.com/u/15186176?s=64 "TechWatching")](https://github.com/TechWatching) * [![prom3theu5](https://avatars.githubusercontent.com/u/1518610?s=64 "prom3theu5")](https://github.com/prom3theu5) * [![eerhardt](https://avatars.githubusercontent.com/u/8291187?s=64 "eerhardt")](https://github.com/eerhardt) * [![fredimachado](https://avatars.githubusercontent.com/u/29800?s=64 "fredimachado")](https://github.com/fredimachado) * [![JamesNK](https://avatars.githubusercontent.com/u/303201?s=64 "JamesNK")](https://github.com/JamesNK) * [![kristremblay](https://avatars.githubusercontent.com/u/7587183?s=64 "kristremblay")](https://github.com/kristremblay) * [![MatsM16](https://avatars.githubusercontent.com/u/17270481?s=64 "MatsM16")](https://github.com/MatsM16) * [![lqdev](https://avatars.githubusercontent.com/u/11130940?s=64 "lqdev")](https://github.com/lqdev) * [![BickelLukas](https://avatars.githubusercontent.com/u/16354178?s=64 "BickelLukas")](https://github.com/BickelLukas) * [![martincostello](https://avatars.githubusercontent.com/u/1439341?s=64 "martincostello")](https://github.com/martincostello) * [![mfcollins3](https://avatars.githubusercontent.com/u/104274?s=64 "mfcollins3")](https://github.com/mfcollins3) * [![Mrxx99](https://avatars.githubusercontent.com/u/33566379?s=64 "Mrxx99")](https://github.com/Mrxx99) * [![r4hulp](https://avatars.githubusercontent.com/u/292704?s=64 "r4hulp")](https://github.com/r4hulp) * [![ekomsctr](https://avatars.githubusercontent.com/u/13435555?s=64 "ekomsctr")](https://github.com/ekomsctr) * [![RubenPX](https://avatars.githubusercontent.com/u/23123160?s=64 "RubenPX")](https://github.com/RubenPX) * [![sschutten](https://avatars.githubusercontent.com/u/10097564?s=64 "sschutten")](https://github.com/sschutten) * [![esskar](https://avatars.githubusercontent.com/u/65206?s=64 "esskar")](https://github.com/esskar) * [![stephentoub](https://avatars.githubusercontent.com/u/2642209?s=64 "stephentoub")](https://github.com/stephentoub) * [![slang25](https://avatars.githubusercontent.com/u/1341446?s=64 "slang25")](https://github.com/slang25) * [![TimHess](https://avatars.githubusercontent.com/u/3947063?s=64 "TimHess")](https://github.com/TimHess) * [![konnta0](https://avatars.githubusercontent.com/u/68390856?s=64 "konnta0")](https://github.com/konnta0) * [![lukedukeus](https://avatars.githubusercontent.com/u/23089287?s=64 "lukedukeus")](https://github.com/lukedukeus) * [![lvde0](https://avatars.githubusercontent.com/u/163427032?s=64 "lvde0")](https://github.com/lvde0) * [![MichielBrys](https://avatars.githubusercontent.com/u/94305767?s=64 "MichielBrys")](https://github.com/MichielBrys) * [![andrey-noskov](https://avatars.githubusercontent.com/u/25082814?s=64 "andrey-noskov")](https://github.com/andrey-noskov) * [![cdschneider](https://avatars.githubusercontent.com/u/5581662?s=64 "cdschneider")](https://github.com/cdschneider) * [![Catalin-Andronie](https://avatars.githubusercontent.com/u/10738038?s=64 "Catalin-Andronie")](https://github.com/Catalin-Andronie) * [![programmation](https://avatars.githubusercontent.com/u/10096185?s=64 "programmation")](https://github.com/programmation) * [![davidfowl](https://avatars.githubusercontent.com/u/95136?s=64 "davidfowl")](https://github.com/davidfowl) * [![dluc](https://avatars.githubusercontent.com/u/371009?s=64 "dluc")](https://github.com/dluc) * [![almostchristian](https://avatars.githubusercontent.com/u/2035340?s=64 "almostchristian")](https://github.com/almostchristian) * [![esond](https://avatars.githubusercontent.com/u/4650644?s=64 "esond")](https://github.com/esond) * [![jfversluis](https://avatars.githubusercontent.com/u/939291?s=64 "jfversluis")](https://github.com/jfversluis) * [![gitbutler-client](https://avatars.githubusercontent.com/u/132921372?s=64 "gitbutler-client")](https://github.com/gitbutler-client) * [![henrikroschmann](https://avatars.githubusercontent.com/u/17333?s=64 "henrikroschmann")](https://github.com/henrikroschmann) * [![IgorShaposhnikov](https://avatars.githubusercontent.com/u/114292165?s=64 "IgorShaposhnikov")](https://github.com/IgorShaposhnikov) * [![josemalm32](https://avatars.githubusercontent.com/u/23101537?s=64 "josemalm32")](https://github.com/josemalm32) * [![nnitkasw](https://avatars.githubusercontent.com/u/126857618?s=64 "nnitkasw")](https://github.com/nnitkasw) * [![larsfjerm](https://avatars.githubusercontent.com/u/6827807?s=64 "larsfjerm")](https://github.com/larsfjerm) * [![lassem-eq](https://avatars.githubusercontent.com/u/269221678?s=64 "lassem-eq")](https://github.com/lassem-eq) * [![lvmajor](https://avatars.githubusercontent.com/u/1885400?s=64 "lvmajor")](https://github.com/lvmajor) * [![Stertz](https://avatars.githubusercontent.com/u/105670456?s=64 "Stertz")](https://github.com/Stertz) [CommunityToolkit/Aspire](https://github.com/CommunityToolkit/Aspire) [Stars 627](https://github.com/CommunityToolkit/Aspire) [MIT License](https://github.com/CommunityToolkit/Aspire/blob/main/LICENSE) ## 🌐 aspire.dev contributors [Section titled “🌐 aspire.dev contributors”](#-aspiredev-contributors) Want to help [improve the aspire.dev site?](/community/contributor-guide/) We’re always looking for contributors to help enhance our docs, fix typos, and add new content. Your contributions make it easier for everyone to learn and use Aspire effectively. Join us in making aspire.dev the best resource it can be! * [![IEvangelist](https://avatars.githubusercontent.com/u/7679720?s=64 "IEvangelist")](https://github.com/IEvangelist) * [![aspire-repo-bot\[bot\]](https://avatars.githubusercontent.com/u/268009190?s=64 "aspire-repo-bot\[bot\]")](https://github.com/aspire-repo-bot\[bot]) * [![davidfowl](https://avatars.githubusercontent.com/u/95136?s=64 "davidfowl")](https://github.com/davidfowl) * [![takashiuesaka](https://avatars.githubusercontent.com/u/61622933?s=64 "takashiuesaka")](https://github.com/takashiuesaka) * [![eerhardt](https://avatars.githubusercontent.com/u/8291187?s=64 "eerhardt")](https://github.com/eerhardt) * [![JamesNK](https://avatars.githubusercontent.com/u/303201?s=64 "JamesNK")](https://github.com/JamesNK) * [![sebastienros](https://avatars.githubusercontent.com/u/1165805?s=64 "sebastienros")](https://github.com/sebastienros) * [![BethMassi](https://avatars.githubusercontent.com/u/5115571?s=64 "BethMassi")](https://github.com/BethMassi) * [![maddymontaquila](https://avatars.githubusercontent.com/u/12660687?s=64 "maddymontaquila")](https://github.com/maddymontaquila) * [![danegsta](https://avatars.githubusercontent.com/u/50252651?s=64 "danegsta")](https://github.com/danegsta) * [![mitchdenny](https://avatars.githubusercontent.com/u/513398?s=64 "mitchdenny")](https://github.com/mitchdenny) * [![adamint](https://avatars.githubusercontent.com/u/20359921?s=64 "adamint")](https://github.com/adamint) * [![alistairmatthews](https://avatars.githubusercontent.com/u/41286777?s=64 "alistairmatthews")](https://github.com/alistairmatthews) * [![joperezr](https://avatars.githubusercontent.com/u/13854455?s=64 "joperezr")](https://github.com/joperezr) * [![jasontaylordev](https://avatars.githubusercontent.com/u/1988321?s=64 "jasontaylordev")](https://github.com/jasontaylordev) * [![Odonno](https://avatars.githubusercontent.com/u/6053067?s=64 "Odonno")](https://github.com/Odonno) * [![DamianEdwards](https://avatars.githubusercontent.com/u/249088?s=64 "DamianEdwards")](https://github.com/DamianEdwards) * [![jfversluis](https://avatars.githubusercontent.com/u/939291?s=64 "jfversluis")](https://github.com/jfversluis) * [![captainsafia](https://avatars.githubusercontent.com/u/1857993?s=64 "captainsafia")](https://github.com/captainsafia) * [![edmondshtogu](https://avatars.githubusercontent.com/u/10067009?s=64 "edmondshtogu")](https://github.com/edmondshtogu) * [![Victor-johnsson](https://avatars.githubusercontent.com/u/70903378?s=64 "Victor-johnsson")](https://github.com/Victor-johnsson) * [![timdeschryver](https://avatars.githubusercontent.com/u/28659384?s=64 "timdeschryver")](https://github.com/timdeschryver) * [![Omnideth](https://avatars.githubusercontent.com/u/3496652?s=64 "Omnideth")](https://github.com/Omnideth) * [![matt-goldman](https://avatars.githubusercontent.com/u/19944129?s=64 "matt-goldman")](https://github.com/matt-goldman) * [![haugis-git](https://avatars.githubusercontent.com/u/55443722?s=64 "haugis-git")](https://github.com/haugis-git) * [![GeertvanHorrik](https://avatars.githubusercontent.com/u/1246444?s=64 "GeertvanHorrik")](https://github.com/GeertvanHorrik) * [![Cameron-McBroom](https://avatars.githubusercontent.com/u/45535570?s=64 "Cameron-McBroom")](https://github.com/Cameron-McBroom) * [![agriffard](https://avatars.githubusercontent.com/u/703248?s=64 "agriffard")](https://github.com/agriffard) * [![aaronpowell](https://avatars.githubusercontent.com/u/434140?s=64 "aaronpowell")](https://github.com/aaronpowell) * [![thomhurst](https://avatars.githubusercontent.com/u/30480171?s=64 "thomhurst")](https://github.com/thomhurst) * [![teo-tsirpanis](https://avatars.githubusercontent.com/u/12659251?s=64 "teo-tsirpanis")](https://github.com/teo-tsirpanis) * [![slang25](https://avatars.githubusercontent.com/u/1341446?s=64 "slang25")](https://github.com/slang25) * [![rocklau](https://avatars.githubusercontent.com/u/221825?s=64 "rocklau")](https://github.com/rocklau) * [![patrickklaeren](https://avatars.githubusercontent.com/u/1341180?s=64 "patrickklaeren")](https://github.com/patrickklaeren) * [![foxminchan](https://avatars.githubusercontent.com/u/56079798?s=64 "foxminchan")](https://github.com/foxminchan) * [![mohsin-mehmood](https://avatars.githubusercontent.com/u/7757162?s=64 "mohsin-mehmood")](https://github.com/mohsin-mehmood) * [![Meir017](https://avatars.githubusercontent.com/u/9786571?s=64 "Meir017")](https://github.com/Meir017) * [![MatsM16](https://avatars.githubusercontent.com/u/17270481?s=64 "MatsM16")](https://github.com/MatsM16) * [![maraf](https://avatars.githubusercontent.com/u/10020471?s=64 "maraf")](https://github.com/maraf) * [![butskristof](https://avatars.githubusercontent.com/u/4030759?s=64 "butskristof")](https://github.com/butskristof) * [![afscrome](https://avatars.githubusercontent.com/u/289860?s=64 "afscrome")](https://github.com/afscrome) * [![Tri125](https://avatars.githubusercontent.com/u/2048645?s=64 "Tri125")](https://github.com/Tri125) * [![vsantele](https://avatars.githubusercontent.com/u/26800140?s=64 "vsantele")](https://github.com/vsantele) * [![Waleed-KH](https://avatars.githubusercontent.com/u/6625879?s=64 "Waleed-KH")](https://github.com/Waleed-KH) * [![WeihanLi](https://avatars.githubusercontent.com/u/7604648?s=64 "WeihanLi")](https://github.com/WeihanLi) * [![zprobinson](https://avatars.githubusercontent.com/u/59841145?s=64 "zprobinson")](https://github.com/zprobinson) * [![ZieMcd](https://avatars.githubusercontent.com/u/59741700?s=64 "ZieMcd")](https://github.com/ZieMcd) * [![huangkevin-apr](https://avatars.githubusercontent.com/u/182325027?s=64 "huangkevin-apr")](https://github.com/huangkevin-apr) * [![lim-dy](https://avatars.githubusercontent.com/u/225886905?s=64 "lim-dy")](https://github.com/lim-dy) * [![philipp985](https://avatars.githubusercontent.com/u/11349081?s=64 "philipp985")](https://github.com/philipp985) * [![suugbut](https://avatars.githubusercontent.com/u/153189317?s=64 "suugbut")](https://github.com/suugbut) * [![BOBx5](https://avatars.githubusercontent.com/u/55046528?s=64 "BOBx5")](https://github.com/BOBx5) * [![alexravenna](https://avatars.githubusercontent.com/u/29532881?s=64 "alexravenna")](https://github.com/alexravenna) * [![alex-clickhouse](https://avatars.githubusercontent.com/u/237136924?s=64 "alex-clickhouse")](https://github.com/alex-clickhouse) * [![askpt](https://avatars.githubusercontent.com/u/2493377?s=64 "askpt")](https://github.com/askpt) * [![angelobelchior](https://avatars.githubusercontent.com/u/4245518?s=64 "angelobelchior")](https://github.com/angelobelchior) * [![meijeran](https://avatars.githubusercontent.com/u/13779871?s=64 "meijeran")](https://github.com/meijeran) * [![lvb2104](https://avatars.githubusercontent.com/u/264902880?s=64 "lvb2104")](https://github.com/lvb2104) * [![BoyanYK](https://avatars.githubusercontent.com/u/28277932?s=64 "BoyanYK")](https://github.com/BoyanYK) * [![brunoborges](https://avatars.githubusercontent.com/u/129743?s=64 "brunoborges")](https://github.com/brunoborges) * [![dracan](https://avatars.githubusercontent.com/u/567988?s=64 "dracan")](https://github.com/dracan) * [![danfiedler-msft](https://avatars.githubusercontent.com/u/151573964?s=64 "danfiedler-msft")](https://github.com/danfiedler-msft) * [![Webmekanic](https://avatars.githubusercontent.com/u/81490414?s=64 "Webmekanic")](https://github.com/Webmekanic) * [![dkroderos](https://avatars.githubusercontent.com/u/75028710?s=64 "dkroderos")](https://github.com/dkroderos) * [![ellahathaway](https://avatars.githubusercontent.com/u/67609881?s=64 "ellahathaway")](https://github.com/ellahathaway) * [![ErikEJ](https://avatars.githubusercontent.com/u/4169187?s=64 "ErikEJ")](https://github.com/ErikEJ) * [![idogit123](https://avatars.githubusercontent.com/u/90967400?s=64 "idogit123")](https://github.com/idogit123) * [![Zylvian](https://avatars.githubusercontent.com/u/33404765?s=64 "Zylvian")](https://github.com/Zylvian) * [![vyrotek](https://avatars.githubusercontent.com/u/518436?s=64 "vyrotek")](https://github.com/vyrotek) * [![javiercn](https://avatars.githubusercontent.com/u/6995051?s=64 "javiercn")](https://github.com/javiercn) * [![Jeffreyyvdb](https://avatars.githubusercontent.com/u/60582071?s=64 "Jeffreyyvdb")](https://github.com/Jeffreyyvdb) * [![karolz-ms](https://avatars.githubusercontent.com/u/15271049?s=64 "karolz-ms")](https://github.com/karolz-ms) * [![kattschan](https://avatars.githubusercontent.com/u/111166669?s=64 "kattschan")](https://github.com/kattschan) * [![kijanawoodard](https://avatars.githubusercontent.com/u/152013?s=64 "kijanawoodard")](https://github.com/kijanawoodard) [microsoft/aspire.dev](https://github.com/microsoft/aspire.dev) [Stars 191](https://github.com/microsoft/aspire.dev) [MIT License](https://github.com/microsoft/aspire.dev/blob/main/LICENSE) ## ⚙️ DCP contributors [Section titled “⚙️ DCP contributors”](#️-dcp-contributors) The [Distributed Component Platform (DCP)](/architecture/overview/#developer-control-plane) is an open-source project that provides a framework for building distributed applications. It is designed to simplify the development of microservices and serverless applications by providing a set of tools and libraries that make it easy to create, deploy, and manage distributed components. * [![karolz-ms](https://avatars.githubusercontent.com/u/15271049?s=64 "karolz-ms")](https://github.com/karolz-ms) * [![danegsta](https://avatars.githubusercontent.com/u/50252651?s=64 "danegsta")](https://github.com/danegsta) * [![bwateratmsft](https://avatars.githubusercontent.com/u/36966225?s=64 "bwateratmsft")](https://github.com/bwateratmsft) * [![dbreshears](https://avatars.githubusercontent.com/u/3432571?s=64 "dbreshears")](https://github.com/dbreshears) * [![RussKie](https://avatars.githubusercontent.com/u/4403806?s=64 "RussKie")](https://github.com/RussKie) * [![timheuer](https://avatars.githubusercontent.com/u/4821?s=64 "timheuer")](https://github.com/timheuer) * [![Arjunmehta312](https://avatars.githubusercontent.com/u/138153442?s=64 "Arjunmehta312")](https://github.com/Arjunmehta312) * [![danfiedler-msft](https://avatars.githubusercontent.com/u/151573964?s=64 "danfiedler-msft")](https://github.com/danfiedler-msft) * [![davidfowl](https://avatars.githubusercontent.com/u/95136?s=64 "davidfowl")](https://github.com/davidfowl) * [![Jal-Bafana](https://avatars.githubusercontent.com/u/193942704?s=64 "Jal-Bafana")](https://github.com/Jal-Bafana) * [![ellismg](https://avatars.githubusercontent.com/u/9602953?s=64 "ellismg")](https://github.com/ellismg) * [![missymessa](https://avatars.githubusercontent.com/u/47990216?s=64 "missymessa")](https://github.com/missymessa) * [![haliaeetusvocifer](https://avatars.githubusercontent.com/u/20953018?s=64 "haliaeetusvocifer")](https://github.com/haliaeetusvocifer) * [![Jah-yee](https://avatars.githubusercontent.com/u/166608075?s=64 "Jah-yee")](https://github.com/Jah-yee) * [![McDonaldSean](https://avatars.githubusercontent.com/u/19911059?s=64 "McDonaldSean")](https://github.com/McDonaldSean) * [![microsoft-github-policy-service\[bot\]](https://avatars.githubusercontent.com/u/77245923?s=64 "microsoft-github-policy-service\[bot\]")](https://github.com/microsoft-github-policy-service\[bot]) [microsoft/dcp](https://github.com/microsoft/dcp) [Stars 187](https://github.com/microsoft/dcp) [MIT License](https://github.com/microsoft/dcp/blob/main/LICENSE) ## 🤓 Aspire team [Section titled “🤓 Aspire team”](#-aspire-team) Made with 💜 at Microsoft… ![Map showing Aspire community locations around the world](/_astro/map-darkdots.iqZ1Q9VF_Z1OvJKR.svg) # Thank you > Celebrate the open-source projects, libraries, and communities that make Aspire possible, from OpenTelemetry to the broader cloud-native ecosystem. Thank you, open source Standing on the shoulders of giants Aspire wouldn’t exist without the incredible open-source projects and communities that power the distributed apps ecosystem. This page is our thank-you to every contributor, maintainer, and community member behind the tools we depend on. We 💜 open source ## Observability & resilience [Section titled “Observability & resilience”](#observability--resilience) The eyes, ears, and safety nets of distributed systems. ![OpenTelemetry logo](/_astro/opentelemetry-icon.DTsvHyvo_j3OI6.svg)**OpenTelemetry**[](https://github.com/open-telemetry/opentelemetry-specification/blob/main/LICENSE "Apache-2.0: https://github.com/open-telemetry/opentelemetry-specification/blob/main/LICENSE") The vendor-neutral observability standard that gives Aspire its distributed traces, metrics, and logs. [opentelemetry.io](https://opentelemetry.io) [Docs](/fundamentals/telemetry/) ![Polly logo](/_astro/polly-icon.DwcvCPMA_Rokhq.webp)**Polly**[](https://github.com/App-vNext/Polly/blob/main/LICENSE "BSD-3-Clause: https://github.com/App-vNext/Polly/blob/main/LICENSE") The resilience and transient-fault-handling library behind Aspire’s default retry and circuit-breaker policies. [github.com](https://github.com/App-vNext/Polly) ## Databases & storage [Section titled “Databases & storage”](#databases--storage) The data engines Aspire integrates with out of the box. ![PostgreSQL logo](/_astro/postgresql-icon.DTORe-rE_qf1rA.webp)**PostgreSQL**[](https://www.postgresql.org/about/licence/ "PostgreSQL License: https://www.postgresql.org/about/licence/") The world’s most advanced open-source relational database. [postgresql.org](https://www.postgresql.org) [Docs](/integrations/databases/postgres/postgres-get-started/) ![MongoDB logo](/_astro/mongodb-icon.KisFuM9l_kLc8q.webp)**MongoDB**[](https://github.com/mongodb/mongo/blob/master/LICENSE-Community.txt "SSPL: https://github.com/mongodb/mongo/blob/master/LICENSE-Community.txt") The document database for modern application development. [mongodb.com](https://www.mongodb.com) [Docs](/integrations/databases/mongodb/mongodb-get-started/) ![Redis logo](/_astro/redis-icon.CxKuuAy2_y6Jk5.webp)**Redis**[](https://github.com/redis/redis/blob/unstable/LICENSE.txt "Source Available (RSAL): https://github.com/redis/redis/blob/unstable/LICENSE.txt") The high-performance in-memory data store used for caching, messaging, and more. [redis.io ](https://redis.io) [Docs](/integrations/caching/redis/redis-get-started/) ![Valkey logo](/_astro/valkey-icon.BdkvDMZ6_2aqeX.webp)**Valkey**[](https://github.com/valkey-io/valkey/blob/unstable/COPYING "BSD-3-Clause: https://github.com/valkey-io/valkey/blob/unstable/COPYING") The open-source, high-performance key/value store. [valkey.io ](https://valkey.io) [Docs](/integrations/caching/valkey/valkey-get-started/) ![Garnet logo](/_astro/garnet-icon.C0u592g4_Z1IyPoz.webp)**Garnet**[](https://github.com/microsoft/garnet/blob/main/LICENSE "MIT: https://github.com/microsoft/garnet/blob/main/LICENSE") A high-performance cache-store from Microsoft Research. [github.com](https://github.com/microsoft/garnet) [Docs](/integrations/caching/garnet/garnet-get-started/) ![Milvus logo](/_astro/milvus-icon.QcNRKvVY_2iFNU0.webp)**Milvus**[](https://github.com/milvus-io/milvus/blob/master/LICENSE "Apache-2.0: https://github.com/milvus-io/milvus/blob/master/LICENSE") The open-source vector database for AI-powered similarity search. [milvus.io ](https://milvus.io) [Docs](/integrations/databases/milvus/milvus-get-started/) ![Qdrant logo](/_astro/qdrant-icon.wEc8LmZK_j3OI6.svg)**Qdrant**[](https://github.com/qdrant/qdrant/blob/master/LICENSE "AGPL-3.0: https://github.com/qdrant/qdrant/blob/master/LICENSE") The vector search engine for next-generation AI applications. [qdrant.tech](https://qdrant.tech) [Docs](/integrations/databases/qdrant/qdrant-get-started/) ![SQLite logo](/_astro/sqlite-icon.5bKv4aj0_EsNj2.webp)**SQLite**[](https://www.sqlite.org/copyright.html "Public Domain: https://www.sqlite.org/copyright.html") The most widely deployed database engine in the world. [Docs](/integrations/databases/sqlite/sqlite-host/) [sqlite.org](https://www.sqlite.org) ## Messaging & eventing [Section titled “Messaging & eventing”](#messaging--eventing) The plumbing that keeps distributed services talking to each other. ![RabbitMQ logo](/_astro/rabbitmq-icon.D4S9ajGZ_j3OI6.svg)**RabbitMQ**[](https://github.com/rabbitmq/rabbitmq-server/blob/main/LICENSE-MPL-RabbitMQ "MPL-2.0: https://github.com/rabbitmq/rabbitmq-server/blob/main/LICENSE-MPL-RabbitMQ") The most widely deployed open-source message broker. [rabbitmq.com](https://www.rabbitmq.com) [Docs](/integrations/messaging/rabbitmq/rabbitmq-get-started/) ![NATS logo](/_astro/nats-icon.D9qyPulo_Z1SikB8.webp)**NATS**[](https://github.com/nats-io/nats-server/blob/main/LICENSE "Apache-2.0: https://github.com/nats-io/nats-server/blob/main/LICENSE") High-performance messaging for distributed apps and edge computing. [nats.io ](https://nats.io) [Docs](/integrations/messaging/nats/nats-get-started/) ![Apache Kafka logo](/_astro/apache-kafka-icon.SnHW5mZV_j3OI6.svg)**Apache Kafka**[](https://github.com/apache/kafka/blob/trunk/LICENSE "Apache-2.0: https://github.com/apache/kafka/blob/trunk/LICENSE") The distributed event streaming platform used by thousands of companies. [kafka.apache.org](https://kafka.apache.org) [Docs](/integrations/messaging/apache-kafka/apache-kafka-get-started/) ## Reverse proxy & networking [Section titled “Reverse proxy & networking”](#reverse-proxy--networking) The traffic directors and protocol layers that connect services. ![YARP logo](/_astro/yarp-icon.C9M5RSPI_j3OI6.svg)**YARP**[](https://github.com/dotnet/yarp/blob/main/LICENSE.txt "MIT: https://github.com/dotnet/yarp/blob/main/LICENSE.txt") Yet Another Reverse Proxy — a highly customizable reverse proxy library built on .NET. [github.com](https://github.com/microsoft/reverse-proxy) [Docs](/integrations/reverse-proxies/yarp/) ![gRPC logo](/_astro/grpc-icon.D5dFM_4R_j3OI6.svg)**gRPC**[](https://github.com/grpc/grpc/blob/master/LICENSE "Apache-2.0: https://github.com/grpc/grpc/blob/master/LICENSE") The high-performance, open-source universal RPC framework. [grpc.io ](https://grpc.io) ## Containers & orchestration [Section titled “Containers & orchestration”](#containers--orchestration) The engines that package, ship, and run distributed workloads. ![Docker logo](/_astro/docker.CnL-arqo_j3OI6.svg)**Docker**[](https://github.com/docker/compose/blob/main/LICENSE "Apache-2.0: https://github.com/docker/compose/blob/main/LICENSE") The platform that popularized containers and makes local development with Aspire seamless. [docker.com](https://www.docker.com) [Docs](/integrations/compute/docker/) ![Podman logo](/_astro/podman-icon.GYP5YcZr_j3OI6.svg)**Podman**[](https://github.com/containers/podman/blob/main/LICENSE "Apache-2.0: https://github.com/containers/podman/blob/main/LICENSE") The daemonless container engine for developing, managing, and running OCI containers. [podman.io ](https://podman.io) [Docs](/get-started/prerequisites/#install-an-oci-compliant-container-runtime) ![Kubernetes logo](/_astro/kubernetes.C_WJle_s_j3OI6.svg)**Kubernetes**[](https://github.com/kubernetes/kubernetes/blob/master/LICENSE "Apache-2.0: https://github.com/kubernetes/kubernetes/blob/master/LICENSE") The open-source system for automating deployment, scaling, and management of containerized applications. [kubernetes.io](https://kubernetes.io) [Docs](/integrations/compute/kubernetes/) ## Email & developer services [Section titled “Email & developer services”](#email--developer-services) Tools that simplify common dev-time workflows. ![Mailpit logo](/_astro/mailpit-icon.DNy-ogcL_j3OI6.svg)**Mailpit**[](https://github.com/axllent/mailpit/blob/master/LICENSE "MIT: https://github.com/axllent/mailpit/blob/master/LICENSE") An email and SMTP testing tool with a modern web UI, perfect for local development. [mailpit.axllent.org](https://mailpit.axllent.org) [Docs](/integrations/devtools/mailpit/mailpit-get-started/) ## AI & machine learning [Section titled “AI & machine learning”](#ai--machine-learning) The open-source projects powering Aspire’s AI integrations. ![Ollama logo](/_astro/ollama-icon.4z_rvyfm_236inw.webp)**Ollama**[](https://github.com/ollama/ollama/blob/main/LICENSE "MIT: https://github.com/ollama/ollama/blob/main/LICENSE") Run large language models locally — the easiest way to bring AI into your Aspire apps. [ollama.com](https://ollama.com) [Docs](/integrations/ai/ollama/ollama-get-started/) ## Multi-language ecosystem [Section titled “Multi-language ecosystem”](#multi-language-ecosystem) Aspire speaks many languages — thanks to these communities. ![Bun logo](/_astro/bun-icon.DvKtlocW_Zvg3lp.webp)**Bun**[](https://github.com/oven-sh/bun/blob/main/LICENSE.md "MIT: https://github.com/oven-sh/bun/blob/main/LICENSE.md") The all-in-one JavaScript runtime and toolkit built for speed. [Docs](/integrations/frameworks/bun-apps/) [bun.sh ](https://bun.sh) ![C# logo](/_astro/csharp.bEYMnWDV_j3OI6.svg)**C#**[](https://github.com/dotnet/roslyn/blob/main/License.txt "MIT: https://github.com/dotnet/roslyn/blob/main/License.txt") The modern, type-safe language that makes Aspire’s app model expressive and powerful. [learn.microsoft.com](https://learn.microsoft.com/dotnet/csharp/) ![Deno logo](/_astro/deno-icon.BlzooUTA_2iK0y7.webp)**Deno**[](https://github.com/denoland/deno/blob/main/LICENSE.md "MIT: https://github.com/denoland/deno/blob/main/LICENSE.md") The secure runtime for JavaScript and TypeScript with built-in tooling and web standards. [Docs](/integrations/frameworks/deno/deno-get-started/) [deno.com ](https://deno.com) ![Go logo](/_astro/go-icon.BtjqguRP_Z26LNUR.webp)**Go**[](https://go.dev/copyright "BSD-3-Clause: https://go.dev/copyright") The simple, efficient language for building reliable distributed services alongside Aspire. [Docs](/integrations/frameworks/go/go-get-started/) [go.dev ](https://go.dev) ![Java logo](/_astro/java-icon.R5ekku_P_Z1eIizY.webp)**Java**[](https://openjdk.org/legal/gplv2+ce.html "GPL-2.0 WITH Classpath-exception-2.0: https://openjdk.org/legal/gplv2+ce.html") The enterprise workhorse that Aspire can orchestrate as part of multi-language architectures. [Docs](/integrations/frameworks/java/java-get-started/) [java.com ](https://www.java.com) ![.NET MAUI logo](/_astro/maui-icon.oIIgefok_yQez1.webp)**.NET MAUI**[](https://github.com/dotnet/maui/blob/main/LICENSE.txt "MIT: https://github.com/dotnet/maui/blob/main/LICENSE.txt") The cross-platform framework for creating native mobile and desktop apps with .NET. [Docs](/integrations/dotnet/maui/) [dotnet.microsoft.com](https://dotnet.microsoft.com/apps/maui) ![Node.js logo](/_astro/nodejs-icon.KKaryUxz_Z2wxP2a.webp)**Node.js**[](https://github.com/nodejs/node/blob/main/LICENSE "MIT: https://github.com/nodejs/node/blob/main/LICENSE") The JavaScript runtime that enables Aspire to orchestrate frontend and full-stack JS apps. [nodejs.org](https://nodejs.org) [Docs](/integrations/frameworks/javascript/) ![Orleans logo](/_astro/microsoft-orleans.CgrIAp1e_ZiJkl0.webp)**Orleans**[](https://github.com/dotnet/orleans/blob/main/LICENSE "MIT: https://github.com/dotnet/orleans/blob/main/LICENSE") The virtual actor framework for building distributed, high-scale applications in .NET. [Docs](/integrations/frameworks/orleans/) [learn.microsoft.com](https://learn.microsoft.com/dotnet/orleans/) ![PowerShell logo](/_astro/powershell-icon.B0QrE_7N_Zkxxwk.webp)**PowerShell**[](https://github.com/PowerShell/PowerShell/blob/master/LICENSE.txt "MIT: https://github.com/PowerShell/PowerShell/blob/master/LICENSE.txt") The cross-platform task automation and configuration management framework. [Docs](/integrations/frameworks/powershell/powershell-get-started/) [github.com](https://github.com/PowerShell/PowerShell) ![Python logo](/_astro/python.HT_z5JOp_j3OI6.svg)**Python**[](https://docs.python.org/3/license.html "PSF-2.0: https://docs.python.org/3/license.html") The versatile language powering data science, AI, and web services in the Aspire ecosystem. [Docs](/integrations/frameworks/python/) [python.org](https://www.python.org) ![Rust logo](/_astro/rust-icon.Dbc7QCHB_ZdPYsX.webp)**Rust**[](https://www.rust-lang.org/policies/licenses "MIT OR Apache-2.0: https://www.rust-lang.org/policies/licenses") The systems programming language bringing safety and speed to distributed apps. [Docs](/integrations/frameworks/rust/rust-get-started/) [rust-lang.org](https://www.rust-lang.org) ![TypeScript logo](/_astro/typescript.C9-blvjE_j3OI6.svg)**TypeScript**[](https://github.com/microsoft/TypeScript/blob/main/LICENSE.txt "Apache-2.0: https://github.com/microsoft/TypeScript/blob/main/LICENSE.txt") The typed superset of JavaScript that powers Aspire’s TypeScript AppHost and frontend tooling. [typescriptlang.org](https://www.typescriptlang.org) ## Testing frameworks [Section titled “Testing frameworks”](#testing-frameworks) The tools that help keep Aspire apps reliable. ![xUnit.net logo](/_astro/xunit-icon.aPri2hmy_j3OI6.svg)**xUnit.net**[](https://github.com/xunit/xunit/blob/main/LICENSE "Apache-2.0: https://github.com/xunit/xunit/blob/main/LICENSE") The community-focused unit testing tool for .NET — the backbone of Aspire’s test story. [Docs](/testing/write-your-first-test/?testing-framework=xunit) [xunit.net ](https://xunit.net) ![NUnit logo](/_astro/nunit-icon.C9Gd4YFA_j3OI6.svg)**NUnit**[](https://github.com/nunit/nunit/blob/master/LICENSE.txt "MIT: https://github.com/nunit/nunit/blob/master/LICENSE.txt") The widely-used unit testing framework for all .NET languages. [Docs](/testing/write-your-first-test/?testing-framework=nunit) [nunit.org ](https://nunit.org) ![Playwright logo](/_astro/playwright-icon.CaBYkfIN_j3OI6.svg)**Playwright**[](https://github.com/microsoft/playwright/blob/main/LICENSE "Apache-2.0: https://github.com/microsoft/playwright/blob/main/LICENSE") The end-to-end testing framework for modern web apps — reliable cross-browser automation. [playwright.dev](https://playwright.dev) ## Localization [Section titled “Localization”](#localization) The tools that help us speak every language. ![Lunaria logo](/_astro/lunaria-icon.n4rwmYoU_j3OI6.svg)**Lunaria**[](https://github.com/lunariajs/lunaria/blob/main/LICENSE "MIT: https://github.com/lunariajs/lunaria/blob/main/LICENSE") The localization management tool that tracks translation status for Astro and Starlight sites. [lunaria.dev](https://lunaria.dev) [Docs](/community/translation-guide/#-about-translations) ## Web stack & documentation [Section titled “Web stack & documentation”](#web-stack--documentation) The tools that power this very website. ![Astro logo](/_astro/astro-icon.QJhfZyl8_j3OI6.svg)**Astro**[](https://github.com/withastro/astro/blob/main/LICENSE "MIT: https://github.com/withastro/astro/blob/main/LICENSE") The web framework for content-driven websites — the engine behind aspire.dev. [astro.build](https://astro.build) ![Starlight logo](/_astro/starlight-icon.DC5j8Auf_Z59XSc.webp)**Starlight**[](https://github.com/withastro/starlight/blob/main/LICENSE "MIT: https://github.com/withastro/starlight/blob/main/LICENSE") The beautiful documentation theme for Astro that makes aspire.dev shine. [starlight.astro.build](https://starlight.astro.build) ![Expressive Code logo](/_astro/expressivecode-icon.Z_AcOYAR_j3OI6.svg)**Expressive Code**[](https://github.com/expressive-code/expressive-code/blob/main/LICENSE "MIT: https://github.com/expressive-code/expressive-code/blob/main/LICENSE") The code block engine that makes our documentation examples beautiful and interactive. [expressive-code.com](https://expressive-code.com) ![Mermaid logo](/_astro/mermaid-icon.DD_1zqzE_j3OI6.svg)**Mermaid**[](https://github.com/mermaid-js/mermaid/blob/develop/LICENSE "MIT: https://github.com/mermaid-js/mermaid/blob/develop/LICENSE") The JavaScript-based diagramming and charting tool that renders our architecture diagrams. [mermaid.js.org](https://mermaid.js.org) ![sharp logo](/_astro/sharp-icon.CiVIswaO_j3OI6.svg)**sharp**[](https://github.com/lovell/sharp/blob/main/LICENSE "Apache-2.0: https://github.com/lovell/sharp/blob/main/LICENSE") The high-performance image processing library that optimizes every image on this site. [sharp.pixelplumbing.com](https://sharp.pixelplumbing.com) ![pnpm logo](/_astro/pnpm.Bm2ieaYB_j3OI6.svg)**pnpm**[](https://github.com/pnpm/pnpm/blob/main/LICENSE "MIT: https://github.com/pnpm/pnpm/blob/main/LICENSE") The fast, disk-space efficient package manager that builds the aspire.dev frontend. [pnpm.io ](https://pnpm.io) ## UI & design [Section titled “UI & design”](#ui--design) The design systems that make Aspire look and feel polished. ![Fluent UI logo](/_astro/fluentui-icon.DUdCrxnY_j3OI6.svg)**Fluent UI**[](https://github.com/microsoft/fluentui/blob/master/LICENSE "MIT: https://github.com/microsoft/fluentui/blob/master/LICENSE") Microsoft’s design system that powers the Aspire dashboard’s components and visual language. [fluent2.microsoft.design](https://fluent2.microsoft.design) [Docs](/dashboard/explore/) ![Spectre.Console logo](/_astro/spectreconsole-icon.6VTl8YWT_1Nt34i.webp)**Spectre.Console**[](https://github.com/spectreconsole/spectre.console/blob/main/LICENSE.md "MIT: https://github.com/spectreconsole/spectre.console/blob/main/LICENSE.md") The beautiful console library that powers the Aspire CLI’s rich terminal experience. [spectreconsole.net](https://spectreconsole.net) [Docs](/reference/cli/overview/) ## Runtime & framework [Section titled “Runtime & framework”](#runtime--framework) The bedrock of everything Aspire does — from the SDK and runtime to the Blazor-powered dashboard. ![.NET logo](/_astro/dotnet.DRRh45_B_j3OI6.svg)**.NET**[](https://github.com/dotnet/runtime/blob/main/LICENSE.TXT "MIT: https://github.com/dotnet/runtime/blob/main/LICENSE.TXT") The free, open-source, cross-platform framework that is the foundation of Aspire. [dotnet.microsoft.com](https://dotnet.microsoft.com) ![Blazor logo](/_astro/blazor-icon.BrxCUNEU_j3OI6.svg)**Blazor**[](https://github.com/dotnet/aspnetcore/blob/main/LICENSE.txt "MIT: https://github.com/dotnet/aspnetcore/blob/main/LICENSE.txt") Powers the Aspire dashboard — a rich, interactive UI built entirely in .NET. [dotnet.microsoft.com](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) [Docs](/dashboard/overview/) ![Dapr logo](/_astro/dapr-icon.MGF9MsE6_1hhe7T.webp)**Dapr**[](https://github.com/dapr/dapr/blob/master/LICENSE "Apache-2.0: https://github.com/dapr/dapr/blob/master/LICENSE") The Distributed Application Runtime — portable, event-driven building blocks for microservices. [dapr.io ](https://dapr.io) [Docs](/integrations/frameworks/dapr/dapr-get-started/) ## Developer experience & CLI [Section titled “Developer experience & CLI”](#developer-experience--cli) The tools that make the Aspire developer experience smooth. ![hex1b logo](/_astro/hex1b-icon.D7vAUBVS_j3OI6.svg)**hex1b**[](https://hex1b.dev "MIT: https://hex1b.dev") The CLI automation tool for terminal applications — used to test and validate Aspire’s CLI experiences. [hex1b.dev ](https://hex1b.dev/) ![asciinema logo](/_astro/asciinema-icon.EDxpvK06_j3OI6.svg)**asciinema**[](https://github.com/asciinema/asciinema/blob/develop/LICENSE "GPL-3.0: https://github.com/asciinema/asciinema/blob/develop/LICENSE") The terminal session recorder that powers the animated CLI demos on aspire.dev. [asciinema.org](https://asciinema.org/) ## And everyone else [Section titled “And everyone else”](#and-everyone-else) This page can’t capture every dependency, every pull request, or every community conversation that shaped Aspire into what it is today. To every contributor who filed an issue, opened a PR, answered a question on Discord, or shipped a NuGet package that Aspire depends on — **thank you**. ## Want to join them? Aspire is open source and welcoming new contributors. Whether it's code, docs, or community support — there's a place for you. [Start contributing](/community/contributor-guide/)[Join Discord](https://aka.ms/aspire/discord) # Translation guide for aspire.dev > Learn how to translate aspire.dev pages: set up your locale, run Lunaria locally, follow the style guide, and open localization pull requests for the Aspire docs. Thank you for your interest in helping translate `aspire.dev`! Localization makes Aspire documentation accessible to developers around the world, and your contributions are greatly appreciated. Tip Scroll to the bottom of any documentation page to find the **Translate this page** link: ![Translate this page link at the bottom of a documentation page](/_astro/translate-page.SO21i9Nq_ZfGIkQ.webp) ## 🗺️ Change locale [Section titled “🗺️ Change locale”](#️-change-locale) You can also switch between available languages using the language selector in the footer of any page: ![Language selector dropdown in the footer Preferences section](/_astro/language-selector.BEsVUsSS_EmBuY.webp) This allows you to view translated content for pages that have already been localized. ## 🌍 About translations [Section titled “🌍 About translations”](#-about-translations) The `aspire.dev` documentation supports multiple languages to help developers worldwide learn and use Aspire in their preferred language. We use [Lunaria](https://lunaria.dev/) to track translation progress and manage the localization workflow. ### Supported languages [Section titled “Supported languages”](#supported-languages) We currently support the following languages: | Language | Code | Status | | ------------------------- | ------- | ---------------- | | English | `en` | Source (default) | | Deutsch (German) | `de` | In progress | | Español (Spanish) | `es` | In progress | | Français (French) | `fr` | In progress | | Italiano (Italian) | `it` | In progress | | 日本語 (Japanese) | `ja` | In progress | | 한국어 (Korean) | `ko` | In progress | | Português do Brasil | `pt-br` | In progress | | Русский (Russian) | `ru` | In progress | | 简体中文 (Simplified Chinese) | `zh-cn` | In progress | | Türkçe (Turkish) | `tr` | In progress | | हिंदी (Hindi) | `hi` | In progress | | Dansk (Danish) | `da` | In progress | | Bahasa Indonesia | `id` | In progress | | Українська (Ukrainian) | `uk` | In progress | ## 📊 Check translation status [Section titled “📊 Check translation status”](#-check-translation-status) Before starting a translation, check the current status of translations using our Lunaria dashboard: [View Translation Status](/i18n/) The dashboard shows: * **Overall progress** for each language * **Individual page status** (translated, outdated, or missing) * **Quick links** to create or update translations ### Understanding the status page [Section titled “Understanding the status page”](#understanding-the-status-page) The Lunaria dashboard displays translation progress with the following indicators: * ✅ **Done** - The page is fully translated and up to date * 🔄 **Outdated** - The source content has changed since the translation was made * ❌ **Missing** - The page has not been translated yet ## 🚀 Getting started with translations [Section titled “🚀 Getting started with translations”](#-getting-started-with-translations) 1. **Visit the translation dashboard** Go to [aspire.dev/i18n/](https://aspire.dev/i18n/) to see the current translation status for all languages. 2. **Find a page to translate** Look for pages marked as “Missing” or “Outdated” in your language. Missing pages are great starting points! 3. **Create or update the translation file** Click on the page link in the dashboard to navigate to the source file. The translated file should be created at the corresponding path under your language’s directory. For example, if you’re translating `src/content/docs/get-started/what-is-aspire.mdx` to Japanese, create: ```mdx src/content/docs/ja/get-started/what-is-aspire.mdx ``` 4. **Submit a pull request** Once you’ve completed your translation, submit a pull request to the repository. See our [Contributor guide](/community/contributor-guide/) for detailed instructions on the PR process. ## 📁 File structure for translations [Section titled “📁 File structure for translations”](#-file-structure-for-translations) Translations follow a specific directory structure: * src/content/docs/ * get-started/ English (default) * what-is-aspire.mdx * ja/ Japanese translations * get-started/ * what-is-aspire.mdx * fr/ French translations * get-started/ * what-is-aspire.mdx * … Other languages Tip When creating a new translation file, copy the original English file first, then translate the content. This ensures you maintain the correct frontmatter structure and any component imports. ## ✍️ Translation best practices [Section titled “✍️ Translation best practices”](#️-translation-best-practices) Follow these best practices to ensure your translations are helpful, accurate, and consistent with the rest of the documentation. ### General guidelines [Section titled “General guidelines”](#general-guidelines) 1. **Preserve frontmatter** - Keep the `title` and other frontmatter fields, but translate their values where appropriate. 2. **Keep code blocks unchanged** - Code examples, command-line instructions, and file paths should generally remain in English. 3. **Translate alt text** - Image `alt` attributes should be translated for accessibility. 4. **Maintain links** - Keep internal links pointing to the same slugs; Starlight handles language routing automatically. 5. **Preserve component syntax** - Astro components like `