Skip to content

Hello World 示例

项目位置

01-HelloWorld/

学习目标

  • 创建最简单的源生成器
  • 理解 ISourceGenerator 接口
  • 学习如何添加生成的源文件

项目结构

01-HelloWorld/
├── HelloWorld.Generator/      # 生成器项目
│   └── HelloWorldGenerator.cs
└── HelloWorld.Consumer/       # 使用生成器的项目
    └── Program.cs

生成器代码

csharp
using Microsoft.CodeAnalysis;

namespace HelloWorld.Generator
{
    [Generator]
    public class HelloWorldGenerator : ISourceGenerator
    {
        public void Initialize(GeneratorInitializationContext context)
        {
            // 初始化代码(本示例中为空)
        }

        public void Execute(GeneratorExecutionContext context)
        {
            // 生成的源代码
            var sourceCode = @"
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""Hello from Source Generator!"";
    }
}";

            // 添加源文件到编译
            context.AddSource("HelloWorld.g.cs", sourceCode);
        }
    }
}

使用生成的代码

csharp
using System;
using Generated;

class Program
{
    static void Main()
    {
        // 调用生成器生成的方法
        Console.WriteLine(HelloWorld.SayHello());
        // 输出: Hello from Source Generator!
    }
}

运行示例

bash
cd 01-HelloWorld
dotnet build
dotnet run --project HelloWorld.Consumer

关键概念

1. [Generator] 特性

标记类为源生成器:

csharp
[Generator]
public class HelloWorldGenerator : ISourceGenerator

2. ISourceGenerator 接口

必须实现两个方法:

  • Initialize - 初始化生成器
  • Execute - 执行代码生成

3. AddSource 方法

添加生成的源文件:

csharp
context.AddSource(
    "HelloWorld.g.cs",  // 文件名(必须唯一)
    sourceCode          // 源代码内容
);

生成的文件

编译后,可以在 obj/ 目录下找到生成的文件:

obj/Debug/net8.0/generated/
└── HelloWorld.Generator/
    └── HelloWorld.Generator.HelloWorldGenerator/
        └── HelloWorld.g.cs

常见问题

Q: 为什么看不到生成的代码?

A: 生成的代码在 obj/ 目录下,IDE 可能不会自动显示。可以:

  1. 在 Solution Explorer 中启用"显示所有文件"
  2. 使用 dotnet build -v detailed 查看详细输出

Q: 如何调试生成器?

A: 参考 调试和诊断 文档。

下一步

项目结构详解

完整目录结构

01-HelloWorld/
├── HelloWorld.slnx                    # 解决方案文件
├── HelloWorld.Generator/              # 生成器项目
│   ├── HelloWorld.Generator.csproj    # 项目文件
│   └── HelloWorldGenerator.cs         # 生成器实现
└── HelloWorld.Consumer/               # 消费者项目
    ├── HelloWorld.Consumer.csproj     # 项目文件
    └── Program.cs                     # 主程序

项目结构图


完整项目配置

生成器项目配置

HelloWorld.Generator.csproj:

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <!-- 目标框架:必须是 .NET Standard 2.0 -->
    <TargetFramework>netstandard2.0</TargetFramework>
    
    <!-- 语言版本 -->
    <LangVersion>latest</LangVersion>
    
    <!-- 不生成引用程序集 -->
    <GenerateReferenceAssemblyAttribute>false</GenerateReferenceAssemblyAttribute>
    
    <!-- 启用可空引用类型 -->
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <!-- Roslyn 核心包 -->
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
  </ItemGroup>

</Project>

关键配置说明

  1. TargetFramework: 必须是 netstandard2.0

    • 源生成器在编译时运行
    • 必须兼容所有 .NET 版本
  2. PrivateAssets="all":

    • 防止 Roslyn 包传递到消费者项目
    • 减少依赖冲突
  3. GenerateReferenceAssemblyAttribute:

    • 避免生成不必要的引用程序集

消费者项目配置

