☰
深入 Refit 编译器工具链:用 Roslyn 驱动源码生成器、分析器与代码修复的实战指南
2026/9/25 1:17:32 网站建设 项目流程
  • 后端
  • 代码生成
  • API设计

【免费下载链接】refit

The automatic type-safe REST library for .NET. Refit turns a REST API into a C# interface and generates the HttpClient implementation, with support for HttpClientFactory, pluggable serializers and a testing package.

项目地址:https://gitcode.com/gh_mirrors/re/refit
点击查看免费下载

导读

Refit 不仅是一个把 REST API 变成 C# 接口的运行时库,还内置了一整套基于 Roslyn 的编译期工具链:源生成器(Source Generator)、诊断分析器(Analyzer)与代码修复器(Code Fix Provider)。本文以仓库中的 Tooling 示例工程 为主体,讲解如何像调用普通库一样在 .NET 10 + C# 14 主机中直接驱动InterfaceStubGeneratorV2与RefitInterfaceAnalyzer,验证生成代码、RF003 路由诊断、RF003/RF005 代码修复、标识符辅助类型以及公共Index/Rangepolyfill,并读懂 Refit 保留 API 的兼容性诊断(CS0618/CS0619/RF006)。读完本文,你将掌握 Refit 编译器组件的运行方式、项目引用组织技巧与一套可直接复用的编译期验证脚手架。

一、示例工程概览:编译器工具链的“体检”沙箱

仓库中的src/examples/Documentation/Tooling/是一个专门用于演示 Refit 编译器组件的可执行工程。它不做任何 HTTP 请求,而是直接运行编译器工具本身,因此被 README 明确称为 “Compiler tooling samples”。

工程核心信息(来自 Tooling.csproj):

  • 目标框架net10.0,语言版本LangVersion=14.0(C# 14);
  • 引用 Roslyn 5.0.0 的Microsoft.CodeAnalysis.CSharp与Microsoft.CodeAnalysis.CSharp.Workspaces(通过VersionOverride覆盖);
  • 通过ProjectReference直接引用 Refit 源码工程:Refit.csproj、Refit.Xml.csproj、InterfaceStubGenerator.Roslyn48.csproj、Refit.Analyzers.Roslyn48.csproj、Refit.CodeFixes.Roslyn48.csproj;
  • IsAotCompatible为false:该主机依赖普通 .NET 运行时与编译器元数据文件,并非 Native AOT 示例。

这里需要理解一个版本分层设计:Refit 对外发布的编译器组件本身以 .NET Standard 2.0 为目标、针对 Roslyn 4.8 编译,而示例主机使用更新的 Roslyn 5.0.0 来解析 C# 14 语法。主机的新版本编译器并不会改变那些发布组件,恰好用来验证“老组件 + 新宿主”的兼容性。

工程结构非常清晰,每个.cs文件对应一类验证职责:

文件验证内容
Program.cs依次驱动全部示例并输出Tooling samples passed.
ToolingCompilation.cs构建带运行时与 Refit 元数据引用的 C# 14 编译单元
GeneratorSample.cs用CSharpGeneratorDriver驱动源生成器
AnalyzerSample.cs用Compilation.WithAnalyzers驱动分析器并检查 RF003
CodeFixSample.cs注册代码动作并应用 RF003/RF005 修复
CompatibilitySample.cs探测保留 API 的 CS0618/CS0619/RF006 兼容性诊断
NameSample.cs验证UniqueNameBuilder与WellKnownTypes
PolyfillSample.cs分别验证生成器/分析器各自携带的System.Index、System.Range
Check.cs把失败的断言变成可执行失败(InvalidOperationException)

二、构建与运行:两条命令验证整套工具链

README 给出了从仓库根目录执行的完整命令。注意工作目录是src/,不是仓库根目录:

dotnet build examples/Documentation/Tooling/Tooling.csproj -c Release -p:LangVersion=14.0 dotnet run --project examples/Documentation/Tooling/Tooling.csproj -c Release --no-build

第一条命令以 Release 配置显式指定 C# 14 语言版本进行构建;第二条使用--no-build直接运行,避免重复编译。正常运行结束时,Program.cs 会打印Tooling samples passed.。任何一步演示契约未满足,都会由 Check.Require 抛出InvalidOperationException使进程失败——这是一种把“编译期工具的行为验证”转化为“可执行断言”的典型做法。

三、宿主编译脚手架:如何为演示准备编译器输入

所有示例共享同一个编译构建工具类 ToolingCompilation.cs,它做了三件事:

  1. 收集元数据引用:从TRUSTED_PLATFORM_ASSEMBLIES读取运行时程序集路径(排除宿主自身目录下的程序集),再显式加入输出目录中的Refit.dll与Refit.Xml.dll,组成编译器的MetadataReference集合;
  2. 固定解析选项:CSharpParseOptions使用LanguageVersion.CSharp14,保证演示代码以 C# 14 语法解析;
  3. 构建可编译单元:Create(source)将源代码字符串解析为语法树,并以OutputKind.DynamicallyLinkedLibrary、可空上下文启用(NullableContextOptions.Enable)创建名为ToolingDemonstration的库编译;RequireNoErrors则收集所有DiagnosticSeverity.Error,一旦存在就抛出携带实际编译器错误的异常。

这个脚手架的意义在于:它把“粘贴一段接口代码到 IDE 里看诊断”变成“在任意 .NET 进程里程序化地编译并断言结果”,是编写编译期测试或文档示例的通用模板。

四、驱动源生成器:GeneratorSample 与 RF006 生成后编译验证

GeneratorSample.cs 演示了如何驱动 Refit 源生成器。核心链路如下:

extern alias GeneratorTooling; using InterfaceStubGeneratorV2 = GeneratorTooling::Refit.Generator.InterfaceStubGeneratorV2; CSharpCompilation compilation = ToolingCompilation.Create(source); InterfaceStubGeneratorV2 generator = new(); GeneratorDriver driver = CSharpGeneratorDriver.Create( [generator.AsSourceGenerator()], parseOptions: new(LanguageVersion.CSharp14)); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation generated, out _); GeneratorDriverRunResult result = driver.GetRunResult();

