Skip to content

扩展专题 3:附加文件与配置输入

这个专题只回答一个问题:如果你的生成器不只读取 C# 源码,而是还要读 .graphql.json、模板文件或项目配置,应该怎么接进增量管道。

本页里的路径都按“仓库根目录相对路径”书写。

什么时候需要看这个专题

当你遇到下面这些需求时,就进入这一层:

  • 读取 .graphql 文件生成客户端代码
  • 读取 .json、模板文件、协议文件生成类型
  • 根据项目配置决定是否生成、生成到哪里、用什么模式

最短理解链路

text
<AdditionalFiles Include="..." />
  -> AdditionalTextsProvider
  -> AdditionalText
  -> GetText(...)

如果进一步接项目配置,链路通常会变成:

text
AnalyzerConfigOptionsProvider
  -> GlobalOptions / GetOptions(...)
  -> TryGetValue(...)

但要先说明清楚一件事:

  • 当前仓库里有真实的 AdditionalFiles 样例
  • 当前仓库里没有完整消费 AnalyzerConfigOptionsProvider 的正式 sample

所以这页会把“附加文件”讲实,“配置输入”讲成带边界的补充说明。

1. 附加文件样例:GraphQL 生成器

这部分最值得对照的就是 sample/09-graphql-query-generator

源码路径 1:示例项目如何声明输入文件

  • sample/09-graphql-query-generator/SampleApp/SampleApp.csproj

关键片段:

xml
<ItemGroup>
  <AdditionalFiles Include="schema.graphql" />
</ItemGroup>

为什么看它:

  • 这一步决定 schema.graphql 能不能进入生成器输入
  • 如果这里不写 AdditionalFiles,生成器里的 AdditionalTextsProvider 通常看不见这个文件
  • 这是“项目侧声明输入”的起点,不是生成器代码,但和生成器是否生效直接相关

源码路径 2:输入文件本身长什么样

  • sample/09-graphql-query-generator/SampleApp/schema.graphql

关键片段:

graphql
type User {
    id: ID!
    name: String!
    email: String!
}

type Query {
    getUser(id: ID!): User!
    getAllUsers: [User!]!
}

为什么看它:

  • 你要先意识到,这里输入给生成器的已经不是 C#,而是另一种文本格式
  • 生成器负责“读取文本并理解它”,而不是编译器天然会理解 .graphql
  • 看到这个文件以后,你会更容易理解为什么生成器侧要先过滤文件、再读文本、再解析

源码路径 3:生成器如何把附加文件接进增量管道

  • sample/09-graphql-query-generator/GraphQLGenerator/GraphQLGenerator.cs

关键片段:

csharp
var graphqlFiles = context.AdditionalTextsProvider
    .Where(file => file.Path.EndsWith(".graphql"));

var allFiles = graphqlFiles.Collect();

context.RegisterSourceOutput(allFiles, (spc, files) =>
{
    GenerateCode(spc, files);
});

为什么看它:

  • AdditionalTextsProvider 是附加文件进入增量管道的入口
  • Where(...) 负责筛出真正要处理的文件类型
  • Collect() 把多个文件合并成一批输入,便于一次性分析整个 schema 集合
  • 这是你从“C# 语法树输入”走向“任意文本输入”的第一步

源码路径 4:生成器如何读取文本并处理错误

  • sample/09-graphql-query-generator/GraphQLGenerator/GraphQLGenerator.cs

关键片段:

csharp
foreach (var file in files)
{
    var text = file.GetText(context.CancellationToken);
    if (text == null)
    {
        continue;
    }

    var content = text.ToString();
    if (string.IsNullOrWhiteSpace(content))
    {
        continue;
    }

    var schema = parser.Parse(content, file.Path);
    if (schema == null)
    {
        diagnosticReporter.ReportSyntaxError(file.Path, "无法解析 GraphQL schema 文件");
        continue;
    }
}

为什么看它:

  • GetText(...) 是从 AdditionalText 读取文本内容的关键方法
  • 这里顺手展示了一个真实工程里很常见的顺序:
    • 先读文本
    • 再跳过空输入
    • 再解析
    • 失败时记录诊断
  • 和主案例相比,这里多了一层“文件格式解析”,这正是附加文件场景的真实复杂度

源码路径 5:读取完文本后如何输出源码

  • sample/09-graphql-query-generator/GraphQLGenerator/GraphQLGenerator.cs

关键片段:

csharp
var code = generator.GenerateModel(type, rootNamespace, typeMapper, knownTypes, circularTypes);
context.AddSource($"{type.Name}.g.cs", SourceText.From(code, Encoding.UTF8));

为什么看它:

  • 这说明附加文件输入最终仍然会回到普通的 AddSource(...) 输出
  • 也就是说,输入通道变复杂了,但输出通道没有本质变化
  • 你会更清楚“读取额外输入”和“生成 .g.cs”其实是两段不同职责

2. 当前仓库里关于配置输入的现实情况

这一点需要明确说清楚:

  • 当前仓库没有正式 sample 展示 context.AnalyzerConfigOptionsProvider
  • 所以这里不能假装它已经有完整实战案例

也就是说,这一页里“附加文件”有真实源码支撑,但“配置输入”暂时只有模式说明,没有配套 sample 可对照。

3. 配置输入的最短写法

如果你后续自己要补一个配置输入生成器,最短模式通常长这样:

csharp
var options = context.AnalyzerConfigOptionsProvider.GlobalOptions;

if (options.TryGetValue("build_property.RootNamespace", out var rootNamespace))
{
    // 使用 rootNamespace
}

你应该重点记住这几点:

  • AnalyzerConfigOptionsProvider 是配置入口
  • GlobalOptions 用于读取全局项目属性
  • TryGetValue(...) 读出来的通常是字符串
  • 你需要自己做布尔、整数、枚举等转换

4. 如果你以后要补一个真实配置输入 sample,优先看什么

建议按下面顺序设计:

  1. .csproj 里先定义一个明确的 MSBuild 属性
  2. 在生成器里通过 AnalyzerConfigOptionsProvider.GlobalOptions 读取它
  3. 把读取结果接入增量管道,而不是在输出阶段到处散读
  4. 用测试验证“不同配置值 -> 不同生成结果”

也就是说,配置输入不是另一个完全独立的体系,它只是“另一类输入源”。

5. 最容易混淆的点

  • 普通项目文件不等于 AdditionalFiles
  • AdditionalTextsProvider 负责接入文件,不负责自动理解文件含义
  • GetText(...) 读出来的是文本,不是语法树
  • 当前仓库没有正式演示 AnalyzerConfigOptionsProvider 的 sample,不要把理论示例误当成现有实现

6. 最推荐的配套文档

基于当前仓库文档副本构建的 VitePress 站点