HelloWorld.Consumer.csproj:

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <!-- 目标框架:可以是任何 .NET 版本 -->
    <TargetFramework>net8.0</TargetFramework>
    <OutputType>Exe</OutputType>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <!-- 引用生成器项目 -->
    <ProjectReference Include="..\HelloWorld.Generator\HelloWorld.Generator.csproj"
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />
  </ItemGroup>

  <!-- 可选:输出生成的文件到指定目录 -->
  <PropertyGroup>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
  </PropertyGroup>

  <!-- 可选:将生成的文件包含到项目中 -->
  <ItemGroup>
    <Compile Remove="Generated/**/*.cs" />
  </ItemGroup>

</Project>

关键配置说明

  1. OutputItemType="Analyzer":

    • 将生成器作为分析器引用
    • 这是最重要的配置
  2. ReferenceOutputAssembly="false":

    • 不引用生成器的输出程序集
    • 只使用生成器的代码生成功能
  3. EmitCompilerGeneratedFiles:

    • 将生成的文件输出到指定目录
    • 方便查看和调试

生成器代码详解

完整生成器实现

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

namespace HelloWorld.Generator
{
    /// <summary>
    /// Hello World 源生成器
    /// 这是最简单的源生成器示例,演示基本的代码生成流程
    /// </summary>
    [Generator]
    public class HelloWorldGenerator : ISourceGenerator
    {
        /// <summary>
        /// 初始化方法
        /// 在生成器首次加载时调用一次
        /// 可以在这里注册语法接收器或其他初始化逻辑
        /// </summary>
        /// <param name="context">初始化上下文</param>
        public void Initialize(GeneratorInitializationContext context)
        {
            // 本示例中不需要初始化逻辑
            // 在更复杂的生成器中,可以在这里:
            // 1. 注册语法接收器:context.RegisterForSyntaxNotifications(...)
            // 2. 注册后处理器:context.RegisterForPostInitialization(...)
            
            #if DEBUG
            // 调试时可以附加调试器
            // System.Diagnostics.Debugger.Launch();
            #endif
        }

        /// <summary>
        /// 执行方法
        /// 每次编译时都会调用
        /// 在这里生成源代码并添加到编译中
        /// </summary>
        /// <param name="context">执行上下文</param>
        public void Execute(GeneratorExecutionContext context)
        {
            // 1. 生成源代码
            var sourceCode = GenerateHelloWorldClass();
            
            // 2. 将源代码添加到编译中
            // 参数1: 文件名(必须唯一,建议使用 .g.cs 后缀)
            // 参数2: 源代码内容
            context.AddSource(
                hintName: "HelloWorld.g.cs",
                sourceText: SourceText.From(sourceCode, Encoding.UTF8)
            );
            
            // 可以添加多个源文件
            // context.AddSource("AnotherFile.g.cs", anotherSourceCode);
        }

        /// <summary>
        /// 生成 HelloWorld 类的源代码
        /// </summary>
        /// <returns>生成的 C# 源代码</returns>
        private string GenerateHelloWorldClass()
        {
            // 使用字符串插值生成代码
            // 注意:实际项目中建议使用 StringBuilder 或代码生成库
            return @"
// <auto-generated/>
// 此文件由 HelloWorldGenerator 自动生成
// 生成时间: " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + @"

namespace Generated
{
    /// <summary>
    /// 自动生成的 Hello World 类
    /// </summary>
    public static class HelloWorld
    {
        /// <summary>
        /// 返回问候消息
        /// </summary>
        /// <returns>问候消息字符串</returns>
        public static string SayHello()
        {
            return ""Hello from Source Generator!"";
        }

        /// <summary>
        /// 返回带名字的问候消息
        /// </summary>
        /// <param name=""name"">名字</param>
        /// <returns>个性化问候消息</returns>
        public static string SayHello(string name)
        {
            return $""Hello {name} from Source Generator!"";
        }

        /// <summary>
        /// 返回生成器信息
        /// </summary>
        /// <returns>生成器信息</returns>
        public static string GetGeneratorInfo()
        {
            return ""Generated by HelloWorldGenerator at " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + @""";
        }
    }
}
";
        }
    }
}

代码生成技巧

1. 使用 StringBuilder 生成复杂代码