关键点:

  • 演示输入是一个极简的 Refit 接口:[Refit.Get("/people/{id}")] Task<string> GetAsync(int id);
  • generator.AsSourceGenerator()将InterfaceStubGeneratorV2包装为可被驱动执行的ISourceGenerator(对应 README 所说的 “A generator driver callsInterfaceStubGeneratorV2.Initialize”);
  • 驱动执行后,代码断言result.GeneratedTrees非空(生成器确实产出了客户端代码),且generated编译无错误;
  • 更严格的身份校验:通过generated.GetTypeByMetadataName同时找到原始契约IPeople与生成实现Refit.Implementation.GeneratedToolingDemonstration+IPeople,再用SymbolEqualityComparer.Default遍历生成类型的AllInterfaces,确认生成客户端实现了编译输入中的真实接口。

这印证了 Refit 生成代码的命名约定:实现类位于Refit.Implementation命名空间下,以 “GeneratedToolingDemonstration+ 接口名” 形式呈现(本例演示主机下为GeneratedToolingDemonstration+IPeople)。从源码结构看,该命名由 InterfaceStubGeneratorV2.cs 与 Parser.GeneratedNaming.cs 共同决定。

五、驱动分析器:AnalyzerSample 与 RF003 路由诊断

AnalyzerSample.cs 演示了分析器的运行与诊断清单校验。演示输入刻意使用了反斜杠路由:

[Refit.Get(@"\people")] System.Threading.Tasks.Task<string> GetAsync();

运行方式同样是程序化驱动:

RefitInterfaceAnalyzer analyzer = new(); ImmutableArray<Diagnostic> diagnostics = await compilation .WithAnalyzers([analyzer]) .GetAnalyzerDiagnosticsAsync();

结果断言反斜杠路由必须产生RF003(InvalidRouteBackslash,即 “Refit route contains a backslash”)。更重要的是一次“清单完整性”校验:analyzer.SupportedDiagnostics必须恰好暴露 9 个诊断 ID,即:

  • RF001:Refit 成员缺少合法 HTTP 方法特性(InvalidRefitMember);
  • RF003:路由含反斜杠(InvalidRouteBackslash);
  • RF004:方法声明了多个 CancellationToken(MultipleCancellationTokens);
  • RF005:[HeaderCollection]参数类型不受支持(InvalidHeaderCollectionParameter);
  • RF006:方法回退到反射请求构建器、与纯生成注册不兼容(GeneratedRequestBuildingFallback);
  • RF008:声明了多个[HeaderCollection]参数(MultipleHeaderCollections);
  • RF009:声明了多个[Authorize]参数(MultipleAuthorizeParameters);
  • RF011:声明了多个[Body]参数(MultipleBodyParameters);
  • RF012:multipart 方法同时声明了[Body]参数(MultipartBodyParameter)。

这些 ID 与语义在 Refit.Analyzers.Shared/DiagnosticIds.cs 中一一对应,示例同时断言所有诊断默认都是Warning且默认启用(DefaultSeverity == DiagnosticSeverity.Warning && IsEnabledByDefault)。这也是 README 中 “checks RF003 route analysis” 的落地细节。

