调试技巧
掌握源生成器的各种调试技术,快速定位和解决问题
📋 文档信息
难度级别: 中级
预计阅读时间: 20 分钟
前置知识:
- C# 调试基础
- Visual Studio 使用
- 源生成器基础
🎯 学习目标
学完本章节后,你将能够:
- 使用
Debugger.Launch()进行交互式调试 - 查看和分析生成的文件
- 使用日志文件记录执行过程
- 在单元测试中调试生成器
- 使用诊断分析器输出调试信息
🔍 高级调试技术
方法 1: 使用 Debugger.Launch() 进行交互式调试
csharp
using System.Diagnostics;
using Microsoft.CodeAnalysis;
/// <summary>
/// 支持交互式调试的源生成器
/// </summary>
[Generator]
public class DebuggableGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
#if DEBUG
// 检查是否需要启动调试器
if (!Debugger.IsAttached)
{
// 方式 1: 自动启动调试器(推荐用于开发)
Debugger.Launch();
// 方式 2: 等待调试器附加(推荐用于 CI/CD)
// while (!Debugger.IsAttached)
// {
// System.Threading.Thread.Sleep(100);
// }
}
// 设置断点在这里
Debugger.Break();
#endif
// 生成器逻辑
var classes = context.SyntaxProvider
.ForAttributeWithMetadataName(
"GenerateAttribute",
predicate: (node, _) => node is ClassDeclarationSyntax,
transform: (ctx, _) =>
{
// 在这里设置断点可以检查每个类的处理
return GetClassInfo(ctx);
});
context.RegisterSourceOutput(classes, (spc, classInfo) =>
{
// 在这里设置断点可以检查代码生成
var code = GenerateCode(classInfo);
spc.AddSource($"{classInfo.Name}.g.cs", code);
});
}
private ClassInfo GetClassInfo(GeneratorAttributeSyntaxContext context)
{
var classSymbol = (INamedTypeSymbol)context.TargetSymbol;
// 调试技巧:使用条件断点
// 右键断点 -> 条件 -> classSymbol.Name == "MyClass"
return new ClassInfo(classSymbol.Name);
}
private string GenerateCode(ClassInfo classInfo)
{
// 调试技巧:使用日志点(Tracepoint)
// 右键断点 -> 操作 -> 记录消息到输出窗口
return $"public partial class {classInfo.Name} {{ }}";
}
private record ClassInfo(string Name);
}使用步骤:
- 在生成器代码中添加
Debugger.Launch() - 编译生成器项目
- 编译使用生成器的项目
- 选择调试器(Visual Studio 实例)
- 设置断点并开始调试
方法 2: 查看生成的文件
xml
<!-- 在使用生成器的项目中添加 -->
<PropertyGroup>
<!-- 启用生成文件输出 -->
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<!-- 指定输出目录(可选) -->
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>生成的文件位置:
obj/
Debug/
net8.0/
generated/
YourGenerator/
YourGenerator.Generator/
GeneratedFile1.g.cs
GeneratedFile2.g.cs查看生成文件的代码:
csharp
// 在生成器中添加文件输出日志
context.RegisterSourceOutput(classes, (spc, classInfo) =>
{
var code = GenerateCode(classInfo);
var fileName = $"{classInfo.Name}.g.cs";
#if DEBUG
// 输出文件名和内容到调试窗口
System.Diagnostics.Debug.WriteLine($"Generating: {fileName}");
System.Diagnostics.Debug.WriteLine(code);
#endif
spc.AddSource(fileName, code);
});方法 3: 使用日志文件进行调试
csharp
/// <summary>
/// 带日志功能的源生成器
/// </summary>
[Generator]
public class LoggingGenerator : IIncrementalGenerator
{
// 日志文件路径
private static readonly string LogPath =
Path.Combine(Path.GetTempPath(), "generator.log");
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 清空日志文件
File.WriteAllText(LogPath, $"=== Generator Started at {DateTime.Now} ===\n");
var classes = context.SyntaxProvider
.ForAttributeWithMetadataName(
"GenerateAttribute",
predicate: (node, _) =>
{
var result = node is ClassDeclarationSyntax;
Log($"Predicate: {node.GetType().Name} -> {result}");
return result;
},
transform: (ctx, _) =>
{
var classSymbol = (INamedTypeSymbol)ctx.TargetSymbol;
Log($"Transform: Processing class {classSymbol.Name}");
try
{
var info = GetClassInfo(ctx);
Log($"Transform: Successfully processed {classSymbol.Name}");
return info;
}
catch (Exception ex)
{
Log($"Transform: Error processing {classSymbol.Name}: {ex}");
throw;
}
});
context.RegisterSourceOutput(classes, (spc, classInfo) =>
{
Log($"RegisterSourceOutput: Generating code for {classInfo.Name}");
try
{
var code = GenerateCode(classInfo);
spc.AddSource($"{classInfo.Name}.g.cs", code);
Log($"RegisterSourceOutput: Successfully generated {classInfo.Name}.g.cs");
}
catch (Exception ex)
{
Log($"RegisterSourceOutput: Error generating {classInfo.Name}: {ex}");
throw;
}
});
Log("=== Generator Initialization Complete ===\n");
}
private static void Log(string message)
{
try
{
var logMessage = $"[{DateTime.Now:HH:mm:ss.fff}] {message}\n";
File.AppendAllText(LogPath, logMessage);
}
catch
{
// 忽略日志错误
}
}
private ClassInfo GetClassInfo(GeneratorAttributeSyntaxContext context)
{
var classSymbol = (INamedTypeSymbol)context.TargetSymbol;
Log($" - Class: {classSymbol.Name}");
Log($" - Namespace: {classSymbol.ContainingNamespace}");
Log($" - Properties: {classSymbol.GetMembers().OfType<IPropertySymbol>().Count()}");
return new ClassInfo(classSymbol.Name);
}
private string GenerateCode(ClassInfo classInfo)
{
var code = $"public partial class {classInfo.Name} {{ }}";
Log($" - Generated code length: {code.Length} characters");
return code;
}
private record ClassInfo(string Name);
}查看日志:
powershell
# Windows
notepad $env:TEMP\generator.log
# 或使用 PowerShell 实时监控
Get-Content $env:TEMP\generator.log -Wait日志输出示例:
=== Generator Started at 2025-01-21 10:30:45 ===
[10:30:45.123] Predicate: ClassDeclarationSyntax -> True
[10:30:45.125] Transform: Processing class MyClass
[10:30:45.126] - Class: MyClass
[10:30:45.126] - Namespace: MyNamespace
[10:30:45.127] - Properties: 3
[10:30:45.128] Transform: Successfully processed MyClass
[10:30:45.130] RegisterSourceOutput: Generating code for MyClass
[10:30:45.131] - Generated code length: 35 characters
[10:30:45.132] RegisterSourceOutput: Successfully generated MyClass.g.cs
=== Generator Initialization Complete ===方法 4: 使用单元测试调试
csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
/// <summary>
/// 源生成器单元测试
/// 可以在测试中设置断点进行调试
/// </summary>
public class GeneratorTests
{
[Fact]
public void Generator_GeneratesExpectedCode()
{
// 准备输入代码
var source = @"
using System;
[Generate]
public partial class MyClass
{
public int Id { get; set; }
public string Name { get; set; }
}";
// 创建编译
var compilation = CreateCompilation(source);
// 创建生成器实例
var generator = new MyGenerator();
// 运行生成器(可以在这里设置断点)
var driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGeneratorsAndUpdateCompilation(
compilation,
out var outputCompilation,
out var diagnostics);
// 验证没有错误
Assert.Empty(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
// 获取生成的代码
var runResult = driver.GetRunResult();
var generatedTrees = runResult.GeneratedTrees;
// 在这里设置断点可以检查生成的代码
Assert.Single(generatedTrees);
var generatedCode = generatedTrees[0].ToString();
// 验证生成的代码
Assert.Contains("public partial class MyClass", generatedCode);
}
private static Compilation CreateCompilation(string source)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var references = new[]
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Attribute).Assembly.Location),
};
return CSharpCompilation.Create(
"TestAssembly",
new[] { syntaxTree },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}
}调试步骤:
- 在测试方法中设置断点
- 右键测试 -> 调试测试
- 可以单步进入生成器代码
- 检查中间变量和生成结果
方法 5: 使用诊断分析器
csharp
/// <summary>
/// 带诊断输出的源生成器
/// </summary>
[Generator]
public class DiagnosticGenerator : IIncrementalGenerator
{
// 定义诊断描述符
private static readonly DiagnosticDescriptor DebugInfo = new(
id: "SG9999",
title: "Generator Debug Info",
messageFormat: "Debug: {0}",
category: "Generator",
DiagnosticSeverity.Info,
isEnabledByDefault: true);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classes = context.SyntaxProvider
.ForAttributeWithMetadataName(
"GenerateAttribute",
predicate: (node, _) => node is ClassDeclarationSyntax,
transform: (ctx, _) => GetClassInfo(ctx));
context.RegisterSourceOutput(classes, (spc, classInfo) =>
{
// 报告调试信息
spc.ReportDiagnostic(Diagnostic.Create(
DebugInfo,
Location.None,
$"Processing class: {classInfo.Name}"));
// 报告属性数量
spc.ReportDiagnostic(Diagnostic.Create(
DebugInfo,
Location.None,
$"Properties found: {classInfo.PropertyCount}"));
var code = GenerateCode(classInfo);
spc.AddSource($"{classInfo.Name}.g.cs", code);
// 报告生成完成
spc.ReportDiagnostic(Diagnostic.Create(
DebugInfo,
Location.None,
$"Generated code for: {classInfo.Name}"));
});
}
private ClassInfo GetClassInfo(GeneratorAttributeSyntaxContext context)
{
var classSymbol = (INamedTypeSymbol)context.TargetSymbol;
var propertyCount = classSymbol.GetMembers()
.OfType<IPropertySymbol>()
.Count();
return new ClassInfo(classSymbol.Name, propertyCount);
}
private string GenerateCode(ClassInfo classInfo)
{
return $"public partial class {classInfo.Name} {{ }}";
}
private record ClassInfo(string Name, int PropertyCount);
}查看诊断输出:
在 Visual Studio 的"错误列表"窗口中,选择"消息"级别,可以看到所有诊断信息。
💡 调试技巧总结
使用条件断点
在复杂的生成器中,使用条件断点可以快速定位问题:
csharp
public void Execute(GeneratorExecutionContext context)
{
foreach (var syntaxTree in context.Compilation.SyntaxTrees)
{
var root = syntaxTree.GetRoot();
var classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>();
foreach (var classDecl in classes)
{
// 在这里设置条件断点:classDecl.Identifier.Text == "MyClass"
var code = GenerateCode(classDecl);
context.AddSource($"{classDecl.Identifier.Text}.g.cs", code);
}
}
}使用数据断点
监视特定变量的值变化:
csharp
public class GeneratorState
{
private int _generatedCount = 0;
public void IncrementCount()
{
_generatedCount++; // 在这里设置数据断点
}
}使用跟踪点(Tracepoints)
不中断执行,只输出日志:
csharp
public void ProcessClass(ClassDeclarationSyntax classDecl)
{
// 设置跟踪点:输出 "Processing class: {classDecl.Identifier.Text}"
var properties = GetProperties(classDecl);
// 设置跟踪点:输出 "Found {properties.Count} properties"
GenerateCode(properties);
}🔗 相关资源
💡 关键要点
- 使用 Debugger.Launch() 进行交互式调试
- 启用 EmitCompilerGeneratedFiles 查看生成的代码
- 使用日志文件 记录详细的执行过程
- 编写单元测试 在测试环境中调试
- 使用诊断分析器 输出调试信息
- 使用条件断点 快速定位特定问题
- 使用跟踪点 不中断执行地输出日志
最后更新: 2025-01-21