csharp
private string GenerateComplexClass()
{
    var sb = new StringBuilder();
    
    // 添加文件头
    sb.AppendLine("// <auto-generated/>");
    sb.AppendLine();
    
    // 添加命名空间
    sb.AppendLine("namespace Generated");
    sb.AppendLine("{");
    
    // 添加类定义
    sb.AppendLine("    public static class HelloWorld");
    sb.AppendLine("    {");
    
    // 添加方法
    for (int i = 1; i <= 5; i++)
    {
        sb.AppendLine($"        public static string Method{i}()");
        sb.AppendLine("        {");
        sb.AppendLine($"            return \"Method {i}\";");
        sb.AppendLine("        }");
        sb.AppendLine();
    }
    
    // 关闭类和命名空间
    sb.AppendLine("    }");
    sb.AppendLine("}");
    
    return sb.ToString();
}

2. 使用原始字符串字面量(C# 11+)

csharp
private string GenerateWithRawString()
{
    return """
        // <auto-generated/>
        
        namespace Generated
        {
            public static class HelloWorld
            {
                public static string SayHello() => "Hello!";
            }
        }
        """;
}

3. 使用代码生成辅助类

csharp
public class CodeBuilder
{
    private readonly StringBuilder _sb = new();
    private int _indentLevel = 0;
    private const string IndentString = "    ";

    public CodeBuilder AppendLine(string line = "")
    {
        if (!string.IsNullOrEmpty(line))
        {
            _sb.Append(IndentString, _indentLevel);
        }
        _sb.AppendLine(line);
        return this;
    }

    public CodeBuilder OpenBrace()
    {
        AppendLine("{");
        _indentLevel++;
        return this;
    }

    public CodeBuilder CloseBrace()
    {
        _indentLevel--;
        AppendLine("}");
        return this;
    }

    public override string ToString() => _sb.ToString();
}

// 使用示例
private string GenerateWithBuilder()
{
    var builder = new CodeBuilder();
    
    builder
        .AppendLine("namespace Generated")
        .OpenBrace()
        .AppendLine("public static class HelloWorld")
        .OpenBrace()
        .AppendLine("public static string SayHello() => \"Hello!\";")
        .CloseBrace()
        .CloseBrace();
    
    return builder.ToString();
}

执行流程图

编译时执行流程

运行时执行流程


关键技术点

1. [Generator] 特性

csharp
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
    // ...
}

作用

  • 标记类为源生成器
  • 编译器会自动发现并加载
  • 必须实现 ISourceGenerator 接口

注意事项

  • 一个程序集可以包含多个生成器
  • 每个生成器必须有唯一的类名
  • 生成器类必须是 public

2. ISourceGenerator 接口

csharp
public interface ISourceGenerator
{
    void Initialize(GeneratorInitializationContext context);
    void Execute(GeneratorExecutionContext context);
}

Initialize 方法

  • 生成器加载时调用一次
  • 用于注册回调和初始化
  • 不应该执行耗时操作

Execute 方法

  • 每次编译时调用
  • 在这里生成代码
  • 应该是幂等的(相同输入产生相同输出)

3. AddSource 方法

csharp
context.AddSource(
    hintName: "HelloWorld.g.cs",  // 文件名提示
    sourceText: SourceText.From(code, Encoding.UTF8)  // 源代码
);

参数说明

  • hintName: 生成文件的名称提示

    • 必须唯一
    • 建议使用 .g.cs 后缀
    • 不能包含路径分隔符
  • sourceText: 源代码内容

    • 使用 SourceText.From() 创建
    • 指定编码(推荐 UTF-8)
    • 必须是有效的 C# 代码

最佳实践

csharp
// ✅ 推荐:使用 SourceText.From
context.AddSource(
    "HelloWorld.g.cs",
    SourceText.From(code, Encoding.UTF8)
);

// ✅ 也可以:直接传递字符串(会自动转换)
context.AddSource("HelloWorld.g.cs", code);

// ❌ 避免:文件名包含路径
context.AddSource("Generated/HelloWorld.g.cs", code);  // 错误!

// ❌ 避免:文件名重复
context.AddSource("HelloWorld.g.cs", code1);
context.AddSource("HelloWorld.g.cs", code2);  // 错误!会覆盖

4. 生成的代码规范

csharp
// ✅ 推荐的生成代码格式
var code = @"
// <auto-generated/>
#nullable enable

namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""Hello!"";
    }
}
";

