TypeSpec http-server-csharp 生成器服务命名空间决议机制:@service 声明如何成为 C# 命名空间的唯一锚点
【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec
本文围绕@typespec/http-server-csharp包的一次行为修正展开:当 TypeSpec 规格中存在先于@service声明出现的导入命名空间或无关命名空间时,生成器现在始终以@service声明的命名空间作为生成的 C# 服务命名空间。读完后,你将理解该 emitter 从"发现命名空间"到"注入渲染上下文"的完整决议链路,以及无服务声明时的回退规则,从而在编写多命名空间规格时准确预测生成项目的 C# 命名空间。
修复内容:不再被"先遇到的命名空间"劫持
仓库中的变更记录 .chronus/changes/http-server-csharp-service-namespace-2026-09-08.md 声明了本次变更:
--- changeKind: fix packages: - "@typespec/http-server-csharp" --- Use the namespace declared with `@service` as the generated C# service namespace, even when an imported or unrelated namespace is encountered first.这是一个fix类型的行为修正,仅影响@typespec/http-server-csharp包。它的实际含义是:规格文件中声明的先后顺序(包括通过import引入的库命名空间、规格作者顺手写在最前面的工具型命名空间)都不应再影响最终 C# 项目的根命名空间——命名空间的唯一权威来源是@service装饰器落在哪个命名空间上。
决议入口:getServiceNamespace 的优先级
服务命名空间的发现逻辑集中在 service-discovery.ts 中。核心函数getServiceNamespace(service-discovery.ts#L113-L118)体现了修复后的优先级:
export function getServiceNamespace(program: Program): TspNamespace | undefined { const service = listServices(program)[0]; if (service) return service.type; return findServiceNamespace(program.getGlobalNamespaceType()); }决议规则可以归纳为两条:
- 首选
@service声明:listServices(program)从编译后的 AST 中提取所有@service装饰器声明的服务,取第一个服务的命名空间(service.type)。只要规格中显式声明了服务,命名空间就完全由它决定,与任何"先出现"的其他命名空间无关。 - 仅在无服务声明时回退:
findServiceNamespace才会被调用,用于兼容不写@service的独立规格(standalone spec),保留"把第一个有内容的非标准库命名空间当作服务命名空间"的旧行为。
对应的姊妹函数getServiceNamespaceName(service-discovery.ts#L123-L129)则在取到命名空间后做一次 C# 名称规范化,得到形如"Microsoft.Contoso"的最终字符串。
无服务声明时的回退:findServiceNamespace
回退路径的实现位于 namespace-utils.ts#L80-L97。它会从全局命名空间出发深度优先遍历子命名空间,跳过标准库命名空间(isStdNamespace),返回第一个有内容(包含 model、interface、operation 或 enum)的命名空间;若当前子节点本身没有内容,则递归更深层级,都找不到时才返回该空节点:
export function findServiceNamespace(globalNs: TspNamespace): TspNamespace | undefined { function findServiceNs(ns: TspNamespace): TspNamespace | undefined { for (const child of ns.namespaces.values()) { if (isStdNamespace(child)) continue; const hasContent = child.models.size > 0 || child.interfaces.size > 0 || child.operations.size > 0 || child.enums.size > 0; if (hasContent) return child; const deeper = findServiceNs(child); if (deeper) return deeper; return child; } return undefined; } return findServiceNs(globalNs); }这正是修复前的问题根源:修复前这类"先遍历到谁就用谁"的逻辑对有@service声明的规格也生效,因此写在@service之前(或经由 import 引入)的无关命名空间会抢先占据服务命名空间的位置。修复后,该函数只在listServices(program)为空时才会被走到。
命名空间名称的 C# 规范化
拿到命名空间全名后,getCSharpNamespaceName(namespace-utils.ts#L25-L36)负责把它转换成合法的 C# 命名空间:
export function getCSharpNamespaceName(dottedName: string): string { const namePolicy = createCSharpNamePolicy(); return dottedName .split(".") .map((part) => namePolicy.getName( namespaceReservedWords.has(part.toLowerCase()) ? `${part}Name` : part, "namespace", ), ) .join("."); }它逐段处理点分路径,有两层处理值得注意:
- 保留段重命名:除 C# 关键字与上下文关键字外,
namespaceReservedWords额外收录了boolean和type两个"非关键字但会遮蔽 BCL 类型"的标识符(namespace-utils.ts#L11-L16)。命中保留段的片段会被追加Name后缀,例如命名空间段Type会变成TypeName,避免与System.Type冲突。 - PascalCase 化:每个片段经
createCSharpNamePolicy()的命名策略转换为 PascalCase,例如my_service.sub_models规范化为MyService.SubModels,Azure.AI.Projects规范化为Azure.Ai.Projects。
决议结果如何注入渲染管线
服务命名空间的决议是全局"一次性"完成的,入口在 service-resolution.ts 的resolveServiceTypes。其五个阶段中,第 1 阶段就是命名空间发现(service-resolution.ts#L82-L85):
// Phase 1: Service namespace const serviceNamespace = getServiceNamespace(program); const serviceNamespaceName = getServiceNamespaceName(program); const declarationNamespaces = getDeclarationNamespaces(program);决议结果被封装进ServiceTypeResolution接口(service-resolution.ts#L33-L52):
export interface ServiceTypeResolution { /** The namespace declared with @service, or the standalone namespace fallback. */ serviceNamespace: TspNamespace | undefined; /** The C#-normalized service namespace name. */ serviceNamespaceName: string | undefined; // ... interfaces、models、enums、unionEnums 等 }接口注释本身就写明了修复语义:serviceNamespace取"用@service声明的命名空间,或独立命名空间回退值"。
随后在 emitter.tsx#L46 处,规范化后的名字被取出并作为渲染上下文注入整个组件树:
const serviceName = resolution.serviceNamespaceName ?? "ServiceProject"; // ... <EmitterOptions.Provider value={{ collectionType, serviceNamespace: serviceName }}>EmitterOptions上下文定义在 emitter-options-context.ts:
export interface EmitterOptionsContext { collectionType: CollectionType; serviceNamespace: string; }各渲染组件(models、enums、controllers 等)通过useEmitterOptions()读取该值,用于子命名空间包裹与类型归属判断;在 provider 之外(如单元测试)使用时回退为{ collectionType: "array", serviceNamespace: "" }。而命名空间的子路径计算由getSubNamespaceParts(namespace-utils.ts#L44-L75)完成:它把类型的命名空间链与服务命名空间链做前缀比对,返回服务命名空间之后的剩余片段(例如服务为Microsoft.Contoso、类型在Microsoft.Contoso.Colors时返回["Colors"])。也就是说,@service命名空间的决议结果会级联影响生成的每一个 model、enum 的 C# 命名空间归属。
测试证据:导入命名空间不再抢先
针对本修复的回归测试位于 service-resolution.test.ts#L37-L53:
it("uses the namespace declared with @service instead of an earlier non-standard namespace", async () => { const resolution = await resolve(` namespace Imported { model ClientOptions {} } @service namespace Azure.AI.Projects { model Widget { id: string; } op read(): Widget; } `); expect(resolution.serviceNamespace?.name).toBe("Projects"); expect(resolution.serviceNamespaceName).toBe("Azure.Ai.Projects"); expect(resolution.models.map((m) => m.name)).toEqual(["Widget"]); });这个用例完整覆盖了三个要点:Imported命名空间先于@service命名空间声明且包含 model,但不影响决议;最终服务命名空间是Azure.AI.Projects(serviceNamespaceName为规范化后的Azure.Ai.Projects);并且只有服务命名空间内的Widget被当作服务模型,Imported.ClientOptions不会被无条件发射。
同文件还有两条与决议规则互相印证的用例:
- 无服务声明时走回退路径(service-resolution.test.ts#L194-L209):
Other与Contoso两个命名空间都没有@service,serviceNamespace回退到先出现的Other,且两个命名空间的模型都会被发射——这正是findServiceNamespace回退语义的验证。 - 规范化行为验证(service-resolution.test.ts#L211-L233):
my_service.sub_models变成MyService.SubModels,名为Type的命名空间变成TypeName。
小结与适用边界
综合源码与测试,该 emitter 的服务命名空间决议可以概括为:
| 场景 | 决议结果 |
|---|---|
规格中存在@service声明(无论声明位置、是否有前置导入命名空间) | 取@service所在命名空间,规范化为 C# 命名空间 |
规格中无@service声明 | 回退到第一个有内容的非标准库命名空间 |
命名空间段命中 C# 关键字或type/boolean | 追加Name后缀(如Type→TypeName) |
| 命名空间段为小写/下划线风格 | 逐段 PascalCase 化 |
| 决议结果缺失(理论上仅组件脱离 provider 运行时) | emitter.tsx回退为ServiceProject |
适用前提说明:以上行为以当前仓库中@typespec/http-server-csharp的实现为准,决议发生在渲染前的resolveServiceTypes单一遍历中(service-resolution.ts#L59-L73 的注释说明了其五阶段顺序),因此命名空间决议对 models、enums、controllers 等所有下游组件是一致的。相关的同包修复(如服务命名空间以Microsoft.开头时ControllerBase的解析、以及"只发射服务内类型"的变更)记录在 packages/http-server-csharp/CHANGELOG.md,可作为理解本修复所处演进脉络的参考。
【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考