
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 中。核心函数getServiceNamespaceservice-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保留把第一个有内容的非标准库命名空间当作服务命名空间的旧行为。对应的姊妹函数getServiceNamespaceNameservice-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# 规范化拿到命名空间全名后getCSharpNamespaceNamenamespace-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.SubModelsAzure.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-L52export 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.tsexport interface EmitterOptionsContext { collectionType: CollectionType; serviceNamespace: string; }各渲染组件models、enums、controllers 等通过useEmitterOptions()读取该值用于子命名空间包裹与类型归属判断在 provider 之外如单元测试使用时回退为{ collectionType: array, serviceNamespace: }。而命名空间的子路径计算由getSubNamespacePartsnamespace-utils.ts#L44-L75完成它把类型的命名空间链与服务命名空间链做前缀比对返回服务命名空间之后的剩余片段例如服务为Microsoft.Contoso、类型在Microsoft.Contoso.Colors时返回[Colors]。也就是说service命名空间的决议结果会级联影响生成的每一个 model、enum 的 C# 命名空间归属。测试证据导入命名空间不再抢先针对本修复的回归测试位于 service-resolution.test.ts#L37-L53it(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.ProjectsserviceNamespaceName为规范化后的Azure.Ai.Projects并且只有服务命名空间内的Widget被当作服务模型Imported.ClientOptions不会被无条件发射。同文件还有两条与决议规则互相印证的用例无服务声明时走回退路径service-resolution.test.ts#L194-L209Other与Contoso两个命名空间都没有serviceserviceNamespace回退到先出现的Other且两个命名空间的模型都会被发射——这正是findServiceNamespace回退语义的验证。规范化行为验证service-resolution.test.ts#L211-L233my_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),仅供参考