Skip to content

Davidfowl/ts apphost#13705

Merged
davidfowl merged 215 commits into
mainfrom
davidfowl/ts-apphost
Jan 13, 2026
Merged

Davidfowl/ts apphost#13705
davidfowl merged 215 commits into
mainfrom
davidfowl/ts-apphost

Conversation

@davidfowl
Copy link
Copy Markdown
Contributor

@davidfowl davidfowl commented Dec 26, 2025

Polyglot AppHost Support: Write Aspire App Hosts in TypeScript

Overview

This PR introduces polyglot AppHost support, enabling developers to write Aspire app hosts in non-.NET languages. TypeScript is the first supported language, with the architecture designed to support additional languages (Go, Python, etc.) in the future.

This work builds on the foundational JSON-RPC infrastructure established by @sebastienros in #11667.

What's New

Developers can now create and run Aspire app hosts entirely in TypeScript:

import { createBuilder, refExpr } from './.modules/aspire.js';

const builder = await createBuilder();

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

const api = await builder
    .addContainer("api", "mcr.microsoft.com/dotnet/samples:aspnetapp")
    .withEnvironment("REDIS_URL", refExpr`redis://${await cache.getEndpoint("tcp")}`)
    .waitFor(cache);

await builder.build().run();

Aspire Type System (ATS)

The core innovation is the Aspire Type System (ATS) - a portable type system that maps .NET types to a unified representation any language can work with:

Concept Description
Type ID Portable identifier: {Assembly}/{FullTypeName}
Capability Named operation: Aspire.Hosting.Redis/addRedis
Handle Opaque typed reference to a .NET object
DTO Serializable data transfer object

Integration authors expose their existing extension methods by adding [AspireExport] attributes - no wrapper code needed:

[AspireExport("addRedis", Description = "Adds a Redis resource")]
public static IResourceBuilder<RedisResource> AddRedis(
    this IDistributedApplicationBuilder builder,
    [ResourceName] string name,
    int? port = null)
{
    // Existing implementation - unchanged
}

Architecture

The CLI orchestrates two processes that communicate via JSON-RPC over Unix domain sockets:

flowchart TB
    subgraph CLI["Aspire CLI"]
        direction LR
        subgraph Guest["Guest Runtime (Node.js)"]
            direction TB
            UserCode["User Code<br/>(apphost.ts)"]
            SDK["Generated SDK<br/>(aspire.ts)"]
            ATSClient["ATS Client"]
            UserCode --> SDK --> ATSClient
        end

        subgraph Host["AppHost Server (.NET)"]
            direction TB
            Packages["Aspire.Hosting.*<br/>(Redis, etc)"]
            Dispatcher["Capability Dispatcher"]
            RPCServer["JSON-RPC Server"]
            Packages --> Dispatcher --> RPCServer
        end

        ATSClient <-->|"JSON-RPC<br/>(Unix Socket)"| RPCServer
    end

    CLI -.->|spawns| Guest
    CLI -.->|spawns| Host
Loading

Key Components

Capability Scanner (Aspire.Hosting)

  • Scans assemblies for [AspireExport] attributes
  • Performs 2-pass expansion: collects type hierarchy, then expands interface targets to concrete implementations
  • Discovers types from parameters, return types, and callback signatures

Code Generation (Aspire.Hosting.CodeGeneration.TypeScript)

  • Generates type-safe TypeScript SDK from ATS capabilities
  • Produces fluent async API with thenable wrappers for single-await chains
  • Handles callbacks, reference expressions, and collection wrappers

RemoteHost (Aspire.Hosting.RemoteHost)

  • JSON-RPC server for guest-host communication
  • Capability dispatcher routes calls to .NET implementations
  • Handle registry manages object lifecycle

CLI Integration (Aspire.Cli)

  • TypeScriptAppHostProject implements IAppHostProject
  • Automatic SDK regeneration when packages change
  • Hot reload support via nodemon

Feature Flag

Polyglot support is behind a feature flag during preview. Enable it globally:

aspire config set features.polyglotSupportEnabled true -g

CLI Commands

Once the feature flag is enabled, the --language option becomes available:

Command Description
aspire init --language typescript Initialize TypeScript AppHost in current directory
aspire new --language typescript Create new project with TypeScript AppHost
aspire run Build and run (development mode)
aspire publish Build and run (publish mode)
aspire add <package> Add integration package

Type System Design

ATS flattens .NET's polymorphism into a simple, portable model:

  • Interface inheritance → Expanded to concrete types at scan time
  • Generic constraints → Resolved to concrete types at scan time
  • Method overloading → Not supported (method names must be unique per target type)

Each concrete type gets a complete list of capabilities - no type hierarchy to reason about in guest languages.

Generated TypeScript Features

  • Fluent async chaining: await builder.addRedis("cache").withRedisCommander()
  • Reference expressions: refExpr`redis://${endpoint}`
  • Typed callbacks: withEnvironmentCallback(async (ctx) => { ... })
  • Collection wrappers: AspireDict<K,V> and AspireList<T> for mutable .NET collections
  • Automatic handle wrapping: Callback parameters receive typed wrapper classes

