Skip to content

练习项目与下一步学习

如果你已经完成了 零基础主教程,接下来最重要的不是立刻去看更多概念,而是把刚建立的理解迁移到新的案例中。

这份文档负责两件事:

  • 帮你判断自己是否已经准备好进入下一阶段
  • 告诉你如何从 sample/01 平滑过渡到 sample/02-09

开始前检查清单

在继续前,请先确认你至少能完成下面这些事:

  • 能解释什么叫“编译时生成代码”
  • 能说清 ToStringGeneratorToStringGenerator.SampleToStringGenerator.Tests 三个工程的分工
  • 能看懂 RegisterPostInitializationOutput(...)AddSource(...)
  • 能说明 ForAttributeWithMetadataName(...) 的作用
  • 能分清语法对象和语义对象
  • 能从 TransformClass(...) 看出类信息和属性信息是怎么提取出来的
  • 能读懂 GenerateToStringCode(...)BuildToStringImplementation(...) 的职责分工

如果你对上面 4 项以上仍然说不清,建议先回到:

如果你已经会主案例,但卡在驱动结果、步骤跟踪、附加文件、诊断这些扩展能力,先补这几份专题再继续往下练:

从 sample/01 到 sample/02-09 的学习路线

这套练习最好的节奏不是“随机挑一个自己喜欢的做”,而是按复杂度逐步加码。

第一阶段:巩固基础生成链路

建议顺序:

  1. sample/01-tostring-generator
  2. sample/02-builder-generator
  3. sample/03-enum-extensions-generator

这一阶段的目标是把下面这些步骤练熟:

  • 固定输出
  • 特性驱动
  • 语法筛选
  • 语义提取
  • 简单代码生成

第二阶段:开始处理更多业务规则

建议顺序:

  1. sample/04-di-registration-generator
  2. sample/05-dto-mapper-generator
  3. sample/06-json-serializer-generator

这一阶段的重点会转向:

  • 聚合多个输入
  • 诊断信息
  • 类型映射
  • 特性参数和常量读取

第三阶段:进入更复杂的生成场景

建议顺序:

  1. sample/07-mvvm-observable-generator
  2. sample/08-validator-generator
  3. sample/09-graphql-query-generator

这一阶段的重点是:

  • 更复杂的语义分析
  • 更严格的错误处理
  • 附加文件输入
  • 多管道组合

sample/02-09 项目概览

项目你会重点练到什么为什么建议在这个位置学
sample/02-builder-generator属性分析、链式方法生成ToString 多一点结构设计,但仍然容易观察输出
sample/03-enum-extensions-generator语法筛选、枚举分析能帮助你巩固“不是所有生成器都必须围绕属性”
sample/04-di-registration-generator接口与聚合开始接触多个目标统一处理
sample/05-dto-mapper-generator类型分析、诊断开始接触“生成失败时要给用户解释原因”
sample/06-json-serializer-generator特性参数、常量读取巩固语义分析和类型处理
sample/07-mvvm-observable-generator字段转属性、通知逻辑结构更复杂,更接近真实业务生成器
sample/08-validator-generator诊断与元数据查找强化错误处理和约束检查
sample/09-graphql-query-generator附加文件、组合管道进入完整的复杂输入场景

sample/02-09 的源码入口

这一节不再只告诉你“去看哪个 sample”,而是直接给出每个项目最适合的第一入口。

统一原则:

  • 先看“示例侧触发代码”
  • 再看“主生成器入口”
  • 最后再看测试和生成结果

sample/02-builder-generator

源码路径:

  • sample/02-builder-generator/BuilderGenerator.Sample/Examples/Product.cs
  • sample/02-builder-generator/BuilderGenerator/BuilderGenerator.cs

关键片段 1:

csharp
[GenerateBuilder]
public partial class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

关键片段 2:

csharp
var classDeclarations = context.SyntaxProvider
    .ForAttributeWithMetadataName(
        fullyQualifiedMetadataName: "BuilderGenerator.GenerateBuilderAttribute",
        predicate: IsClassDeclaration,
        transform: TransformClass);

context.RegisterSourceOutput(classDeclarations, GenerateBuilderCode);

为什么看它:

  • 它和 sample/01 的触发方式最接近,适合先做平滑迁移
  • 你会看到“同样是特性驱动”,输出结果已经从单个方法变成了整套 Builder 结构
  • 这个项目最适合用来练“中间模型一复杂,生成代码结构就会立刻变复杂”

sample/03-enum-extensions-generator

源码路径:

  • sample/03-enum-extensions-generator/SampleApp/Enums.cs
  • sample/03-enum-extensions-generator/EnumExtensionsGenerator/EnumExtensionsGenerator.cs

关键片段 1:

