C# incremental source generators can reduce repetitive code, improve consistency, and help developers build strongly typed APIs. They can generate properties, mapping methods, logging helpers, and other code during compilation. However, when a generator does not produce the expected output, finding the cause can be harder than debugging ordinary C# code.
The generated file might be missing, a syntax filter might reject the intended class, or the compiler might report an error in code that you never wrote directly. Sometimes, the generator works in the IDE but behaves differently during a command-line build.
The key is to debug the generator as a compiler component, not as part of the running application. This article explains how incremental source generators work, how to inspect generated files, how to report useful diagnostics, and how to test common failure scenarios.
What Is a C# Incremental Source Generator?
A source generator is a compiler extension that examines a project's source code and adds generated C# code to the compilation. The generated code is compiled alongside the project's existing files.
An incremental source generator implements the IIncrementalGenerator interface. Instead of repeatedly processing all available inputs without considering previous work, it defines a pipeline of transformations. The compiler can reuse results when inputs have not changed.
For example, a generator might find classes marked with a custom attribute and create a helper method for each matching class.
A typical incremental generator follows this process:
Identify relevant syntax or symbols.
Extract the information needed for generation.
Transform that information into generated source code.
Add the generated source to the compilation.
Report diagnostics when input code does not meet the generator's requirements.
Source generators do not modify existing source files. They add new source files to the compilation. They also do not execute as part of the application at runtime.
Create a Simple Incremental Source Generator
Before debugging a complex generator, create a small example that you can inspect and test.
Suppose you want to generate a helper class for every class declared in a project. The following example finds class declarations and generates a helper containing the original class name.
Implement IIncrementalGenerator
Create a separate class library for the generator and add compatible references to Microsoft.CodeAnalysis and Microsoft.CodeAnalysis.CSharp.
The generator can use the following implementation:
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Demo.Generators;
[Generator]
public sealed class ClassHelperGenerator : IIncrementalGenerator
{
public void Initialize(
IncrementalGeneratorInitializationContext context)
{
var classNames = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) =>
node is ClassDeclarationSyntax,
transform: static (syntaxContext, _) =>
((ClassDeclarationSyntax)syntaxContext.Node)
.Identifier.Text)
.Where(static name => !string.IsNullOrWhiteSpace(name));
context.RegisterSourceOutput(
classNames,
static (productionContext, className) =>
{
var source = $$"""
// <auto-generated />
namespace Demo.Generated
{
internal static class {{className}}Helper
{
public static string GetClassName()
=> "{{className}}";
}
}
""";
productionContext.AddSource(
$"{className}Helper.g.cs",
SourceText.From(source, Encoding.UTF8));
});
}
}This example demonstrates the main parts of an incremental generator:
CreateSyntaxProvideridentifies syntax nodes that match the predicate.The predicate selects class declarations.
The transform extracts the class name.
RegisterSourceOutputgenerates and adds a source file.AddSourcegives the generated file a hint name and supplies its contents.
This is a learning example rather than a production-ready generator. It does not handle nested types, duplicate generated names, generic types, or namespace conflicts. Those cases require additional design and validation.
The generator project must also be referenced correctly by the consuming project. Use a project reference configured for analyzer consumption, or package the generator as an analyzer. Make sure the Roslyn package versions and target framework are compatible with the compiler host that will load it.
Enable Generated Source Files
The first debugging step is to inspect the actual generated code. Do not rely only on the error message displayed by the IDE.
By default, generated files may not appear as ordinary files in your project. You can configure the consuming project to write them to disk.
Add the following properties to the application's project file:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>
$(BaseIntermediateOutputPath)Generated
</CompilerGeneratedFilesOutputPath>
</PropertyGroup>Build the project:
dotnet clean
dotnet buildInspect the generated output under the configured Generated directory, usually within the project's intermediate output area.
You can now check whether the generator produced the expected file and whether its contents are valid C#.
What to Check in the Generated File
Start with these questions:
Does the generated file exist?
Does it contain the expected class or method?
Is the namespace correct?
Are identifiers valid C# names?
Are required types and namespaces available?
Does the generated code match the project's nullable settings and language requirements?
Are two inputs producing the same generated hint name?
For example, if the generator produces this declaration:
namespace Demo.Generated
{
internal static class OrderHelper
{
public static string GetClassName()
=> "Order";
}
}but the consuming project reports an error involving OrderHelper, inspect the generated declaration before changing the generator pipeline. The problem could be a duplicate type, an inaccessible type, or a conflict with existing code.
If no file appears, move to the pipeline itself.
Debug the Incremental Pipeline
An incremental generator is a series of connected operations. A failure at an early stage can prevent later stages from receiving any input.
Consider the following pipeline:
var classNames = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) =>
node is ClassDeclarationSyntax,
transform: static (syntaxContext, _) =>
((ClassDeclarationSyntax)syntaxContext.Node)
.Identifier.Text);If the generator produces no files, investigate each stage separately.
Check the Syntax Predicate
The predicate decides which syntax nodes enter the transformation stage.
For example, this predicate only selects class declarations:
predicate: static (node, _) =>
node is ClassDeclarationSyntaxIt does not select interfaces, records, methods, or other syntax nodes.
If you expect the generator to process a record declaration, the predicate must account for the relevant syntax type. A filter that is too restrictive can make a working generator appear broken.
For an attribute-based generator, consider using ForAttributeWithMetadataName when its matching behavior fits the requirement. It can simplify finding declarations associated with a specific attribute and may avoid manually filtering unrelated syntax nodes.
Check the Transformation Stage
The transform converts syntax into the information needed for generation.
For the example generator, the transform returns the class identifier. If the transform extracts the wrong value, the generated file may be syntactically valid but semantically incorrect.
During development, use a debugger or temporary diagnostics to confirm which declarations reach the transform. Avoid adding permanent console output to the generator; compiler hosts may run generators in environments where console output is not useful or visible.
Keep transformations small and focused. Extract only the information required by the next stage instead of carrying entire syntax trees through the pipeline unnecessarily.
Check the Output Stage
The output stage receives the transformed data and creates source files.
A common mistake is to assume that RegisterSourceOutput runs only once. Incremental pipelines can produce output for multiple inputs, and the compiler can rerun relevant parts when inputs change.
Check that:
The pipeline actually produces values.
Each value leads to the expected output.
Generated hint names are unique.
Generated text is valid C#.
The generator does not depend on mutable global state or unpredictable values.
Avoid relying on timestamps, random identifiers, or environment-specific values in generated output unless they are explicitly part of the design. Non-deterministic output makes debugging and build reproducibility harder.
Report Diagnostics Instead of Hiding Errors
Generated code is often consumed by developers who do not know the generator's internal rules. When an input is invalid, a clear compiler diagnostic is more useful than silently skipping it or producing confusing code.
Use DiagnosticDescriptor to define a diagnostic rule.
private static readonly DiagnosticDescriptor MissingNameRule =
new(
id: "GEN001",
title: "Required name is missing",
messageFormat: "The declaration '{0}' requires a valid name.",
category: "CodeGeneration",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);You can report the diagnostic from the output stage:
context.RegisterSourceOutput(
classNames,
static (productionContext, className) =>
{
if (string.IsNullOrWhiteSpace(className))
{
productionContext.ReportDiagnostic(
Diagnostic.Create(
MissingNameRule,
Location.None,
className));
return;
}
// Generate source for valid input.
});The example illustrates the reporting mechanism. In a real implementation, use a diagnostic condition that can actually occur in your pipeline and pass the relevant source location when one is available.
A diagnostic should explain the problem and, where possible, how to fix it. Use an error severity when invalid input must prevent a successful build. Use a warning when the input is questionable but compilation can continue.
Avoid Reporting the Same Problem Repeatedly
Incremental generators may process many declarations. If a validation rule applies to individual inputs, report diagnostics at the appropriate stage and avoid producing duplicate messages for the same underlying issue.
Also distinguish generator diagnostics from errors in the generated code. A diagnostic can explain why an input was rejected; a compiler error in a generated file may indicate that the generator produced invalid syntax or referenced an unavailable type.
Debug Common Generator Problems
The following table connects common symptoms to practical checks.
Problem | Likely cause | What to check |
|---|---|---|
No generated file appears | The syntax filter excludes the input | Review the predicate and transform |
Generated file is empty or incomplete | Incorrect source construction | Inspect the generated text |
Duplicate type errors | Multiple inputs generate the same type name | Use unique names and account for namespaces |
Generator works in the IDE but fails in CI | Different compiler or package versions | Compare SDK, Roslyn references, and build configuration |
Generated code cannot find a type | Missing namespace or incorrect type name | Inspect imports, namespaces, and symbol information |
Changes do not appear as expected | Stale build artifacts or incorrect project reference | Clean and rebuild, then verify analyzer loading |
Generator crashes on certain inputs | Unexpected syntax shape or unsafe assumptions | Validate syntax and symbols before using them |
Build behavior changes between machines | Environment-dependent or non-deterministic output | Remove hidden state and verify toolchain versions |
Handle Namespaces and Nested Types
The simple example generates every helper inside Demo.Generated, regardless of the original class namespace. That may be fine for a demonstration, but production generators often need namespace-aware output.
Use semantic information when the generator must understand the declared symbol, its containing namespace, generic parameters, or type accessibility. Syntax text alone does not always provide enough information.
Be careful with nested types and generic declarations. A nested class named Item may not be unique across the project, and a generic type such as Repository<T> needs a deliberate strategy if the generator intends to reference it.
Watch for Identifier and String Escaping
Do not assume that a source identifier can always be inserted directly into generated C# text.
Names can involve escaped identifiers, and string literals have their own escaping rules. For complex generators, build output carefully and use Roslyn syntax APIs where they make the implementation safer than manual string concatenation.
Test the Generator Independently
Manual builds are useful during initial debugging, but they are not enough for a generator that will be used across multiple projects.
Create automated tests that provide sample source code, run the generator, and verify the generated output and diagnostics. Roslyn provides testing libraries designed for analyzer and source-generator scenarios. Choose versions compatible with the Roslyn packages used by the generator.
A useful test suite should cover:
A valid input that produces the expected source.
An input that should be ignored.
Invalid input that produces a diagnostic.
Multiple declarations with similar names.
Namespaces, nested classes, and generic types when supported.
Nullable reference types and relevant language-version settings.
Repeated runs and input changes that exercise incremental behavior.
Prefer testing the resulting source and diagnostics over checking only whether the generator executes without throwing an exception.
For larger generators, keep separate tests for syntax filtering, validation, and generated output. This makes failures easier to locate when a change affects only one part of the pipeline.
Best Practices for Production Source Generators
Keep the Pipeline Focused
Filter out irrelevant syntax early, and extract only the data required for generation. Avoid performing expensive semantic analysis on every syntax node when a narrower filter is sufficient.
Avoid Mutable Shared State
Do not store per-compilation results in mutable static fields. Incremental generators are expected to work through their declared pipeline, and hidden state can cause stale or inconsistent output.
Make Output Deterministic
The same inputs and configuration should produce the same generated source. Deterministic output improves build reliability, testing, and troubleshooting.
Treat Generated Code as Publicly Visible Build Output
Generated code may be inspected by developers, included in diagnostics, and affected by language-version changes. Use predictable naming, clear structure, and appropriate generated-code comments.
Validate Compiler Compatibility
A generator's target framework, Roslyn package versions, and consuming compiler must work together. When the IDE and CI produce different results, compare the .NET SDK and compiler versions first instead of assuming the generator logic is at fault.
Advantages and Disadvantages of Incremental Source Generators
Advantages | Disadvantages |
|---|---|
Reduce repetitive hand-written code | Add complexity to the build process |
Catch some invalid inputs during compilation | Generated code can be difficult to inspect without configuration |
Can reuse unchanged pipeline results | Pipeline design and caching behavior require care |
Support consistent code patterns across projects | Compiler and package compatibility must be maintained |
Can improve developer productivity in large codebases | Poor diagnostics can make failures difficult to understand |
Incremental generators are most useful when they remove meaningful repetition or provide compile-time checks that would otherwise require manual maintenance. They are less useful when a small helper method would solve the problem with much less complexity.
Summary
Debugging a C# incremental source generator becomes easier when you inspect the generated files and trace the pipeline one stage at a time. Start by confirming that the generator loads, check the syntax predicate, validate the transformation output, and inspect the code passed to AddSource.
Use compiler diagnostics to explain invalid input, test expected and unexpected cases with Roslyn testing tools, and keep the pipeline deterministic. When the generator works in the IDE but fails in a build server, compare the compiler, SDK, package references, and project configuration.
The main goal is to make the generator's behavior visible and repeatable. Once you can identify which stage is failing, even a complex generator becomes much easier to maintain.


Join the conversation! Your thoughts help the community grow.