TypeSpec C 生成器插件开发实战:用 GeneratorPlugin 轻量定制 http-client-csharp 的生成输出
2026/9/17 22:02:46 网站建设 项目流程

TypeSpec C# 生成器插件开发实战:用 GeneratorPlugin 轻量定制 http-client-csharp 的生成输出

【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec

本文围绕@typespec/http-client-csharp官方文档中的 Generator plugins 机制展开,系统讲解如何在不编写完整自定义生成器的前提下,通过实现GeneratorPlugin挂接额外的 visitor、rewriter、元数据引用与共享源码目录,参与内置ScmCodeModelGenerator的常规生成流水线。读完本文,你将掌握插件从编写、构建、配置到被加载执行的全链路方法,并理解其底层 MEF 装配与路径解析原理。

什么是 Generator plugins:轻量定制 C# 生成器的方式

@typespec/http-client-csharp(本仓库位于 packages/http-client-csharp)的 C# 代码生成由 .NET 侧生成器完成,默认入口是内置的ScmCodeModelGenerator。官方文档 plugins.md 指出:插件是一种轻量级的定制手段,无需编写完整自定义生成器,只需注册额外的 visitors、rewriters、metadata references 或 shared source 目录,即可参与正常生成流水线。典型用途包括:

  • 重命名生成的类型(rename types);
  • 为生成的成员添加属性(add attributes);
  • 对生成的模型做后置处理(post-process models);
  • 引入额外的程序集引用或共享源码文件。

插件的存在让团队可以在不 fork 生成器、不修改仓库源码的前提下,对生成结果做精细的个性化调整,同时持续跟随上游ScmCodeModelGenerator的更新。

生成器如何发现插件:两种加载途径

按照 plugins.md 的说明,生成器通过两种方式发现插件:

  1. 自动发现:扫描项目node_modules中列出的包的dist文件夹;
  2. 显式指定:通过pluginsemitter 选项提供的路径加载。

本文重点讨论plugins选项——它允许 emitter 作者将一个或多个插件程序集(或包含程序集的目录/工程)指向生成器。

从源码看,两种途径最终都汇入GeneratorHandler的 MEF 目录装配逻辑(GeneratorHandler.cs):

  • AddPluginDlls:自动发现路径。它从启动目录向上寻找node_modules根目录,读取根目录下package.jsondependencies,按依赖顺序依次检查每个包目录下的dist子目录,收集其中的*.dll;若某个依赖包没有预编译 DLL,则尝试通过BuildPluginIfNeeded就地构建其.csproj
  • AddConfiguredPluginDlls:处理plugins配置项,把用户显式给出的路径加入AggregateCatalog

随后这些目录/程序集被组合进 MEFCompositionContainerGeneratorPlugin子类即可被自动装配并发现。

编写插件:继承 GeneratorPlugin 并重写 Apply

一个插件就是一个继承GeneratorPlugin的 C# 类,重写其Apply方法。基类定义在 GeneratorPlugin.cs,它通过 MEF 的[InheritedExport]导出:

// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.ComponentModel.Composition; namespace Microsoft.TypeSpec.Generator { /// <summary> /// Base class for generator plugins. /// </summary> [InheritedExport] [Export(typeof(GeneratorPlugin))] public abstract class GeneratorPlugin { public abstract void Apply(CodeModelGenerator generator); } }

关键点:[InheritedExport]意味着任何子类在程序集被加载后都会被自动发现,你不需要自己添加[Export]特性。一个最小插件如下(与官方文档示例一致):