csharp
public enum UserStatus
{
    [Display(Name = "活动", Description = "用户账户处于活动状态,可以正常使用所有功能")]
    Active = 0,
    Inactive = 1
}

关键片段 2:

csharp
var enumDeclarations = context.SyntaxProvider.CreateSyntaxProvider(
    predicate: IsEnumDeclaration,
    transform: TransformEnum
);

为什么看它:

  • 这是你第一次系统接触“不是靠特性找类,而是靠语法形状找目标”
  • 输入对象从类切换成枚举,能帮你摆脱“源生成器就是围绕属性生成代码”的固定印象
  • 这个项目也很适合对照看 SemanticModel.GetDeclaredSymbol(...) 是怎么把 EnumDeclarationSyntax 变成 INamedTypeSymbol

sample/04-di-registration-generator

源码路径:

  • sample/04-di-registration-generator/SampleApp/Services.cs
  • sample/04-di-registration-generator/DiRegistrationGenerator/DiRegistrationGenerator.cs

关键片段 1:

csharp
[Injectable(ServiceLifetime.Singleton)]
public class EmailService : IEmailService
{
    public void SendEmail()
    {
        Console.WriteLine("发送邮件");
    }
}

关键片段 2:

csharp
var serviceProvider = context.SyntaxProvider
    .ForAttributeWithMetadataName(
        fullyQualifiedMetadataName: "Microsoft.Extensions.DependencyInjection.InjectableAttribute",
        predicate: static (node, _) => node is ClassDeclarationSyntax,
        transform: static (context, _) => TransformToServiceInfo(context)
    )
    .Collect();

为什么看它:

  • 这个项目开始要求你把“单个目标类生成代码”升级成“聚合多个目标一起生成注册代码”
  • 你会首次稳定接触“特性构造参数/命名参数的读取”
  • 如果你对依赖注入比较熟,这个案例会特别容易理解生成结果为什么有价值

sample/05-dto-mapper-generator

源码路径:

  • sample/05-dto-mapper-generator/SampleApp/Dtos.cs
  • sample/05-dto-mapper-generator/DtoMapperGenerator/DtoMapperGenerator.cs

关键片段 1:

csharp
[DtoMapperGenerator.MapFrom(typeof(UserEntity))]
public partial class UserDto
{
    public int Id { get; set; }

    [DtoMapperGenerator.MapIgnore]
    public string Password { get; set; } = string.Empty;
}

关键片段 2:

csharp
var combined = context.CompilationProvider
    .Combine(mapFromClasses)
    .Combine(mapToClasses);

context.RegisterSourceOutput(combined, (spc, source) =>
{
    // 提取语义信息、匹配属性并上报诊断
});

为什么看它:

  • 它把“多入口特性”、“属性匹配”、“类型兼容性判断”、“诊断上报”都放到了一起
  • 你会看到生成器不只是读目标类本身,还会跨类型分析源类型和目标类型
  • 如果你想真正理解“为什么生成器离不开 Compilation 和 SemanticModel”,这个项目非常关键

sample/06-json-serializer-generator

源码路径:

  • sample/06-json-serializer-generator/JsonSerializerGenerator.Sample/Product.cs
  • sample/06-json-serializer-generator/JsonSerializerGenerator/MetadataExtractor.cs

关键片段 1:

csharp
[JsonSerializable]
public partial class Product
{
    [JsonProperty(Name = "product_name")]
    public string Name { get; set; } = string.Empty;

    [JsonProperty(Ignore = true)]
    public string InternalCode { get; set; } = string.Empty;
}

关键片段 2:

csharp
var jsonPropertyAttribute = propertySymbol.GetAttributes()
    .FirstOrDefault(a => a.AttributeClass?.Name == "JsonPropertyAttribute");

var (customName, ignore) = ReadJsonPropertyAttribute(jsonPropertyAttribute);
propertyInfo.Ignore = ignore;
propertyInfo.JsonName = DetermineJsonName(propertyInfo.PropertyName, customName);

为什么看它:

  • 这个项目是“读取特性参数和命名参数”的直接练习场
  • 你会看到属性级元数据如何一步步变成最终 JSON 键名和忽略规则
  • 如果你对“Roslyn 读到的特性参数最终怎么影响输出”还不够稳,这个项目最值得多看几遍

sample/07-mvvm-observable-generator

源码路径:

  • sample/07-mvvm-observable-generator/SampleApp/UserViewModel.cs
  • sample/07-mvvm-observable-generator/MvvmObservableGenerator/ObservablePropertyGenerator.cs

关键片段 1:

csharp
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string _firstName = string.Empty;

关键片段 2:

csharp
var fieldDeclarations = context.SyntaxProvider
    .CreateSyntaxProvider(
        predicate: static (node, _) => IsFieldWithAttribute(node),
        transform: static (ctx, _) => GetFieldSymbol(ctx))
    .Where(static field => field is not null);

为什么看它:

  • 这个项目把输入目标从“类”切到了“字段”
  • 你会开始接触字段分组、冲突检查、同一类中多个成员联合生成的情况
  • 它很适合用来理解“语法筛选和语义确认经常是两段式”的原因

sample/08-validator-generator

源码路径:

  • sample/08-validator-generator/SampleApp/Models/User.cs
  • sample/08-validator-generator/ValidatorGenerator/ValidatorGenerator.cs

关键片段 1:

csharp
public partial class User
{
    [Required]
    [StringLength(50, MinimumLength = 2)]
    public string Name { get; set; } = string.Empty;

    [Range(18, 100)]
    public int Age { get; set; }
}

关键片段 2:

csharp
var classDeclarations = context.SyntaxProvider
    .CreateSyntaxProvider(
        predicate: static (node, _) => IsCandidateClass(node),
        transform: static (ctx, _) => GetClassInfoWithDiagnostics(ctx))
    .Where(static result => result.ClassInfo != null || result.Diagnostics.Count > 0);

为什么看它:

  • 这个项目把“验证规则提取”和“诊断收集”放进了同一条管道
  • 你会看到生成器不仅生成 Validate(),还要在输入不合法时主动解释问题
  • 它很适合作为“生成器开始像真实业务规则引擎一样工作”的分界点

sample/09-graphql-query-generator

源码路径:

  • sample/09-graphql-query-generator/SampleApp/SampleApp.csproj
  • sample/09-graphql-query-generator/SampleApp/schema.graphql
  • sample/09-graphql-query-generator/GraphQLGenerator/GraphQLGenerator.cs

关键片段 1:

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

关键片段 2:

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

var allFiles = graphqlFiles.Collect();

为什么看它:

  • 这是你正式进入“输入不再只是 C# 源码”的阶段
  • 它把附加文件、文本解析、诊断、批量输出组合到了一起
  • 如果你已经吃透 sample/01-08,这个项目会让你真正建立“源生成器可以消费任意编译期输入”的认知

三个推荐练习

这些练习都基于 sample/01-tostring-generator,不要求你新建项目。

练习 1:给 GenerateToString 增加一个开关参数

目标:

  • 理解特性参数会如何影响生成逻辑

建议尝试:

  • GenerateToStringAttribute 增加一个布尔参数,例如 IncludeClassName
  • 当它为 false 时,不在最终字符串里输出类名

你会练到:

  • 特性定义改动
  • 特性参数读取
  • 动态输出分支控制

练习 2:忽略某些属性

目标:

  • 学会让生成器根据额外规则过滤成员

建议尝试:

  • 增加一个 [IgnoreToString] 特性
  • 被它标记的属性不参与输出

你会练到:

  • 多特性场景
  • 属性级别语义分析
  • 中间模型扩展

练习 3:更友好地输出集合类型

目标:

  • 提升真实场景下的输出体验

建议尝试:

  • 如果属性是数组或集合,不直接输出类型名
  • 尝试输出元素数量,或拼接元素内容

你会练到:

  • 更细粒度的类型判断
  • 复杂字符串生成策略

学习一个新案例时的固定步骤

每次打开一个新的 sample,都建议用同一套顺序:

  1. 先看示例工程的 .csproj
  2. 再看示例侧的触发代码,例如 Examples/*.csModels/*.csServices.csschema.graphql
  3. 再看主生成器文件
  4. 再看测试工程
  5. 最后看 obj/generated 或运行 Program.cs 观察最终效果

这套顺序能帮你把每个练习都迅速映射回你在 sample/01 中已经建立的理解。

推荐复读的 API 条目

进入下一轮练习前,最值得反复看的条目是:

  • IIncrementalGenerator
  • IncrementalGeneratorInitializationContext
  • SyntaxProvider
  • ForAttributeWithMetadataName(...)
  • GeneratorAttributeSyntaxContext
  • SemanticModel
  • INamedTypeSymbol
  • IPropertySymbol
  • ITypeSymbol
  • SourceProductionContext

对应文档:

继续深入时的建议

  • 不要急着追求“一次写完整所有边界情况”
  • 先做出最小可运行版本,再补 null、泛型、嵌套类、诊断
  • 每次新增一个能力,就补一个测试
  • 如果对某个 API 很模糊,先回到当前 sample 搜它在哪出现,再查手册

一句话路线图

最合理的下一步不是“看更多文档”,而是:

  • sample/01 出发
  • 迁移你的理解到 sample/02sample/03
  • 再逐步进入更复杂的生成器场景

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