规范说明

  1. 添加 <auto-generated/> 注释

    • 标记为自动生成的代码
    • 某些工具会跳过这些文件
  2. 使用 #nullable enable

    • 启用可空引用类型
    • 与现代 C# 项目保持一致
  3. 使用完整的命名空间

    • 避免命名冲突
    • 使代码更清晰
  4. 添加 XML 文档注释

    • 提供 IntelliSense 支持
    • 帮助用户理解生成的代码

代码生成原理

编译管道中的位置

生成器的执行时机

  1. 编译开始

    • 编译器加载所有源生成器
    • 调用每个生成器的 Initialize() 方法
  2. 语法分析完成

    • 编译器构建语法树
    • 准备语义模型
  3. 生成器执行

    • 调用每个生成器的 Execute() 方法
    • 生成器可以访问语法树和语义模型
    • 生成器添加新的源文件
  4. 重新编译

    • 编译器将生成的代码加入编译
    • 重新进行语义分析
    • 生成最终的 IL 代码

生成器的限制

  1. 只能添加代码,不能修改

    • 生成器只能添加新文件
    • 不能修改现有代码
    • 不能删除代码
  2. 必须是确定性的

    • 相同输入必须产生相同输出
    • 不应该依赖外部状态
    • 不应该有副作用
  3. 性能要求

    • 生成器在编译时运行
    • 必须快速执行
    • 避免耗时操作

最佳实践 vs 反模式

最佳实践

✅ 1. 使用有意义的文件名

csharp
// ✅ 好:清晰的文件名
context.AddSource("HelloWorld.g.cs", code);
context.AddSource("HelloWorld.Extensions.g.cs", extensionCode);

// ❌ 差:模糊的文件名
context.AddSource("Generated.cs", code);
context.AddSource("File1.cs", code);

✅ 2. 添加文件头注释

csharp
var code = @"
// <auto-generated/>
// This file is generated by HelloWorldGenerator
// Do not modify this file manually

namespace Generated
{
    // ...
}
";

✅ 3. 使用 SourceText.From

csharp
// ✅ 好:指定编码
context.AddSource(
    "HelloWorld.g.cs",
    SourceText.From(code, Encoding.UTF8)
);

// ❌ 差:依赖默认编码
context.AddSource("HelloWorld.g.cs", code);

✅ 4. 处理异常

csharp
public void Execute(GeneratorExecutionContext context)
{
    try
    {
        var code = GenerateCode();
        context.AddSource("HelloWorld.g.cs", code);
    }
    catch (Exception ex)
    {
        // 报告诊断信息而不是抛出异常
        context.ReportDiagnostic(Diagnostic.Create(
            new DiagnosticDescriptor(
                "HW001",
                "Generation Error",
                $"Failed to generate code: {ex.Message}",
                "HelloWorldGenerator",
                DiagnosticSeverity.Error,
                isEnabledByDefault: true
            ),
            Location.None
        ));
    }
}

反模式

❌ 1. 在 Initialize 中生成代码

csharp
// ❌ 错误:不要在 Initialize 中生成代码
public void Initialize(GeneratorInitializationContext context)
{
    // 错误!Initialize 只用于初始化
    var code = GenerateCode();
    // context.AddSource(...);  // 这里没有 AddSource 方法
}

// ✅ 正确:在 Execute 中生成代码
public void Execute(GeneratorExecutionContext context)
{
    var code = GenerateCode();
    context.AddSource("HelloWorld.g.cs", code);
}

❌ 2. 依赖外部状态

csharp
// ❌ 错误:依赖外部文件
public void Execute(GeneratorExecutionContext context)
{
    var template = File.ReadAllText("template.txt");  // 错误!
    var code = ProcessTemplate(template);
    context.AddSource("HelloWorld.g.cs", code);
}

// ✅ 正确:将模板嵌入到生成器中
private const string Template = @"
namespace Generated
{
    public static class HelloWorld { }
}
";

public void Execute(GeneratorExecutionContext context)
{
    var code = ProcessTemplate(Template);
    context.AddSource("HelloWorld.g.cs", code);
}

❌ 3. 生成无效的 C# 代码

csharp
// ❌ 错误:生成的代码有语法错误
var code = @"
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello()  // 缺少方法体
    }
}
";