Security

Both guest and host run locally, started by the CLI:

  • Capability allowlist (only [AspireExport] methods callable)
  • Socket authentication with secret token
  • Unix socket permissions (owner-only)
  • DTO enforcement (only [AspireDto] types serialized)

Documentation

See docs/specs/polyglot-apphost.md for complete specification including:

  • Wire protocol details
  • Type mapping tables
  • Code generation architecture
  • Guide for adding new languages

Testing

  • Unit tests for capability scanning and type expansion
  • Snapshot tests for generated TypeScript code
  • Integration tests with TypeScript playground

- Introduced AppHostRunner and AppHostRunnerFactory for managing different AppHost types.
- Implemented validation and execution logic for TypeScript (apphost.ts) and Python (apphost.py) AppHosts.
- Enhanced ProjectLocator to detect and handle TypeScript and Python AppHost files.
- Added AppHostType enum to represent different AppHost project types.
- Added ProjectModel class to scaffold and manage the GenericAppHost project.
- Introduced Instruction models for handling various commands (Create, Run, Pragma, Declare, Invoke).
- Developed InstructionProcessor to execute instructions and manage application builders.
- Created JsonRpcServer to handle client connections and execute instructions via JSON-RPC.
- Implemented OrphanDetector to monitor the host process and exit if it is no longer running.
- Added Program class to initialize and start the JsonRpc server with graceful shutdown handling.
- Added ICodeGenerationService interface for generating TypeScript code from Aspire packages.
- Created TypeScriptCodeGenerator class for generating TypeScript SDK from the Aspire application model.
- Introduced ApplicationModel, IntegrationModel, ResourceModel, and related classes to represent the application structure.
- Developed client and distributed application builder for managing resources and executing instructions in TypeScript.
- Added TypeScript project files and dependencies for the new code generation functionality.
- Added IAppHostProject interface for defining common methods for AppHost projects.
- Created AddPackageContext class to encapsulate package addition context.
- Implemented PythonAppHostProject class to handle Python AppHost projects, including validation, running, and package management.
- Implemented TypeScriptAppHostProject class to handle TypeScript AppHost projects, including validation, running, and package management.
- Introduced IAppHostProjectFactory interface for creating AppHost project handlers based on type.
- Added methods for managing dependencies and environment setup for both Python and TypeScript projects.
- Deleted client TypeScript definitions and implementations, including callback registration and remote app host client logic.
- Removed distributed application builder and resource builder classes, along with their associated methods and types.
- Eliminated launch settings reader and related functions for managing application launch configurations.
- Cleaned up index files by removing exports related to the deleted modules.
- Removed package.json and package-lock.json files as they are no longer needed.
…and error propagation; add ListProxy for .NET collections
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Dec 26, 2025

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 13705

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 13705"

… proxy handling; update InstructionProcessor for marshalled results
…eration based on ASPIRE_REPO_ROOT configuration
- Removed Python AppHost project handling from AppHostProjectFactory and related classes.
- Deleted PythonAppHostProject.cs and its associated logic for validation and execution.
- Updated AppHostLanguage and AppHostType enums to exclude Python.
- Modified InitCommand and NewCommand to no longer create Python AppHost templates.
- Adjusted RunCommand and ProjectLocator to eliminate Python-specific checks.
- Cleaned up comments and documentation to reflect the removal of Python support.
- Added RemoteAppHostService to handle JSON-RPC methods for executing instructions, invoking methods, and managing properties.
- Introduced JsonRpcServer to manage client connections over a Unix domain socket, including graceful shutdown and client disconnection handling.
- Implemented OrphanDetector as a background service to monitor the parent process and trigger shutdown if it dies.
- Integrated orphan detection into the server startup process, allowing for automatic shutdown on parent process termination.
- Enhanced error handling and logging throughout the server and service methods.
- Implement JsonRpcCallbackInvoker to handle callback invocations using JSON-RPC.
- Create ObjectRegistry to manage registration and lookup of .NET objects for marshalling to/from TypeScript.
- Update RemoteAppHostService to utilize the new JsonRpcCallbackInvoker.
- Add unit tests for InstructionProcessor and ObjectRegistry to ensure functionality.
- Implement TestCallbackInvoker for testing callback invocations.
- Added ReferenceExpression class for creating and serializing reference expressions.
- Introduced ResourceBuilderBase, AspireList, and AspireDict classes for resource management.
- Developed transport layer with Handle, CapabilityError, and callback registration.
- Implemented AspireClient for JSON-RPC connection handling and capability invocation.
- Added error handling for ATS capabilities and structured error responses.
…ement

- Added methods to scan assemblies and return an AtsContext for code generation.
- Updated ICodeGenerator interface to generate code from AtsContext instead of separate capabilities and DTO types.
- Introduced AtsEnumTypeInfo to represent enum types discovered during scanning.
- Enhanced AtsCapabilityScanner to collect and manage enum types during assembly scanning.
- Updated AtsMarshaller to handle unmarshalling of enums from string and numeric values.
- Added tests for enum handling in marshalling and capability dispatching.
- Updated TypeScript code generation to include generated enums and their usage in DTOs and options.
@@ -0,0 +1 @@
BB3E5A59ABF66ED257D929D56559C54898462D19316B5AE7E86B7011FF370555 No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this folder supposed to be tracked? The cli is still required to start the apphost so it would still be re-generated if not.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reasons to keep it: detect changes when updating codegen, see what it looks like (same as manifest files)