六、驱动代码修复:CodeFixSample 中 RF003 与 RF005 的完整修复闭环

CodeFixSample.cs 演示了比分析更进一步的“修复闭环”:先制造错误代码,再让RefitInterfaceCodeFixProvider产出修复动作,最后验证修复后的编译既无错误也不再出现原诊断。

工作流程(FixAsync方法):

  1. 用AdhocWorkspace创建一个名为FixDemo的 C# 工程与文档Api.cs;
  2. 取该文档对应的Compilation,调用AnalyzerSample.DiagnoseAsync收集诊断,找到目标 ID(RF003 或 RF005)对应的Diagnostic;
  3. 实例化RefitInterfaceCodeFixProvider,用CodeFixContext触发RegisterCodeFixesAsync收集CodeAction;
  4. 执行actions[0].GetOperationsAsync,从操作中提取ApplyChangesOperation,拿到修复后的Document;
  5. 重新编译修复后的文档:必须无错误,且重新跑分析器后目标诊断 ID 不再出现。

示例覆盖了两个可修复 ID:

  • RF003 修复:输入是AnalyzerSample.BrokenRoute(反斜杠路由),修复应把@"\people"中的反斜杠规范化;
  • RF005 修复:输入是[HeaderCollection] string headers(不支持的类型),修复应给出合法写法。

同时断言provider.FixableDiagnosticIds恰好包含且仅包含RF003与RF005两个 ID,且GetFixAllProvider()非空——即该修复器支持 “Fix All”。这正是 README “both RF003/RF005 corrections” 的含义,对应的实现位于 Refit.CodeFixes.Shared/RefitInterfaceCodeFixProvider.cs。

七、兼容性探测:CompatibilitySample 中保留 API 的诊断契约

Refit 在演进过程中保留了一批旧 API,但通过[Obsolete]或[Obsolete(..., error: true)]显式标记。CompatibilitySample 把“探测这些诊断是否仍然存在”做成了 5 个独立断言(对应 CompatibilitySample.cs):

探测目标预期诊断说明
[AttachmentName("sent.bin")]直接用于 multipart 参数CS0618(警告)旧附件命名特性被保留但标记过时
默认生成的客户端使用[FormObject]RF006multipart 表单对象展平会回退到反射构建器,与纯生成注册不兼容
直接new Refit.JsonContentSerializer()CS0619(错误)构造器级别过时,直接构造即编译错误
直接引用BodySerializationMethod.JsonCS0618(警告)旧 JSON 请求体枚举成员保留但过时
读写Refit.XmlReaderWriterSettings.AllowDtdProcessingCS0618 × 2XML DTD 选择加入/退出同时保留警告,需恰好出现 2 个

注意 README 强调的表述:CheckXmlDtdWarning对读写两处访问各产生一次 CS0618,因此断言warnings == accessorCount(2 次);而CheckJsonContentSerializer是 CS0619 错误级诊断,RequireNoErrors之前先单独捕获它,避免误判。这些都是“预期内的发现”——编译器探测工具专门用来暴露它们,而工程本身在无抑制、无警告的条件下正常构建。

八、标识符与类型辅助:NameSample 中的 UniqueNameBuilder 与 WellKnownTypes

生成器内部有两个重要的“名称与类型”辅助工具,NameSample.cs 直接对它们做了契约级验证(实现见 UniqueNameBuilder.cs 与 WellKnownTypes.cs)。

UniqueNameBuilder的行为验证:

  • 先Reserve("client"),再Reserve(["client0", "response"])(同时验证单个与枚举两种重载);
  • New("client")必须跳过已保留的名字,得到client1;
  • 已返回的名字会继续被保留,因此下一次New("client")得到client2;
  • 枚举形式的保留同样占用原名,所以New("response")得到response0;
  • 大小写敏感:New("Client")原样返回Client。

WellKnownTypes的查找行为验证:

  • Get(typeof(string))通过编译元数据名返回System.String符号;
  • TryGet("Demo.Missing")对不存在的类型返回null,且重复查询保持null结果;
  • 两种查询共享同一缓存符号(用ReferenceEquals断言);
  • 必得查询Get对编译引用中不存在的类型(如NameSample自身)抛出InvalidOperationException,消息为Could not get type ...;
  • 对没有元数据全名的运行时类型(如List<>的类型参数)同样拒绝,消息为Could not get name of type ...。

这些细节保证了生成器在生成命名与类型解析时行为可预期,也是生成代码能稳定命名的底层支撑。

九、多副本 Polyfill:PolyfillSample 中 Index/Range 的隔离验证

