---
URL: "/zh-CN/blog/2026-08/Roslyn.html"
LLMS_URL: "/zh-CN/blog/2026-08/Roslyn.md"
lastUpdated: true
commentabled: true
recommended: true
title: "C# Roslyn 编译器平台实战"
description: "源生成器、分析器与代码修补"
date:

pageClass: "blog-page-class"
cover: "/covers/dotnet.svg"
---

Roslyn 是微软对 C# / VB.NET 编译器的开源重写，它不再是一个黑箱——而是将编译过程完全暴露为 API。你可以介入编译的几乎每一个阶段：在语法树层面分析代码、报告诊断信息、自动修复问题，甚至在编译期间凭空生成新的源代码。

本文涵盖 Roslyn 生态中最实用的三大扩展点：源生成器（Source Generator） 、分析器（Analyzer） 和 代码修补（Code Fix Provider） ，从项目创建、日常使用到调试技巧，逐一展开。

## 核心概念速览 ##

在深入之前，有必要厘清三者的定位：

| 扩展点 | 作用 | 介入时机 | 输出 |
| :--- | :--- | :--- | :--- |
| 源生成器 | 在编译时生成新的 C# 源文件 | 编译期间，语法树已就绪但尚未进行语义分析 | 新增的 .cs 文件，参与后续编译 |
| 分析器 | 检查代码并报告诊断（警告/错误/信息） | 编译期间或编辑器实时分析时 | Diagnostic 对象（带 Squiggly line） |
| 代码修补 | 为分析器报告的诊断提供自动修复方案 | 用户在编辑器中触发 Quick Action (Ctrl+.) | 修改后的文档（语法树变换） |

三者的关系通常是：分析器发现问题 → 代码修补解决问题；源生成器则独立地生成代码以减少手写样板。

## 源生成器（Source Generator） ##

### 什么是源生成器 ###

源生成器在编译期间运行，读取当前编译的语法树和语义信息，然后向编译中追加全新的源文件。典型场景包括：自动生成序列化代码、根据配置生成 DTO、为接口生成代理实现、AOP 风格的代码织入等。

与传统的 T4 模板不同，源生成器是编译管线的一等公民——不需要额外的构建步骤，IDE 也能实时感知生成的代码。

### 创建源生成器项目 ###

项目模板方式：

```bash
dotnet new install Microsoft.CodeAnalysis.CSharp.SourceGenerators.ProjectTemplates
dotnet new sourcegenerator -n MyGenerators
```

手动创建：

```bash
dotnet new classlib -n MyGenerators -f netstandard2.0
```

然后修改 `.csproj`：

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

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>latest</LangVersion>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    <!-- 源生成器必须是 netstandard2.0，因为它运行在编译器进程内 -->
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
  </ItemGroup>

</Project>
```

关键约束：源生成器的目标框架必须是 netstandard2.0，因为它被加载到编译器进程中运行，而编译器运行在 .NET Framework 或 .NET Core 上，需要最大兼容性。

### 编写一个源生成器 ###

推荐实现 IIncrementalGenerator 接口（增量式生成器，性能更好）：

```csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Text;
// 注意要添加Generator特性
[Generator]
public class AutoLogGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // 第一步：语法过滤——找出所有带 [AutoLog] 特性的方法声明
        var methodDeclarations = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                fullyQualifiedName: "MyApp.AutoLogAttribute",
                predicate: (node, _) => node is MethodDeclarationSyntax,
                transform: (ctx, _) => (MethodDeclarationSyntax)ctx.TargetNode)
            .Where(m => m is not null);

        // 第二步：注册输出——为每个方法生成日志包装代码
        context.RegisterSourceOutput(methodDeclarations, (spc, method) =>
        {
            var className = method.Parent is ClassDeclarationSyntax c ? c.Identifier.Text : "Unknown";
            var methodName = method.Identifier.Text;
            var parameters = method.ParameterList.Parameters;

            var sb = new StringBuilder();
            sb.AppendLine("// <auto-generated/>");
            sb.AppendLine("using System;");
            sb.AppendLine();
            sb.AppendLine($"partial class {className}");
            sb.AppendLine("{");
            sb.AppendLine($"    partial void On{methodName}Enter()");
            sb.AppendLine("    {");
            sb.AppendLine($"        Console.WriteLine("[Enter] {className}.{methodName}");");
            sb.AppendLine("    }");
            sb.AppendLine();
            sb.AppendLine($"    partial void On{methodName}Exit()");
            sb.AppendLine("    {");
            sb.AppendLine($"        Console.WriteLine("[Exit] {className}.{methodName}");");
            sb.AppendLine("    }");
            sb.AppendLine("}");

            spc.AddSource($"{className}.{methodName}.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
        });
    }
}
```

在消费方项目中定义触发特性：

```csharp
namespace MyApp;

