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.

Join the conversation! Your thoughts help the community grow.