Aspire TypeScript AppHost 指南:使用 Aspire.Hosting.CodeGeneration.TypeScript 生成类型化托管 API 绑定
【免费下载链接】aspireAspire is the tool for code-first, extensible, observable dev and deploy.项目地址: https://gitcode.com/GitHub_Trending/as/aspire
Aspire 的Aspire.Hosting.CodeGeneration.TypeScript模块提供了一整套 TypeScript AppHost 脚手架与代码生成工具:它根据 Aspire Type System(ATS)元数据,为用 TypeScript 编写 AppHost 提供类型安全的 SDK,并通过 JSON-RPC 驱动 .NET AppHost 服务器。读完本文,你将掌握从aspire init --language typescript初始化、理解脚手架产物、到通过aspire restore/aspire run管理生成绑定的完整工作流,并了解生成的 TypeScript SDK 内部结构、类型映射规则与运行机制。
这个模块到底是什么
在深入代码之前,先明确它的定位:Aspire.Hosting.CodeGeneration.TypeScript是"用 TypeScript 编写 AppHost"的工具链,而不是"在 Aspire 中托管 JavaScript 或 TypeScript 应用"的集成。区别很关键——它的服务对象是 AppHost(应用程序编排宿主)本身,而不是业务应用。
从仓库结构看,该模块位于 src/Aspire.Hosting.CodeGeneration.TypeScript,核心由以下几部分组成:
- AtsTypeScriptCodeGenerator.cs:实现
ICodeGenerator接口的代码生成器,负责产出整个 TypeScript SDK; - TypeScriptLanguageSupport.cs:实现
ILanguageSupport接口,负责脚手架文件生成、语言检测与运行时配置; - TypeScriptApiProjector.cs:负责将
AtsContext解析为 TypeScript 特有的 API 决策(类型映射、options 展平、回调塑形、Promise 包装); - Resources 下的
base.mts、transport.mts、package.json等作为嵌入资源随包发布。
此外,csproj 文件 中有一个值得注意的细节:该包设置了IsAspirePolyglotCompatible=false,注释明确说明"这是代码生成基础设施,不是可发现的 polyglot 集成",因此它不会出现在aspire add面向非 C# AppHost 的集成列表里——这正是 README 中"不要用aspire add安装它"的原因。
快速开始:初始化一个 TypeScript AppHost
前置条件
按照 README 与源码中 package.json 的 engines 字段,你需要准备:
- Aspire CLI(提供
aspire init、aspire restore、aspire run等命令); - 受支持的 Node.js 运行时:引擎约束为
^20.19.0 || ^22.13.0 || >=24(该约束与 ESLint 10 的 Node 版本要求保持一致,避免安装或运行时失败); - 一个包管理器(npm、pnpm 或 yarn,CLI 会按已有 lockfile 识别)。
初始化命令
在应用目录下执行:
aspire init --language typescriptCLI 会做两件事:
- 生成
apphost.mts脚手架文件; - 自动恢复(restore)本代码生成包——所以请勿用
aspire add手动安装它。
这里还有一个分支逻辑值得注意:如果应用目录已经存在根级package.json(所谓 brownfield 场景),AppHost 会被创建在一个嵌套的aspire-apphost/包中,避免与现有 Node.js 项目的包结构冲突。源码中通过IsNestedBrownfieldPackage检测:目标目录名为aspire-apphost且其父目录存在package.json时,即按嵌套包处理(见 TypeScriptLanguageSupport.cs)。
最小 AppHost 程序
脚手架生成的apphost.mts内容如下:
import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); await builder.build().run();源码中脚手架的模板还附带了一些注释示例(见 TypeScriptLanguageSupport.cs),例如添加容器或 PostgreSQL:
// Add your resources here, for example: // const redis = await builder.addContainer("cache", "redis:latest"); // const postgres = await builder.addPostgres("db");注意createBuilder的导入路径是./.aspire/modules/aspire.mjs——这正是下一节要讲的生成绑定。
脚手架产物全解析
TypeScriptLanguageSupport.Scaffold方法(源码)一次会生成一组配套文件,理解每个文件的用途对日常开发很有帮助:
| 文件 | 作用 |
|---|---|
apphost.mts | AppHost 入口,导入生成的 SDK,声明资源与编排逻辑 |
package.json | 包清单,含脚本与依赖(详见下文) |
tsconfig.apphost.json | 仅针对 AppHost 的 TypeScript 配置,避免破坏已有项目的 tsconfig 设置 |
eslint.config.mjs | ESLint 配置,启用@typescript-eslint/no-floating-promises,让未 await 的 AppHost Promise 直接以 lint 错误暴露 |
apphost.run.json | 运行配置文件,包含随机生成的 Dashboard/OTLP/Resource Service 端口(见下文) |
.gitignore | 忽略node_modules/、dist/、.aspire/ |
package.json 的脚本与依赖
CreatePackageJson方法(源码)生成的脚本包括:
{ "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" } }在全新(非 brownfield)项目下还会额外生成lint、dev、build、watch四个别名脚本。依赖方面,运行时依赖vscode-jsonrpc@^8.2.0(JSON-RPC 传输层),开发依赖包括@types/node、eslint@^10.0.3、nodemon、tsx@^4.21.0、typescript@^5.9.3与typescript-eslint。
需要注意:脚手架生成 package.json 时不会读取磁盘上已有的 package.json——所有合并工作由 CLI 侧的PackageJsonMerger负责,避免二次合并导致依赖对象迭代顺序问题。
apphost.run.json 的随机端口
apphost.run.json通过AppHostProfilePortGenerator生成随机端口,包含 https 配置文件:
{ "profiles": { "https": { "applicationUrl": "https://localhost:{DashboardHttpsPort};http://localhost:{DashboardHttpPort}", "environmentVariables": { "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:{OtlpHttpsPort}", "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:{ResourceServiceHttpsPort}" } } } }测试场景下可通过ScaffoldRequest.PortSeed固定随机种子,保证端口可复现。
生成的绑定(Generated Bindings)
绑定从哪来
apphost.mts导入的 SDK 位于.aspire/modules/目录,它由两部分信息生成:
- Aspire Type System(ATS)元数据:来自
Aspire.Hosting及各个托管集成(hosting integration)——ATS 描述了每个托管 API 的类型、能力(capability)与参数; aspire.config.json中配置的集成:决定当前 AppHost 实际引入哪些集成及其能力。
生成出的 SDK 提供 AppHost 使用的类型化 API,并通过 JSON-RPC 调用 .NET AppHost 服务器。生成流程的主入口是 AtsTypeScriptCodeGenerator.GenerateDistributedApplication,它会产出三个文件:
transport.mts:传输层,包含AspireClient、Handle、registerCallback、CancellationToken、CapabilityError等 JSON-RPC 通信原语;base.mts:基础类型库,手工维护而非生成,包含ReferenceExpression、Awaitable<T>、ResourceBuilderBase、AspireList/AspireDict等;aspire.mts:真正根据 ATS 元数据生成的 SDK,即 AppHost 导入的文件。
绑定管理命令
| 命令 | 作用 |
|---|---|
aspire restore | 在 AppHost 目录下执行,不启动 AppHost仅重新生成绑定 |
aspire run | 启动 AppHost(运行前同样会确保绑定是最新的) |
铁律:永远不要手编辑.aspire/modules/下的任何文件——它是生成产物。需要改变 API 行为时,应该修改 AppHost 代码或调整aspire.config.json中的集成引用,然后重新aspire restore。从生成文件的头部注释 "GENERATED CODE - DO NOT EDIT" 也能印证这一点。
生成的 TypeScript SDK 内部结构与类型映射
AtsTypeScriptCodeGenerator的 XML 文档注释(源码)完整记录了 ATS 到 TypeScript 的类型映射规则,这是理解生成 SDK 的核心。
基本类型映射
| ATS 类型 | TypeScript 类型 |
|---|---|
string | string |
number | number |
boolean | boolean |
any | unknown |
callback | (context: EnvironmentContextHandle) => Promise<void> |
T[](数组) | T[](映射后的类型数组) |
Handle 类型命名规则
ATS 类型 ID 使用{AssemblyName}/{TypeName}格式,映射为 TypeScript 的 Handle 类型别名时遵循三条规则:
- 核心类型:类型名 +
Handle,例如Aspire.Hosting/IDistributedApplicationBuilder→BuilderHandle、Aspire.Hosting/DistributedApplication→ApplicationHandle、Aspire.Hosting/DistributedApplicationExecutionContext→ExecutionContextHandle; - 接口类型:接口名 +
Handle(保留 I 前缀),例如Aspire.Hosting.ApplicationModel/IResource→IResourceHandle; - 资源类型:类型名 +
BuilderHandle,例如Aspire.Hosting.Redis/RedisResource→RedisResourceBuilderHandle、Aspire.Hosting/ContainerResource→ContainerResourceBuilderHandle。
这些别名在GenerateHandleTypeAliases中生成,形如type BuilderHandle = Handle<'Aspire.Hosting/IDistributedApplicationBuilder'>;(源码),属于内部类型,用户实际接触的是包装类。
Builder 类与 Promise 包装
- 资源类型会生成对应的 builder 类及其 thenable 包装:
RedisResource→RedisResourceBuilder类 +RedisResourceBuilderPromise(extends PromiseLike<RedisResourceBuilder>),这样 fluent 调用可以在未 await 的情况下继续链式书写; - 接口类型生成抽象基类并以
BuilderBase后缀命名,例如IResource→ResourceBuilderBase; - 具体 builder 依据类型继承层级扩展接口 builder,实现"具体继承抽象"的类体系。
生成器中还有一个AspireClient客户端类,承载剩余的入口点方法(entry point capabilities)。从 TypeScriptApiProjector.cs 可以看到,每个入口点函数都把client: AspireClientRpc作为第一个参数显式传入,而AspireClientRpc的核心签名只有一个:
export interface AspireClientRpc { readonly connected: boolean; invokeCapability<TResult = unknown>(capabilityId: string, args?: Record<string, unknown>): Promise<TResult>; }也就是说,一切 AppHost 操作最终都归结为对invokeCapability的 JSON-RPC 调用。
方法命名与属性生成
- 方法名默认派生自能力 ID:如
Aspire.Hosting.Redis/addRedis→addRedis;TypeScript 侧统一使用 camelCase(能力 ID 本身就是规范形式); - 也可以通过
[AspireExport(MethodName = "...")]特性显式覆盖; - 能力按
PropertyGetter/PropertySetter/InstanceMethod/Method分类,getter/setter 会按属性名分组合并生成{ get, set }形式的属性接口(源码); - DTO 类型生成
export interface,所有属性均为可选(propName?: type)以允许部分对象,且 PascalCase 属性名会被转换为 camelCase; - 每个方法/属性都会携带从 ATS 元数据投影出的 JSDoc 文档注释,包括
@param、@returns、@deprecated标记。
文档与生成的代码永不漂移
TypeScriptApiProjector有一个很有意思的设计保证(见其 类注释):它同时被AtsTypeScriptCodeGenerator(生成实际 SDK)和TypeScriptApiExportWriter(生成规范 API 导出文档)消费,两者基于同一个解析结果输出,因此文档中重建的签名与真实发布的 SDK 签名不可能漂移。这正是 README 中"生成的 SDK"稳定性的源码级保证。
运行机制:从 AppHost 到 .NET 服务器的执行链路
TypeScriptLanguageSupport.GetRuntimeSpec(源码)定义了完整的运行契约:
| 阶段 | 执行内容 |
|---|---|
| 安装依赖 | npm install |
| PreExecute(执行前) | npx --no-install tsc --noEmit -p tsconfig.apphost.json(类型检查) |
| Execute(执行) | npx --no-install tsx --tsconfig tsconfig.apphost.json {appHostFile} |
| WatchExecute(监听模式) | nodemon监听ts,mts扩展名,忽略node_modules/与.aspire/modules/,每次变更先tsc --noEmit再通过tsx重启 |
语言标识为typescript/nodejs(格式{language}/{runtime},为将来支持typescript/bun、typescript/deno预留了扩展空间),检测模式为apphost.mts与apphost.ts两种文件 + 必须存在package.json。
另外,模块通过NODE_EXTRA_CA_CERTS环境变量向 Node.js 注入 Aspire 的证书包(CertificateBundleEnvironmentVariable),保证 TLS 链路在本地开发环境可用;该属性通过反射探测方式设置,以保证与旧版 CLI 的兼容性。
ReferenceExpression:连接资源的表达式
base.mts中定义的ReferenceExpression是生成 SDK 里非常实用的能力(源码),它允许把端点引用组合成表达式,并在协议层序列化为$expr格式。其官方注释给出的示例:
const redis = await builder.addRedis("cache"); const endpoint = await redis.getEndpoint("tcp"); // Create a reference expression const expr = refExpr`redis://${endpoint}:6379`; // Use it in an environment variable await api.withEnvironment("REDIS_URL", expr);序列化后的 JSON 形如:
{ "$expr": { "format": "redis://{0}:{1}", "valueProviders": [ { "$handle": "Aspire.Hosting.ApplicationModel/EndpointReference:1" }, { "$handle": "Aspire.Hosting.ApplicationModel/EndpointReference:2" } ] } }条件表达式(condition+whenTrue/whenFalse)同样受支持,这让环境变量的取值可以跟随运行时条件动态解析。
测试与验证体系
仓库在 tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests 下提供了完整的测试套件,可作为你理解该模块行为的活文档:
- TypeScriptLanguageSupportTests.cs:验证脚手架输出的完整性与正确性——包括 package.json 的名称、脚本、
engines.node约束、依赖版本、tsconfig.apphost.json的 outDir、ESLint 配置内容等;还验证 brownfield 场景下输出仅包含 Aspire 想要的条目、不会破坏已有 package.json; - AtsTypeScriptCodeGeneratorTests.cs:验证代码生成器输出;
- Snapshots 目录:存放快照测试基线,如
AtsGeneratedAspire.verified.ts(整体生成的 SDK)、WithPersistenceCapability.verified.txt、WithDataVolumeOptionsMerged.verified.ts(options 展平)等,展示真实生成结果; - TestTypes:测试用集成类型,如
TestRedisResource.cs、TestMarkerResource.cs,用于验证各类能力的代码生成路径。
注意事项与最佳实践
- 不要用
aspire add安装本包:它通过aspire init --language typescript自动还原,手动安装会破坏版本一致性; - 不要编辑
.aspire/modules/:所有改动通过修改 AppHost 或aspire.config.json中的集成引用来完成,随后执行aspire restore; - 区分概念:本模块服务于 TypeScript AppHost 的开发体验,与托管 JS/TS 应用是两回事,不要混淆使用场景;
- Node 版本:遵循
^20.19.0 || ^22.13.0 || >=24的引擎约束,否则 ESLint 10 等工具链可能出现安装或运行失败; - brownfield 场景:已有根
package.json时 AppHost 会放入嵌套的aspire-apphost/包,生成独立的tsconfig.apphost.json,避免污染现有 TypeScript 配置。
通过本文,你可以完整掌握基于Aspire.Hosting.CodeGeneration.TypeScript的 TypeScript AppHost 开发流程:从初始化、理解脚手架与生成绑定,到读懂生成的 SDK 类型系统与运行链路。后续可以结合 src/Aspire.Hosting.CodeGeneration.TypeScript 的源码与 tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests 的测试深入实践。
【免费下载链接】aspireAspire is the tool for code-first, extensible, observable dev and deploy.项目地址: https://gitcode.com/GitHub_Trending/as/aspire
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考