// ✅ 正确:确保生成的代码是有效的
var code = @"
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello()
        {
            return ""Hello!"";
        }
    }
}
";

扩展练习

练习 1: 添加参数

目标: 修改生成器,生成带参数的方法

提示:

csharp
public static string SayHello(string name)
{
    return $"Hello {name} from Source Generator!";
}

练习 2: 生成多个方法

目标: 生成多个不同的问候方法

提示:

csharp
public static string SayGoodMorning() => "Good morning!";
public static string SayGoodNight() => "Good night!";

练习 3: 添加时间戳

目标: 在生成的代码中包含生成时间

提示:

csharp
public static string GetGenerationTime()
{
    return "Generated at: 2025-01-21 10:30:00";
}

练习 4: 生成多个文件

目标: 生成多个不同的类文件

提示:

csharp
context.AddSource("HelloWorld.g.cs", helloWorldCode);
context.AddSource("Goodbye.g.cs", goodbyeCode);

练习 5: 添加 XML 文档注释

目标: 为生成的方法添加 XML 文档注释

提示:

csharp
/// <summary>
/// 返回问候消息
/// </summary>
/// <returns>问候消息字符串</returns>
public static string SayHello() => "Hello!";

常见问题

Q1: 为什么我的生成器没有运行?

A: 检查以下几点:

  1. 项目引用配置
xml
<ProjectReference Include="..\HelloWorld.Generator\HelloWorld.Generator.csproj"
                  OutputItemType="Analyzer"
                  ReferenceOutputAssembly="false" />
  1. 生成器项目目标框架
xml
<TargetFramework>netstandard2.0</TargetFramework>
  1. [Generator] 特性
csharp
[Generator]
public class HelloWorldGenerator : ISourceGenerator
  1. 清理并重新构建
bash
dotnet clean
dotnet build

Q2: 如何查看生成的代码?

A: 三种方法:

  1. 在 obj 目录中查看
obj/Debug/net8.0/generated/HelloWorld.Generator/HelloWorld.Generator.HelloWorldGenerator/HelloWorld.g.cs
  1. 配置输出到指定目录
xml
<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
  1. 在 Visual Studio 中查看
    • 展开项目节点
    • 展开 "Dependencies" → "Analyzers" → "HelloWorld.Generator"
    • 查看生成的文件

Q3: 生成的代码报错怎么办?

A: 调试步骤:

  1. 检查生成的代码语法

    • 确保代码是有效的 C#
    • 检查括号、分号等
  2. 添加调试输出

csharp
public void Execute(GeneratorExecutionContext context)
{
    var code = GenerateCode();
    
    // 输出到构建日志
    System.Diagnostics.Debug.WriteLine(code);
    
    context.AddSource("HelloWorld.g.cs", code);
}
  1. 使用 Debugger.Launch()
csharp
public void Execute(GeneratorExecutionContext context)
{
    #if DEBUG
    System.Diagnostics.Debugger.Launch();
    #endif
    
    // ...
}

Q4: 如何在生成器中使用第三方库?

A: 注意事项:

  1. 只能使用 .NET Standard 2.0 兼容的库
  2. 避免大型依赖
  3. 考虑将代码内联
xml
<ItemGroup>
  <!-- 可以使用的库 -->
  <PackageReference Include="System.Text.Json" Version="6.0.0" PrivateAssets="all" />
  
  <!-- 避免使用运行时库 -->
  <!-- <PackageReference Include="EntityFrameworkCore" /> ❌ -->
</ItemGroup>

Q5: 生成器性能很慢怎么办?

A: 优化建议:

  1. 使用增量生成器(参见下一个示例)
  2. 缓存计算结果
  3. 避免重复操作
  4. 减少生成的代码量

Q6: 如何测试生成器?

A: 参考 测试调试示例


🔗 相关资源

深入学习

API 参考

官方文档

学习指南

下一步

API 参考


最后更新: 2025-01-21


🔍 深入理解生成器工作原理

编译器集成

源生成器是编译器的一部分,在编译过程中运行。理解这一点对于编写高效的生成器至关重要。

编译器如何发现生成器

  1. 程序集扫描:编译器扫描所有引用的程序集
  2. 特性识别:查找标记了 [Generator] 特性的类
  3. 接口验证:确认类实现了 ISourceGenerator 接口
  4. 实例化:创建生成器实例并调用方法