这是示例中最“精巧”的一部分。Refit 的生成器与分析器各自内置了公共System.Index、System.Range类型副本(用于在 .NET Standard 2.0 目标下使用切片语法)。Tooling 工程通过项目引用别名把它们隔离开:

<ProjectReference Include="../../../InterfaceStubGenerator.Roslyn48/InterfaceStubGenerator.Roslyn48.csproj" Aliases="GeneratorTooling" /> <ProjectReference Include="../../../Refit.Analyzers.Roslyn48/Refit.Analyzers.Roslyn48.csproj" Aliases="AnalyzerTooling" />

在 PolyfillSample.cs 中:

extern alias GeneratorTooling; extern alias AnalyzerTooling; using AnalyzerIndex = AnalyzerTooling::System.Index; using AnalyzerRange = AnalyzerTooling::System.Range; using GeneratorIndex = GeneratorTooling::System.Index; using GeneratorRange = GeneratorTooling::System.Range;

extern alias让这两个公共类型副本既互相隔离,又与 .NET 运行时的System.Index/System.Range隔离。普通应用程序直接使用运行时类型,而这里验证的是编译器组件自身携带的那一份。

对四个身份(Generator 的 Index/Range 与 Analyzer 的 Index/Range)逐一验证:

  • Index 契约:构造参数(位置值与fromEnd)被保留;GetOffset(length)从前端/后端计算偏移(如new Index(1, fromEnd: true).GetOffset(4) == 3);Start/End描述序列边界(GetOffset(4)分别为 0 与 4);隐式转换Index←int;相等性、GetHashCode、ToString都按存储字段描述;
  • Range 契约:构造参数保留两端点;All覆盖序列两端;StartAt/EndAt工厂保留指定端点;值相等性按两端点比较;ToString包含 “Range” 字样;
  • 负值与跨副本隔离:两副本都保留负数构造值(new Index(-1, fromEnd: true)的Value == -1);但生成器的Index与分析器的Index用Equals(object)互相比较时必然为false,GeneratorRange.All.Equals((object)AnalyzerRange.All)同样为false——类型是不同身份。

这段验证直接对应 README 的说明:extern alias保证两个组件各自携带的公共System.Index/System.Range副本互不干扰,polyfill 示例解释了编译工具面(compiled tooling surface)的构成。

十、设计要点回顾:把文档示例转化为可复用实践

综合 README 与源码,这套示例沉淀出几条值得迁移的实践:

  1. 用源码工程引用而非 NuGet 包:Tooling 工程通过ProjectReference直连生成器/分析器/修复器/Refit 本体,便于在仓库内验证最新源码行为;对外部使用者而言,等价做法是引用 Refit 的 NuGet 包并读取其分析器/生成器程序集。
  2. 新编译器宿主 + 旧发布组件:宿主可用 Roslyn 5.0.0 解析 C# 14,而 Refit 发布组件仍以 Roslyn 4.8 编译、.NET Standard 2.0 为目标,二者解耦,这正是编译期组件向后兼容的典型架构。
  3. 程序化断言取代人工检查:每个示例都用Check.Require把“行为契约”变成进程退出码级别的失败信号,适合接入 CI 作为编译期回归测试。
  4. 注意 AOT 边界:该主机需要普通 .NET 运行时和编译器元数据文件,不是 Native AOT 场景(IsAotCompatible=false);若要验证 AOT 兼容,应参考仓库中的 Refit.NativeAotSmoke 工程。
  5. 诊断与修复的“可证明闭环”:分析器暴露 9 个默认警告级诊断,修复器只对 RF003/RF005 提供修复并支持 Fix All;兼容性 API 则通过 CS0618/CS0619/RF006 在编译期显式提示迁移路径(详见 docs/breaking-changes.md)。

结语

本文从 Tooling 示例 出发,逐步拆解了 Refit 编译器工具链的完整驱动方式:从构建运行命令、宿主编译脚手架,到生成器驱动、分析器诊断清单(RF001–RF012)、代码修复闭环(RF003/RF005)、兼容性探测(CS0618/CS0619/RF006)、标识符与类型辅助,以及双组件多副本Index/Range的隔离验证。无论你是想为 Refit 贡献编译期能力、还是希望在自己的项目里用类似手法验证 Roslyn 组件,这套示例都提供了一份结构清晰、可直接运行的参考实现。

  • 后端
  • 代码生成
  • API设计

【免费下载链接】refit

The automatic type-safe REST library for .NET. Refit turns a REST API into a C# interface and generates the HttpClient implementation, with support for HttpClientFactory, pluggable serializers and a testing package.

项目地址:https://gitcode.com/gh_mirrors/re/refit
点击查看免费下载

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

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

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

立即咨询