using Microsoft.TypeSpec.Generator; public class MyPlugin : GeneratorPlugin { public override void Apply(CodeModelGenerator generator) { // Register a custom visitor that transforms the output library. generator.AddVisitor(new MyLibraryVisitor()); } }

Apply收到的CodeModelGenerator参数(定义于 CodeModelGenerator.cs)暴露了插件可用的全部扩展点:

扩展点签名作用
注册访问器AddVisitor(LibraryVisitor visitor)添加一个遍历并修改输出库的 visitor
注册重写器AddRewriter(LibraryRewriter rewriter)添加 rewriter 转换生成的成员
添加元数据引用AddMetadataReference(MetadataReference reference)让额外程序集对生成代码可见
添加共享源码目录AddSharedSourceDirectory(string sharedSourceDirectory)引入一个共享源码文件目录

从实现细节看,这些方法都是向生成器实例内部的集合追加元素:

  • AddVisitor追加到_visitorsAddRewriter追加到_rewriters,二者分别通过只读属性VisitorsRewriters对外暴露;
  • AddMetadataReference追加到_additionalMetadataReferencesAddSharedSourceDirectory追加到_sharedSourceDirectories

值得注意的是,CodeModelGenerator还提供了配套的RemoveVisitor<T>()RemoveVisitor(string visitorTypeName)方法,可按类型或类型名移除已注册的 visitor——后者尤其适合移除那些类型不可公开访问的内置 visitor,这一能力在定制内置生成器行为时非常实用。

编写 LibraryVisitor:挂接输出库的遍历钩子

visitor 让你能挂接到输出库的特定部分。官方文档给出的示例是转换每一个生成的类型:

using Microsoft.TypeSpec.Generator; using Microsoft.TypeSpec.Generator.Providers; public class MyLibraryVisitor : LibraryVisitor { protected override TypeProvider? VisitType(TypeProvider type) { // Inspect or modify the generated type here. return base.VisitType(type); } }

LibraryVisitor的遍历机制(LibraryVisitor.cs)决定了你能钩住哪些环节。其核心流程是:

  1. VisitLibrary(OutputLibrary library)作为入口,遍历library.TypeProviders
  2. 对每个TypeProvider调用VisitTypeCore,其内部依次调用:
    • VisitType(type)—— 对类型本身的钩子(返回null可移除该类型);
    • 遍历typeProvider.MethodsConstructorsPropertiesFields,分别派发VisitMethodVisitConstructorVisitPropertyVisitField
    • 递归处理SerializationProvidersNestedTypes
    • 最后调用PostVisitType(type)收尾。
  3. 通过type.Update(methods, constructors, properties, fields, serializations, nestedTypes)把修改后的成员写回类型。

此外,LibraryVisitor还提供一组PreVisit*钩子(如PreVisitModelPreVisitPropertyPreVisitEnum),它们在TypeFactory创建对应 provider 时提前介入,适合在类型尚未完全构建前调整输入模型到输出的转换结果。

也就是说,一个 visitor 可以在「类型层」「成员层」「转换前」三个维度上修改输出,而VisitType返回null即可实现从生成结果中剔除指定类型的效果。

插件工程文件:如何引用生成器包并构建

插件应构建为类库工程,并引用生成器发布的 NuGet 包。根据 plugins.md,引用Microsoft.TypeSpec.Generator.ClientModel会传递性地引入Microsoft.TypeSpec.Generator(包含GeneratorPlugin基类的那个程序集)。使用的包版本应与生成所用的@typespec/http-client-csharp版本保持一致。一个最小.csproj如下:

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.TypeSpec.Generator.ClientModel" Version="X.Y.Z" /> </ItemGroup> </Project>

编译后得到插件程序集*.dll,供后续plugins选项加载。

配置 plugins 选项:在 tspconfig.yaml 中启用插件

有了插件程序集之后,用tspconfig.yamlplugins选项把 emitter 指向它。根据官方文档与 options.ts 中的 schema 定义,pluginsstring[]类型,每个路径可以是绝对路径,也可以是相对于解析后的emitter-output-dir的相对路径;路径可指向:

  • 一个文件—— 预构建的插件程序集(*.dll);
  • 一个目录—— 其中包含预构建的插件程序集(*.dll)或一个.csproj;当目录中存在.csproj时,生成器会先自动执行dotnet build -c Release构建,再加载产物程序集。

官方示例(加载输出目录下codegen文件夹中的插件):

emit: - "@typespec/http-client-csharp" options: "@typespec/http-client-csharp": plugins: - "codegen/MyPlugin.dll" # file relative to emitter-output-dir - "codegen" # directory containing plugin assemblies - "/abs/path/to/MyPlugin.dll" # absolute path used as-is

需要明确的规则(原文档要点,原文照录并加以印证):

  • 绝对路径按原样使用;相对路径锚定到解析后的emitter-output-dir
  • 目录中存在.csproj时自动构建;否则扫描其中的预构建*.dll文件;
  • 通过plugins选项加载的插件,与通过node_modules自动发现的插件是叠加生效的,两者都会被应用。

在 emitter 侧,路径解析发生在 emitter.ts 中——options["plugins"]中的每一项都会先经resolvePath(outputFolder, p)转换为绝对路径,再写入生成器配置,这正是「相对路径相对emitter-output-dir解析」的具体实现。emitter 的单元测试 options.test.ts 也验证了plugins会被原样传入配置,未设置时不会出现在配置中。

底层加载原理:从 plugins 选项到 Apply 被调用

理解插件如何被真正执行,需要看GeneratorHandler的完整流程(GeneratorHandler.cs):

  1. 装配目录LoadGenerator首先用AppContext.BaseDirectory创建DirectoryCatalog,再调用AddPluginDlls处理node_modules自动发现,随后调用AddConfiguredPluginDlls处理plugins配置项,最终组合成AggregateCatalog并创建 MEFCompositionContainer
  2. 解析配置路径AddConfiguredPluginDlls读取Configuration.PluginPaths(对应 Configuration.cs 中的PluginPaths属性,配置键名为plugins)。对每个路径,若目录存在.csprojBuildPluginIfNeeded后以AssemblyCatalog加载产物;否则枚举目录下所有*.dll逐个尝试作为 MEF 目录加载(无法作为 MEF catalog 加载的 DLL,例如原生 DLL,会被静默跳过);
  3. 自动构建:目录含.csproj时,FindPluginProject会优先选择src目录下的工程(避免误构建测试或示例工程),随后BuildPlugin将工程构建到进程隔离的输出目录(根目录常量typespec-generator-plugins),这样并发运行的不同进程不会共享同一份构建产物——源码注释明确说明这是为了避免并发构建互相覆盖、导致前一次运行的陈旧 DLL 被加载而静默丢失插件行为;若构建失败,则记录诊断码plugin-build-failed(见 DiagnosticCodes.cs)并警告,而不是让整个生成中断;
  4. 触发 Apply:生成器选定(默认ScmCodeModelGenerator)后,SelectGenerator遍历 MEF 装配出的全部Plugins,对每个插件调用plugin.Apply(CodeModelGenerator.Instance)——此时所有注册的 visitors、rewriters、metadata references、shared source 目录都会在正式生成开始前生效。

hosted 模式限制:当IsHosted为真时,AddPluginDlls直接返回(不自动发现),而AddConfiguredPluginDlls会抛出InvalidOperationException("Custom plugins are disabled in hosted mode."),即托管环境中不允许加载自定义插件。

用测试验证插件行为

仓库自带测试可作为插件正确性的参照。单元测试 GeneratorPluginTests.cs 验证了插件最核心的契约——Apply中注册的 visitor 会被加入生成器:

[Test] public void PluginAddsVisitorToGenerator() { var generator = MockHelpers.LoadMockGenerator(); var plugin = new TestGeneratorPlugin(); plugin.Apply(generator.Object); Assert.AreEqual(1, generator.Object.Visitors.Count); Assert.IsInstanceOf<TestLibraryVisitor>(generator.Object.Visitors[0]); }

配套的 TestGeneratorPlugin.cs 展示了插件的最简形态——Apply中只做一件事:generator.AddVisitor(new TestLibraryVisitor())。而 TestLibraryVisitor.cs 则是空实现的LibraryVisitor子类,说明 visitor 的所有Visit*方法都是可选覆写的,默认行为是原样放行。

注意事项与最佳实践小结

综合文档与源码实现,使用插件时有几点值得留意:

  • 版本对齐:插件引用的Microsoft.TypeSpec.Generator.ClientModel版本应匹配生成所用@typespec/http-client-csharp版本,否则可能出现 API 不兼容;
  • 路径语义plugins相对路径相对emitter-output-dir解析,绝对路径原样使用;路径必须存在(指向目录时目录必须存在,否则抛异常);
  • 加载顺序plugins选项与node_modules自动发现是叠加关系,二者发现的插件都会被Apply
  • 托管限制:hosted 模式下自定义插件不可用;
  • 构建时机:目录模式下若检测到.csproj会自动构建(dotnet build -c Release),src目录下的工程会被优先选中,构建产物放在进程隔离目录以避免并发冲突;
  • 移除内置行为:如需移除某个内置 visitor,可借助RemoveVisitor<T>()或按类型名移除的RemoveVisitor(string)

至此,从GeneratorPlugin编写、LibraryVisitor覆写、.csproj构建、tspconfig.yaml配置到 MEF 装配与Apply触发,插件定制的完整链路已经打通。相关细节可继续参考 emitter.md 的 plugins 一节 以及生成器源码 GeneratorHandler.cs 与 CodeModelGenerator.cs。

【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询