生成器的生命周期

生成的代码如何被使用

生成的代码会被添加到编译中,就像它是源文件的一部分一样:

csharp
// 原始代码 (Program.cs)
using Generated;

class Program
{
    static void Main()
    {
        // 调用生成的代码
        Console.WriteLine(HelloWorld.SayHello());
    }
}

// 生成的代码 (HelloWorld.g.cs)
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => "Hello from Source Generator!";
    }
}

// 编译器将两者合并编译

🎨 代码生成的艺术

字符串构建技巧

技巧 1: 使用原始字符串字面量

C# 11 引入的原始字符串字面量让代码生成更简单:

csharp
// ✅ 使用原始字符串字面量(C# 11+)
var code = """
    namespace Generated
    {
        public static class HelloWorld
        {
            public static string SayHello() => "Hello!";
        }
    }
    """;

// ❌ 传统方式(需要转义)
var oldCode = @"
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""Hello!"";
    }
}";

技巧 2: 使用插值字符串

csharp
var className = "HelloWorld";
var methodName = "SayHello";
var message = "Hello from Generator!";

var code = $$"""
    namespace Generated
    {
        public static class {{className}}
        {
            public static string {{methodName}}() => "{{message}}";
        }
    }
    """;

技巧 3: 使用 StringBuilder 处理复杂逻辑

csharp
public string GenerateMultipleMethods(int count)
{
    var sb = new StringBuilder();
    
    sb.AppendLine("namespace Generated");
    sb.AppendLine("{");
    sb.AppendLine("    public static class HelloWorld");
    sb.AppendLine("    {");
    
    for (int i = 1; i <= count; i++)
    {
        sb.AppendLine($"        public static string Method{i}()");
        sb.AppendLine("        {");
        sb.AppendLine($"            return \"Method {i}\";");
        sb.AppendLine("        }");
        
        if (i < count)
        {
            sb.AppendLine();
        }
    }
    
    sb.AppendLine("    }");
    sb.AppendLine("}");
    
    return sb.ToString();
}

代码格式化

保持一致的缩进

csharp
public class IndentedCodeBuilder
{
    private readonly StringBuilder _sb = new();
    private int _indentLevel = 0;
    private const string IndentString = "    "; // 4 个空格
    
    public IndentedCodeBuilder AppendLine(string line = "")
    {
        if (!string.IsNullOrEmpty(line))
        {
            for (int i = 0; i < _indentLevel; i++)
            {
                _sb.Append(IndentString);
            }
        }
        _sb.AppendLine(line);
        return this;
    }
    
    public IndentedCodeBuilder Indent()
    {
        _indentLevel++;
        return this;
    }
    
    public IndentedCodeBuilder Unindent()
    {
        if (_indentLevel > 0)
        {
            _indentLevel--;
        }
        return this;
    }
    
    public override string ToString() => _sb.ToString();
}

// 使用示例
var builder = new IndentedCodeBuilder();
builder
    .AppendLine("namespace Generated")
    .AppendLine("{")
    .Indent()
    .AppendLine("public static class HelloWorld")
    .AppendLine("{")
    .Indent()
    .AppendLine("public static string SayHello() => \"Hello!\";")
    .Unindent()
    .AppendLine("}")
    .Unindent()
    .AppendLine("}");

var code = builder.ToString();

🚀 进阶练习

练习 6: 生成多个类

目标: 修改生成器,生成多个不同的类

提示:

csharp
public void Execute(GeneratorExecutionContext context)
{
    // 生成 HelloWorld 类
    context.AddSource("HelloWorld.g.cs", GenerateHelloWorld());
    
    // 生成 Goodbye 类
    context.AddSource("Goodbye.g.cs", GenerateGoodbye());
    
    // 生成 Greeting 类
    context.AddSource("Greeting.g.cs", GenerateGreeting());
}

private string GenerateHelloWorld()
{
    return @"
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""Hello!"";
    }
}";
}

private string GenerateGoodbye()
{
    return @"
namespace Generated
{
    public static class Goodbye
    {
        public static string SayGoodbye() => ""Goodbye!"";
    }
}";
}