[AttributeUsage(AttributeTargets.Method)]
public class AutoLogAttribute : Attribute { }
```

### 在项目中引用源生成器 ###

消费方项目的 `.csproj` 中这样引用：

```xml
<ItemGroup>
  <ProjectReference Include="..\MyGenerators\MyGenerators.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>
```

`OutputItemType="Analyzer"` 是关键——它告诉编译器将此程序集作为分析器/生成器加载，而非普通引用。`ReferenceOutputAssembly="false"` 表示不将其作为运行时依赖。

编译后，生成的文件会参与编译。在 Visual Studio 中，你可以在解决方案资源管理器的 "Dependencies → Analyzers → MyGenerators" 节点下看到生成的文件。

## 分析器（Analyzer） ##

### 什么是分析器 ###

分析器对代码进行实时检查，当发现不符合约定的模式时，向编辑器报告一条诊断信息（Diagnostic），表现为代码下方的波浪线。分析器可以检查命名规范、禁止使用某些 API、强制架构约定、发现潜在 Bug 等。

### 创建分析器项目 ###

使用 VS 模板（推荐）：

在 Visual Studio 中搜索模板 "Analyzer with Code Fix (.NET Standard)"。这会同时创建分析器、代码修补和测试三个项目。
手动创建：

```bash
dotnet new classlib -n MyAnalyzers -f netstandard2.0
```

`.csproj` 与源生成器基本一致，同样需要 Microsoft.CodeAnalysis.CSharp 包和目标框架 netstandard2.0。

### 编写一个分析器 ###

以下分析器检查所有 public 方法是否包含 XML 文档注释，缺少则报告警告：

```csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MissingDocCommentAnalyzer : DiagnosticAnalyzer
{
    // 定义诊断描述
    public static readonly DiagnosticDescriptor Rule = new(
        id: "MY001",
        title: "公开方法缺少文档注释",
        messageFormat: "方法 '{0}' 缺少 XML 文档注释",
        category: "Documentation",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        description: "所有 public 方法都应包含 /// 文档注释以提高可维护性。");

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
        => ImmutableArray.Create(Rule);

    public override void Initialize(AnalysisContext context)
    {
        // 性能最佳实践：并发执行 + 跳过生成代码
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();

        // 注册语法节点回调——只关心方法声明
        context.RegisterSyntaxNodeAction(AnalyzeMethod, SyntaxKind.MethodDeclaration);
    }

    private static void AnalyzeMethod(SyntaxNodeAnalysisContext context)
    {
        var method = (MethodDeclarationSyntax)context.Node;

        // 只检查 public 方法
        if (!method.Modifiers.Any(SyntaxKind.PublicKeyword))
            return;

        // 检查是否存在三斜杠文档注释
        var trivia = method.GetLeadingTrivia();
        var hasDocComment = trivia.Any(t =>
            t.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) ||
            t.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia));

        if (!hasDocComment)
        {
            var diagnostic = Diagnostic.Create(
                Rule,
                method.Identifier.GetLocation(),
                method.Identifier.Text);
            context.ReportDiagnostic(diagnostic);
        }
    }
}
```

### 分析器的引用方式 ###

与源生成器完全相同：

```xml
<ProjectReference Include="..\MyAnalyzers\MyAnalyzers.csproj"
                  OutputItemType="Analyzer"
                  ReferenceOutputAssembly="false" />
