Appearance
练习项目与下一步学习
如果你已经完成了 零基础主教程,接下来最重要的不是立刻去看更多概念,而是把刚建立的理解迁移到新的案例中。
这份文档负责两件事:
- 帮你判断自己是否已经准备好进入下一阶段
- 告诉你如何从
sample/01平滑过渡到sample/02-09
开始前检查清单
在继续前,请先确认你至少能完成下面这些事:
- 能解释什么叫“编译时生成代码”
- 能说清
ToStringGenerator、ToStringGenerator.Sample、ToStringGenerator.Tests三个工程的分工 - 能看懂
RegisterPostInitializationOutput(...)和AddSource(...) - 能说明
ForAttributeWithMetadataName(...)的作用 - 能分清语法对象和语义对象
- 能从
TransformClass(...)看出类信息和属性信息是怎么提取出来的 - 能读懂
GenerateToStringCode(...)和BuildToStringImplementation(...)的职责分工
如果你对上面 4 项以上仍然说不清,建议先回到:
如果你已经会主案例,但卡在驱动结果、步骤跟踪、附加文件、诊断这些扩展能力,先补这几份专题再继续往下练:
从 sample/01 到 sample/02-09 的学习路线
这套练习最好的节奏不是“随机挑一个自己喜欢的做”,而是按复杂度逐步加码。
第一阶段:巩固基础生成链路
建议顺序:
sample/01-tostring-generatorsample/02-builder-generatorsample/03-enum-extensions-generator
这一阶段的目标是把下面这些步骤练熟:
- 固定输出
- 特性驱动
- 语法筛选
- 语义提取
- 简单代码生成
第二阶段:开始处理更多业务规则
建议顺序:
sample/04-di-registration-generatorsample/05-dto-mapper-generatorsample/06-json-serializer-generator
这一阶段的重点会转向:
- 聚合多个输入
- 诊断信息
- 类型映射
- 特性参数和常量读取
第三阶段:进入更复杂的生成场景
建议顺序:
sample/07-mvvm-observable-generatorsample/08-validator-generatorsample/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.cssample/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.cssample/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.cssample/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.cssample/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.cssample/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.cssample/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.cssample/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.csprojsample/09-graphql-query-generator/SampleApp/schema.graphqlsample/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,都建议用同一套顺序:
- 先看示例工程的
.csproj - 再看示例侧的触发代码,例如
Examples/*.cs、Models/*.cs、Services.cs、schema.graphql - 再看主生成器文件
- 再看测试工程
- 最后看
obj/generated或运行Program.cs观察最终效果
这套顺序能帮你把每个练习都迅速映射回你在 sample/01 中已经建立的理解。
推荐复读的 API 条目
进入下一轮练习前,最值得反复看的条目是:
IIncrementalGeneratorIncrementalGeneratorInitializationContextSyntaxProviderForAttributeWithMetadataName(...)GeneratorAttributeSyntaxContextSemanticModelINamedTypeSymbolIPropertySymbolITypeSymbolSourceProductionContext
对应文档:
继续深入时的建议
- 不要急着追求“一次写完整所有边界情况”
- 先做出最小可运行版本,再补 null、泛型、嵌套类、诊断
- 每次新增一个能力,就补一个测试
- 如果对某个 API 很模糊,先回到当前 sample 搜它在哪出现,再查手册
一句话路线图
最合理的下一步不是“看更多文档”,而是:
- 从
sample/01出发 - 迁移你的理解到
sample/02和sample/03 - 再逐步进入更复杂的生成器场景