private string GenerateGreeting()
{
    return @"
namespace Generated
{
    public static class Greeting
    {
        public static string Greet(string name) => $""Hello, {name}!"";
    }
}";
}

练习 7: 添加条件编译

目标: 根据编译配置生成不同的代码

提示:

csharp
public void Execute(GeneratorExecutionContext context)
{
    // 检查是否是 Debug 模式
    var isDebug = context.ParseOptions
        .PreprocessorSymbolNames
        .Contains("DEBUG");
    
    var code = isDebug 
        ? GenerateDebugCode() 
        : GenerateReleaseCode();
    
    context.AddSource("HelloWorld.g.cs", code);
}

private string GenerateDebugCode()
{
    return @"
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""Hello from Debug!"";
        
        public static void DebugInfo()
        {
            System.Console.WriteLine(""Debug mode enabled"");
        }
    }
}";
}

private string GenerateReleaseCode()
{
    return @"
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""Hello from Release!"";
    }
}";
}

练习 8: 生成扩展方法

目标: 生成字符串扩展方法

提示:

csharp
private string GenerateExtensionMethods()
{
    return @"
namespace Generated
{
    public static class StringExtensions
    {
        public static string Reverse(this string str)
        {
            var chars = str.ToCharArray();
            System.Array.Reverse(chars);
            return new string(chars);
        }
        
        public static bool IsNullOrEmpty(this string str)
        {
            return string.IsNullOrEmpty(str);
        }
        
        public static string Repeat(this string str, int count)
        {
            return string.Concat(System.Linq.Enumerable.Repeat(str, count));
        }
    }
}";
}

练习 9: 生成接口实现

目标: 自动生成接口的默认实现

提示:

csharp
private string GenerateInterfaceImplementation()
{
    return @"
namespace Generated
{
    public interface IGreeter
    {
        string Greet(string name);
    }
    
    public class DefaultGreeter : IGreeter
    {
        public string Greet(string name)
        {
            return $""Hello, {name}!"";
        }
    }
    
    public class FormalGreeter : IGreeter
    {
        public string Greet(string name)
        {
            return $""Good day, {name}."";
        }
    }
}";
}

练习 10: 生成单元测试辅助类

目标: 生成测试辅助代码

提示:

csharp
private string GenerateTestHelpers()
{
    return @"
namespace Generated.Testing
{
    public static class TestHelpers
    {
        public static void AssertHelloWorld(string actual)
        {
            const string expected = ""Hello from Source Generator!"";
            if (actual != expected)
            {
                throw new System.Exception(
                    $""Expected: {expected}, Actual: {actual}"");
            }
        }
        
        public static void RunAllTests()
        {
            System.Console.WriteLine(""Running tests..."");
            
            var result = HelloWorld.SayHello();
            AssertHelloWorld(result);
            
            System.Console.WriteLine(""All tests passed!"");
        }
    }
}";
}

🎓 学习路径建议

初学者路径

  1. 第 1 周:理解 Hello World 示例

    • 创建基本的生成器
    • 理解 ISourceGenerator 接口
    • 学习如何添加源文件
  2. 第 2 周:学习代码生成技巧

    • 使用 StringBuilder
    • 处理字符串转义
    • 格式化生成的代码
  3. 第 3 周:探索更多示例

中级路径

  1. 第 4-5 周:学习语法分析

    • 理解语法树
    • 使用 SyntaxReceiver
    • 访问语义模型
  2. 第 6-7 周:实现实用生成器

  3. 第 8 周:学习测试和调试

高级路径

  1. 第 9-10 周:性能优化

    • 增量生成器
    • 缓存策略
    • 内存管理
  2. 第 11-12 周:高级模式

  3. 第 13+ 周:实战项目

    • 构建自己的生成器
    • 贡献开源项目

📚 补充资源

官方文档

社区资源

相关工具


🎯 实战项目建议

项目 1: 配置类生成器

创建一个生成器,自动为配置类生成验证代码:

csharp
// 输入
[GenerateValidation]
public class AppConfig
{
    [Required]
    public string ConnectionString { get; set; }
    
    [Range(1, 100)]
    public int MaxConnections { get; set; }
}