- Aspire.Hosting.Redis/withDataVolume
- ThirdParty.Integration/withDataVolume

Resolution: Use [AspireExport("uniqueMethodName")] to disambiguate.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really a resolution or a requirement on the library authors? What about something that the app host dev could specify in an options file, could even rename anything even if there aren't any conflicts.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then next level of option is to be able to aspire-import any method that might not be exported by default. (kind of private reflection).


**Startup Sequence:**

1. CLI scaffolds an AppHost server project in `$TMPDIR/.aspire/hosts/<hash>/`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... unique to the apphost project location

2. CLI adds required hosting packages (Redis, Postgres, etc.)
3. CLI builds the .NET project
4. Code generation scans assemblies for `[AspireExport]` and generates SDK
5. CLI starts the AppHost server with socket path
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is guest runtime the official name? No "App Host" something

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes apphost sever and guest


| Environment Variable | Description | Example |
|---------------------|-------------|---------|
| `REMOTE_APP_HOST_SOCKET_PATH` | Unix socket path (or named pipe name on Windows) | `/tmp/aspire/host.sock` |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be renamed to match the name "App Host Server" instead of "remote"?

CLI->>Guest: Start (socket path via env var)

Guest->>Host: invokeCapability("Aspire.Hosting/createBuilder", {})
Host-->>Guest: { $handle: "1", $type: "Aspire.Hosting/Aspire.Hosting.IDistributedApplicationBuilder" }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will check. Are handles strings (unique)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are ints today but will probably need to change that (overflow), the handle is is unique.


## Aspire Type System (ATS)

ATS is the central type system that bridges .NET and guest languages. Every type crossing the boundary has an **ATS type ID** that serves as its portable identity.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should guest runtimes have hooks/events/filters to be able to alter the messages?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we need it we can add it. What scenario are you thinking ?


### Type Exporting and Polymorphism Flattening

ATS doesn't have a closed set of primitive types. Instead, any .NET type can be exported using `[AspireExport]`, and the scanner automatically expands capabilities based on type relationships.
Copy link
Copy Markdown
Contributor

@sebastienros sebastienros Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a command to list the capabilities of a specific project? Might be useful for API reviews and detect breaking changes. Or even debugging.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure that’s useful for anyone but us so not a command but something we can do in debug mode

Declared on extension methods:

```csharp
[AspireExport("addRedis", Description = "Adds a Redis resource")]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a doc format for things like see cref but using ATS? Are parameters documented?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea likely


| Direction | `[AspireDto]` Type | Non-`[AspireDto]` Type |
|-----------|-------------------|----------------------|
| Input (JSON → .NET) | Deserialized | **Error** |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really an Error or more precisely it should be a handle that is sent (otherwise error)

@Meir017
Copy link
Copy Markdown
Contributor

Meir017 commented Jan 12, 2026

Looks really cool, is this a similar pattern to how playwright supports multiple languages?

@davidfowl
Copy link
Copy Markdown
Contributor Author

Looks really cool, is this a similar pattern to how playwright supports multiple languages?

I didn't look at playwright. @sebastienros and I built several prototypes over the last year, and this is the approach we liked best.

@davidfowl davidfowl merged commit e94fdf3 into main Jan 13, 2026
292 checks passed
@davidfowl davidfowl deleted the davidfowl/ts-apphost branch January 13, 2026 04:34
@dotnet-policy-service dotnet-policy-service Bot added this to the 13.2 milestone Jan 13, 2026
@Meir017
Copy link
Copy Markdown
Contributor

Meir017 commented Jan 13, 2026

Looks really cool, is this a similar pattern to how playwright supports multiple languages?

I didn't look at playwright. @sebastienros and I built several prototypes over the last year, and this is the approach we liked best.

FYI @pavelfeldman

@sebastienros
Copy link
Copy Markdown
Contributor

@Meir017 your comment got me curious so I investigated as I had never actually looked at how playwright works.

  • Playright client SDKs start a node process they own and communicate with it using JSON-RPC over stdio. It accepts parallel commands (they are made serial by the SDK).
  • The client sdks generation is driven by the communication protocol and api specifications.
  • The node app owns the Chrome/CDP interop.

Key differences:

  • Aspire sdks specification is extracted from the nuget packages using reflection. But first in a intermediate model, kind of like the api specification of playwright.
  • The aspire is a shared process so communication is using a network stream instead of stdio.
  • Aspire's API surface is not stable as integrations can be imported, whereas Playwright has a stable api.json for the client sdks to store the shape of the API.

Disclaimer: based on my (and ChatGPT's) understanding of playwright

@github-actions github-actions Bot locked and limited conversation to collaborators Feb 13, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants