- 后端
- 代码生成
- 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.
导读
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,它做了三件事:
- 收集元数据引用:从
TRUSTED_PLATFORM_ASSEMBLIES读取运行时程序集路径(排除宿主自身目录下的程序集),再显式加入输出目录中的Refit.dll与Refit.Xml.dll,组成编译器的MetadataReference集合; - 固定解析选项:
CSharpParseOptions使用LanguageVersion.CSharp14,保证演示代码以 C# 14 语法解析; - 构建可编译单元:
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方法):
- 用
AdhocWorkspace创建一个名为FixDemo的 C# 工程与文档Api.cs; - 取该文档对应的
Compilation,调用AnalyzerSample.DiagnoseAsync收集诊断,找到目标 ID(RF003 或 RF005)对应的Diagnostic; - 实例化
RefitInterfaceCodeFixProvider,用CodeFixContext触发RegisterCodeFixesAsync收集CodeAction; - 执行
actions[0].GetOperationsAsync,从操作中提取ApplyChangesOperation,拿到修复后的Document; - 重新编译修复后的文档:必须无错误,且重新跑分析器后目标诊断 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] | RF006 | multipart 表单对象展平会回退到反射构建器,与纯生成注册不兼容 |
直接new Refit.JsonContentSerializer() | CS0619(错误) | 构造器级别过时,直接构造即编译错误 |
直接引用BodySerializationMethod.Json | CS0618(警告) | 旧 JSON 请求体枚举成员保留但过时 |
读写Refit.XmlReaderWriterSettings.AllowDtdProcessing | CS0618 × 2 | XML 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 与源码,这套示例沉淀出几条值得迁移的实践:
- 用源码工程引用而非 NuGet 包:Tooling 工程通过
ProjectReference直连生成器/分析器/修复器/Refit 本体,便于在仓库内验证最新源码行为;对外部使用者而言,等价做法是引用 Refit 的 NuGet 包并读取其分析器/生成器程序集。 - 新编译器宿主 + 旧发布组件:宿主可用 Roslyn 5.0.0 解析 C# 14,而 Refit 发布组件仍以 Roslyn 4.8 编译、.NET Standard 2.0 为目标,二者解耦,这正是编译期组件向后兼容的典型架构。
- 程序化断言取代人工检查:每个示例都用
Check.Require把“行为契约”变成进程退出码级别的失败信号,适合接入 CI 作为编译期回归测试。 - 注意 AOT 边界:该主机需要普通 .NET 运行时和编译器元数据文件,不是 Native AOT 场景(
IsAotCompatible=false);若要验证 AOT 兼容,应参考仓库中的 Refit.NativeAotSmoke 工程。 - 诊断与修复的“可证明闭环”:分析器暴露 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.
相关推荐
Humanizer.Analyzers:v2 到 v3 迁移的 Roslyn 分析器与代码修复实战指南
Humanizer.Analyzers:v2 到 v3 迁移的 Roslyn 分析器与代码修复实战指南 Humanizer v3 将原本散落在 Humanize
开发工具MessagePack-CSharp代码生成器原理:Roslyn编译器集成技术
MessagePack CSharp代码生成器原理:Roslyn编译器集成技术 想要了解MessagePack CSharp代码生成器如何实现高效的序列化性能吗
序列化后端Refit源生成器技术揭秘:编译时代码生成的黑科技
Refit源生成器技术揭秘:编译时代码生成的黑科技 Refit 是一个为.NET生态系统设计的革命性REST客户端库,它通过接口声明的方式自动生成HTTP AP
后端代码生成API设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考