// 生成的代码
public partial class AppConfig
{
    public List<string> Validate()
    {
        var errors = new List<string>();
        
        if (string.IsNullOrEmpty(ConnectionString))
            errors.Add("ConnectionString is required");
        
        if (MaxConnections < 1 || MaxConnections > 100)
            errors.Add("MaxConnections must be between 1 and 100");
        
        return errors;
    }
}

项目 2: DTO 映射生成器

自动生成 DTO 和实体之间的映射代码:

csharp
// 输入
[GenerateMapper]
public class UserDto
{
    public string Name { get; set; }
    public string Email { get; set; }
}

public class User
{
    public string Name { get; set; }
    public string Email { get; set; }
    public DateTime CreatedAt { get; set; }
}

// 生成的代码
public static class UserMapper
{
    public static UserDto ToDto(User user)
    {
        return new UserDto
        {
            Name = user.Name,
            Email = user.Email
        };
    }
    
    public static User ToEntity(UserDto dto)
    {
        return new User
        {
            Name = dto.Name,
            Email = dto.Email,
            CreatedAt = DateTime.Now
        };
    }
}

项目 3: 枚举扩展生成器

为枚举类型生成扩展方法:

csharp
// 输入
[GenerateExtensions]
public enum Status
{
    [Description("待处理")]
    Pending,
    
    [Description("进行中")]
    InProgress,
    
    [Description("已完成")]
    Completed
}

// 生成的代码
public static class StatusExtensions
{
    public static string GetDescription(this Status status)
    {
        return status switch
        {
            Status.Pending => "待处理",
            Status.InProgress => "进行中",
            Status.Completed => "已完成",
            _ => status.ToString()
        };
    }
    
    public static Status Parse(string value)
    {
        return value switch
        {
            "待处理" => Status.Pending,
            "进行中" => Status.InProgress,
            "已完成" => Status.Completed,
            _ => throw new ArgumentException($"Invalid status: {value}")
        };
    }
}

💡 专家提示

提示 1: 使用 #nullable enable

在生成的代码中启用可空引用类型:

csharp
var code = @"
// <auto-generated/>
#nullable enable

namespace Generated
{
    public static class HelloWorld
    {
        public static string? SayHello(string? name)
        {
            return name != null ? $""Hello, {name}!"" : null;
        }
    }
}
";

提示 2: 添加 EditorBrowsable 特性

隐藏生成的代码,避免 IntelliSense 混乱:

csharp
var code = @"
using System.ComponentModel;

namespace Generated
{
    [EditorBrowsable(EditorBrowsableState.Never)]
    public static class HelloWorld
    {
        public static string SayHello() => ""Hello!"";
    }
}
";

提示 3: 使用 #pragma warning disable

禁用生成代码的警告:

csharp
var code = @"
// <auto-generated/>
#pragma warning disable CS1591 // Missing XML comment
#pragma warning disable CS8019 // Unnecessary using directive

namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""Hello!"";
    }
}

#pragma warning restore CS8019
#pragma warning restore CS1591
";

提示 4: 添加生成器版本信息

在生成的代码中包含版本信息:

csharp
public void Execute(GeneratorExecutionContext context)
{
    var version = GetType().Assembly.GetName().Version;
    
    var code = $@"
// <auto-generated/>
// Generator: HelloWorldGenerator
// Version: {version}
// Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}

namespace Generated
{{
    public static class HelloWorld
    {{
        public static string SayHello() => ""Hello from Source Generator!"";
        
        public static string GetGeneratorVersion() => ""{version}"";
    }}
}}
";
    
    context.AddSource("HelloWorld.g.cs", code);
}

🎉 总结

恭喜你完成了 Hello World 示例的学习!你现在应该:

✅ 理解源生成器的基本概念 ✅ 知道如何创建简单的生成器 ✅ 掌握代码生成的基本技巧 ✅ 了解如何调试和测试生成器 ✅ 准备好学习更高级的主题

下一步行动

  1. 实践: 尝试完成所有扩展练习
  2. 探索: 查看其他示例项目
  3. 构建: 创建自己的生成器
  4. 分享: 与社区分享你的经验

记住,源生成器是一个强大的工具,但也需要谨慎使用。始终考虑性能、可维护性和用户体验。

祝你在源生成器的旅程中取得成功!🚀


最后更新: 2025-01-21

基于 MIT 许可发布