```

如果分析器以 NuGet 包形式发布，消费方可以这样引用：

```xml
<PackageReference Include="MyAnalyzers" Version="1.0.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
```

引用后，编辑器会实时显示诊断波浪线，编译时也会在输出中报告对应的警告或错误。

### 通过 `.editorconfig` 或 `.globalconfig` 配置严重级别 ###

消费方可以覆盖分析器默认的严重级别：

```ini
# .editorconfig
[*.cs]
dotnet_diagnostic.MY001.severity = error
```

也可以完全禁用某条规则：

```ini
dotnet_diagnostic.MY001.severity = none
```

## 代码修补（Code Fix Provider） ##

### 什么是代码修补 ###

代码修补为分析器报告的诊断提供自动修复方案。用户在编辑器中将光标放在波浪线上，按下 Ctrl+.（Quick Actions），就能看到修复建议，确认后自动修改代码。

### 编写代码修补 ###

继续上面的例子——为缺少文档注释的方法自动生成注释骨架：

```csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AddDocCommentCodeFix)), Shared]
public class AddDocCommentCodeFix : CodeFixProvider
{
    public sealed override ImmutableArray<string> FixableDiagnosticIds
        => ImmutableArray.Create("MY001");

    public sealed override FixAllProvider GetFixAllProvider()
        => WellKnownFixAllProviders.BatchFixer;

    public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
        var diagnostic = context.Diagnostics.First();
        var span = diagnostic.Location.SourceSpan;

        // 找到对应的方法声明节点
        var method = root?.FindToken(span.Start)
            .Parent?.AncestorsAndSelf()
            .OfType<MethodDeclarationSyntax>()
            .FirstOrDefault();

        if (method is null) return;

        context.RegisterCodeFix(
            CodeAction.Create(
                title: "添加 XML 文档注释",
                createChangedDocument: ct => AddDocCommentAsync(context.Document, method, ct),
                equivalenceKey: "AddDocComment"),
            diagnostic);
    }

    private static async Task<Document> AddDocCommentAsync(
        Document document,
        MethodDeclarationSyntax method,
        CancellationToken cancellationToken)
    {
        // 构造 /// <summary> 注释文本
        var paramLines = method.ParameterList.Parameters
            .Select(p => $"/// <param name="{p.Identifier.Text}"></param>");

        var hasReturn = method.ReturnType.ToString() != "void";

        var commentText = "/// <summary>\n/// \n/// </summary>\n"
            + string.Join("\n", paramLines)
            + (hasReturn ? "\n/// <returns></returns>" : "")
            + "\n";

        var trivia = SyntaxFactory.ParseLeadingTrivia(commentText);
        var newMethod = method.WithLeadingTrivia(
            method.GetLeadingTrivia().InsertRange(0, trivia));

        var root = await document.GetSyntaxRootAsync(cancellationToken);
        var newRoot = root!.ReplaceNode(method, newMethod);

        return document.WithSyntaxRoot(newRoot);
    }
}
```

关键要点：

FixableDiagnosticIds 声明此修补对应哪些诊断 ID（这里是 MY001）。`GetFixAllProvider()` 返回 BatchFixer，让用户可以一次性修复整个项目中的所有同类问题。RegisterCodeFixesAsync 中注册修复动作，实际修改通过操作语法树完成。

### 代码修补的引用 ###

代码修补与分析器在同一个程序集中，引用方式一致。通常分析器和代码修补打包在一起发布。

## 调试技巧 ##

调试 Roslyn 扩展比普通应用更特殊，因为生成器和分析器运行在编译器进程中（而非你的应用进程）。以下是几种实用的调试方法。

### 使用 `Debugger.Launch()` 断点 ###

最直接的方式——在生成器或分析器代码中插入：

```csharp
#if DEBUG
if (!System.Diagnostics.Debugger.IsAttached)
    System.Diagnostics.Debugger.Launch();
#endif
```

编译时，系统会弹出对话框让你选择一个调试器实例附加。附加后就能命中后续断点。

注意：这会让编译过程暂停等待调试器附加，只应在开发期间使用。

### Visual Studio 实验实例（Experimental Hive） ###

如果你在 Visual Studio 中开发，可以创建一个 VSIX 项目来打包分析器，然后启动一个 VS 实验实例来测试：

- 在分析器项目的 Debug 配置中，设置 "Start external program" 为 devenv.exe 的路径。
- 在命令行参数中添加 /rootsuffix Exp。
- 按 F5 启动，会打开一个新的 VS 窗口（实验实例）。
- 在实验实例中打开消费方项目，分析器将在此实例中运行，主 VS 实例保持调试状态。

### 单元测试 ###

这是最推荐的开发方式——为分析器和生成器编写单元测试，无需启动编译器进程即可验证行为。

测试项目设置：

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

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.2" />
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
    <PackageReference Include="xunit" Version="2.7.0" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.5.7" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\MyAnalyzers\MyAnalyzers.csproj" />
  </ItemGroup>

</Project>
```

分析器测试示例：

```csharp
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Xunit;

public class MissingDocCommentAnalyzerTests
{
    [Fact]
    public async Task PublicMethod_WithoutDocComment_ReportsWarning()
    {
        var code = """
            public class MyClass
            {
                public void {|#0:DoSomething|}() { }
            }
            """;

        var expected = new DiagnosticResult("MY001", DiagnosticSeverity.Warning)
            .WithLocation(0)
            .WithArguments("DoSomething");

        var test = new CSharpAnalyzerTest<MissingDocCommentAnalyzer, DefaultVerifier>
        {
            TestCode = code,
            ExpectedDiagnostics = { expected },
        };

        await test.RunAsync();
    }

    [Fact]
    public async Task PublicMethod_WithDocComment_NoDiagnostic()
    {
        var code = """
            public class MyClass
            {
                /// <summary>Does something.</summary>
                public void DoSomething() { }
            }
            """;

        var test = new CSharpAnalyzerTest<MissingDocCommentAnalyzer, DefaultVerifier>
        {
            TestCode = code,
        };

        await test.RunAsync();
    }
}
```

代码修补测试示例：

```csharp
[Fact]
public async Task CodeFix_AddsDocComment()
{
    var code = """
        public class MyClass
        {
            public void {|#0:DoSomething|}() { }
        }
        """;

    var fixedCode = """
        public class MyClass
        {
            /// <summary>
            ///
            /// </summary>
            public void DoSomething() { }
        }
        """;

    var expected = new DiagnosticResult("MY001", DiagnosticSeverity.Warning)
        .WithLocation(0)
        .WithArguments("DoSomething");

    var test = new CSharpCodeFixTest<MissingDocCommentAnalyzer, AddDocCommentCodeFix, DefaultVerifier>
    {
        TestCode = code,
        FixedCode = fixedCode,
        ExpectedDiagnostics = { expected },
    };

    await test.RunAsync();
}
```

源生成器测试示例：

```csharp
[Fact]
public async Task Generator_ProducesPartialMethods()
{
    var source = """
        using MyApp;

        [AutoLog]
        public partial class MyService
        {
            [AutoLog]
            public partial void Process() { }
        }

        namespace MyApp
        {
            [System.AttributeUsage(System.AttributeTargets.Method)]
            public class AutoLogAttribute : System.Attribute { }
        }
        """;

    var test = new CSharpSourceGeneratorTest<AutoLogGenerator, DefaultVerifier>
    {
        TestState =
        {
            Sources = { source },
            GeneratedSources =
            {
                // 验证生成的文件内容
                (typeof(AutoLogGenerator), "MyService.Process.g.cs",
                    SourceText.From(ExpectedGeneratedCode, Encoding.UTF8)),
            },
        },
    };

    await test.RunAsync();
}
```

运行 dotnet test 就能验证所有行为，断点调试与普通单元测试无异。

### 查看生成器输出 ###

开发期间，可以让生成器将中间结果写到磁盘以便检查：

```csharp
#if DEBUG
System.IO.File.WriteAllText(
    $@"C:\temp\generator{methodName}.cs",
    generatedCode);
#endif
```

在消费方项目中也可以配置将生成文件输出到磁盘：

```xml
<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
```

编译后，所有生成的文件会出现在 `Generated/` 目录下，可以直接打开查看。

### 常见调试陷阱 ###

*生成器未被加载*： 确保消费方项目引用了 OutputItemType="Analyzer"。如果用 NuGet 包，检查包的 analyzers/dotnet/cs/ 目录结构是否正确。

*netstandard2.0 限制*： 生成器中不能使用 Span、IAsyncEnumerable 等较新 API，除非额外引用兼容包。遇到 MissingMethodException 或 TypeLoadException 时优先检查这一点。

*增量生成器的缓存语义*： IIncrementalGenerator 要求 transform 步骤是纯函数且结果可比较。如果你的中间结果类型没有正确实现 Equals 和 GetHashCode，生成器会不必要地重新运行，导致性能问题。建议使用 record 类型或手动实现值相等。

*调试器无法附加*： `Debugger.Launch()` 在 CI 环境下会卡死编译。务必用 `#if DEBUG` 保护，或改用条件断点 + 日志输出的方式。

## 项目结构最佳实践 ##

一个完整的 Roslyn 扩展解决方案通常包含以下结构：

```txt
MyRoslynTools/
├── src/
│   ├── MyAnalyzers/              # 分析器 + 代码修补（netstandard2.0）
│   │   ├── MyAnalyzers.csproj
│   │   ├── MissingDocCommentAnalyzer.cs
│   │   └── AddDocCommentCodeFix.cs
│   ├── MyGenerators/             # 源生成器（netstandard2.0）
│   │   ├── MyGenerators.csproj
│   │   └── AutoLogGenerator.cs
│   └── MyLibrary/                # 消费方项目（net8.0）
│       ├── MyLibrary.csproj
│       └── MyService.cs
├── tests/
│   ├── MyAnalyzers.Tests/        # 分析器单元测试（net8.0）
│   │   └── MyAnalyzers.Tests.csproj
│   └── MyGenerators.Tests/       # 生成器单元测试（net8.0）
│       └── MyGenerators.Tests.csproj
└── MyRoslynTools.sln
```

将分析器和生成器分开为独立项目，因为它们的职责不同、更新节奏不同，测试也应该独立。

## NuGet 打包与分发 ##

将分析器打包为 NuGet 供其他团队使用时，`.csproj` 需要额外配置：

```xml
<PropertyGroup>
  <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
  <IncludeBuildOutput>false</IncludeBuildOutput>
  <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
  <DevelopmentDependency>true</DevelopmentDependency>
</PropertyGroup>

<ItemGroup>
  <!-- 将编译输出放到 NuGet 包的 analyzers/dotnet/cs 目录 -->
  <None Include="$(OutputPath)$(AssemblyName).dll"
        Pack="true"
        PackagePath="analyzers/dotnet/cs"
        Visible="false" />
</ItemGroup>
```

消费方引用 NuGet 包后，分析器会自动加载，无需额外配置。

## 总结 ##

Roslyn 编译器平台将 C# 编译过程从黑箱变为透明的、可编程的管线。源生成器消除了手写样板代码的痛苦，分析器让团队编码规范从文档变成了可执行的约束，代码修补则将修复方案自动化到一键完成。

从实践角度看，最重要的几个建议：始终使用 IIncrementalGenerator 而非旧版 ISourceGenerator；为分析器和生成器编写充分的单元测试，这是最高效的开发和调试方式；注意 netstandard2.0 的 API 限制；以及善用 EmitCompilerGeneratedFiles 来观察生成器的实际输出。
