Debugging Source Generators: Techniques That Actually Work
Source generators run inside the compiler process. You cannot simply set a breakpoint and press F5. This makes debugging them one of the most frustrating aspects of the development experience. Here are the techniques that actually work.
1. Debugger.Launch()
The most direct approach. Add Debugger.Launch() to your generator code, and when the compiler loads your generator, it will prompt you to attach a debugger:
[Generator]
public class MyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
#if DEBUG
if (!Debugger.IsAttached)
{
Debugger.Launch();
}
#endif
// ... pipeline setup
}
}
When you build the consuming project, a dialog appears asking which debugger instance to attach. Select your Visual Studio instance and you can step through your generator code with full breakpoint support.
Caveats: This fires on every build, which gets annoying. Wrap it in a #if DEBUG block and consider using an environment variable or a temporary flag file to toggle it on and off.
2. Writing Generated Files to Disk
Often you do not need a debugger — you just need to see what your generator is producing. Add this to the consuming project's .csproj:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>
$(BaseIntermediateOutputPath)\GeneratedFiles
</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
After building, you will find the generated .cs files in obj/GeneratedFiles/. They are organised by generator name and hint name. This is invaluable for verifying that your string interpolation is producing valid C#.
3. Unit Testing the Generator Directly
The most reliable debugging technique is not to debug the generator inside the compiler at all. Instead, write unit tests that invoke the generator programmatically:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
[Fact]
public void Generator_Produces_Expected_Output()
{
var source = """
using Generated;
[AutoToString]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
""";
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var references = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location))
.Select(a => MetadataReference.CreateFromFile(a.Location))
.Cast<MetadataReference>()
.ToList();
var compilation = CSharpCompilation.Create("TestAssembly")
.AddReferences(references)
.AddSyntaxTrees(syntaxTree);
var generator = new MyGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGeneratorsAndUpdateCompilation(
compilation,
out var outputCompilation,
out var diagnostics);
// Assert no errors
Assert.Empty(diagnostics.Where(
d => d.Severity == DiagnosticSeverity.Error));
// Inspect generated sources
var results = driver.GetRunResult();
var generatedSource = results.GeneratedTrees
.First()
.GetText()
.ToString();
Assert.Contains("public override string ToString()", generatedSource);
}
You can set breakpoints in your generator, run the test with the debugger, and step through the code normally. No Debugger.Launch() hacks required.
4. Logging with Diagnostics
Sometimes you need to trace what the generator is doing without stopping execution. You can emit informational diagnostics:
private static readonly DiagnosticDescriptor DebugLog = new(
"GEN_DBG",
"Generator Debug",
"{0}",
"Debug",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
// Inside your output registration:
context.RegisterSourceOutput(pipeline, (ctx, model) =>
{
ctx.ReportDiagnostic(Diagnostic.Create(
DebugLog,
Location.None,
$"Processing class: {model.ClassName}"));
// ... generate code
});
These warnings appear in the build output and the Error List, giving you visibility into what the generator saw.
5. Inspecting the Syntax Visualiser
When your generator is not finding the nodes you expect, open the Syntax Visualiser in Visual Studio (View > Other Windows > Syntax Visualiser). It shows the full syntax tree for the current file. This lets you verify that your predicate is matching the correct node types.
6. Common Pitfalls
Generator not running at all: Check that the consuming project references the generator with OutputItemType="Analyzer" and ReferenceOutputAssembly="false".
Stale output: Visual Studio caches generators aggressively. If your changes are not reflected, try closing and reopening Visual Studio, or deleting the obj folder.
Missing types in the semantic model: If your generator emits attributes via RegisterPostInitializationOutput, those types are available in the compilation. But if you emit them as regular source output, they are not — use post-initialisation for marker attributes.
IDE showing errors but build succeeds: This usually means the generator is working at build time but not in the IDE's live analysis. Check for differences in the IDE vs build-time compilation, or try restarting the language server.
Recommended Workflow
- Start with unit tests. They give you the fastest feedback loop and full debugger support.
- Use
EmitCompilerGeneratedFilesto inspect output during integration testing. - Reserve
Debugger.Launch()for when you need to debug the generator in the context of a real project. - Use diagnostic logging for tracing in CI/CD builds.