Blog
C# Source Generators - Incremental Source Generators
This blog post explains why this is needed and describes how to build and test incremental source generators. Also provides a walk through for converting legacy generators to the new incremental approach.
๐ Intro
In my previous blog posts, I explained how to write a C# Source Generator from scratch, how to easily create unit tests and how to handle Generic Attributes.
It’s advised to read those articles to get a heads-up for this blogpost.
๐ก Incremental Source Generators
With .NET 6 and beyond, Incremental Source Generators (ISGs) were introduced, addressing the limitations of the original model. If you’re building or maintaining a source generator today, you should be using the incremental model. In this post, I will explain why, show how with examples, and walk through converting legacy generators to the incremental approach.
Why Incremental Source Generators?
Performance and Scaling
Traditional source generators (now called “non-incremental”) are executed on every compilation, even for the smallest code change.
That means:
- The generator reprocesses all syntax trees every time.
- Even small changes can trigger full re-execution.
- Build times grow linearly with project size and the number of classes the source generator needs to process.
The advantage of incremental generators are:
- React only to relevant changes.
- Cache intermediate computations.
- Compose data transformations efficiently.
๐ Result: Much faster builds, especially in large solutions.
Reliability and Determinism
ISGs work like a pipeline.
Each transformation (syntax โ semantic model โ user model โ generated code) is clearly separated and executed only if inputs change.
This structure makes generators:
- Easier to debug and reason about.
- More deterministic โ the same input always yields the same output.
- Less error-prone โ the compiler tracks changes and invalidates only what’s needed.
Better IDE Experience
Incremental generators are tightly integrated with the Roslyn workspace model. This improves:
- Live feedback in the IDE.
- Intellisense updates based on generated code.
- Possibility to show diagnostics in the IDE.
๐งช Example: A Simple Attribute-Based Generator
As example, let’s imagine that you want to generate ToString() methods for classes marked with [AutoToString].
๐ซ Non-Incremental Generator
An example for normal Generator looks like this:
public void Execute(GeneratorExecutionContext context)
{
var syntaxTrees = context.Compilation.SyntaxTrees;
foreach (var tree in syntaxTrees)
{
var root = tree.GetRoot();
var classes = root.DescendantNodes().OfType();
// analyze and generate
}
}
This re-runs on every file, every change. This is not ideal.
โ Incremental Generator
An example for Incremental Generator looks like this:
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var languageDataProvider = context.ParseOptionsProvider.Select(static (options, _) =>
{
if (options is not CSharpParseOptions csParseOptions)
{
throw new NotSupportedException($"Only {LanguageNames.CSharp} is supported.");
}
return new LanguageData
{
Nullable = csParseOptions.LanguageVersion >= LanguageVersion.CSharp8,
GenericAttributes = csParseOptions.LanguageVersion >= LanguageVersion.CSharp11
};
});
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (node, _) => node is ClassDeclarationSyntax,
transform: (ctx, _) => (ClassDeclarationSyntax)ctx.Node)
.Where(cls => cls.AttributeLists
.Any(attr => attr.ToString().Contains("AutoToString")));
var compilationAndClassesProvider =
context.CompilationProvider.Combine(classDeclarations.Collect());
var combinedProvider = languageDataProvider.Combine(compilationAndClassesProvider);
context.RegisterSourceOutput(compilationAndClasses, (sourceProductionContext, pair) =>
{
// Diagnostics logging is possible
sourceProductionContext.ReportDiagnostic(diagnostic);
// generate source for each class
});
}
This pipeline follows these principles:
- Only reacts when relevant class syntax changes.
- Caches outputs.
- Is modular and maintainable.
๐Logging / Feedback
When you want to provide some feedback during the generation from source-code, there are several ways to do this:
- Write informational and error messages to the console using
Console.WriteLine(...). - Instead of generating the requested source files, generate an
Error.g.csfile which contains the exception.
Both options are possible, but the Console.WriteLine(...) is not recommended anymore when building modern Source Generators.
It’s advised to add this element to your .csproj file: <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> which will generate an error during compilation from your source generator:
src\FluentBuilderGenerator\SyntaxReceiver\AutoGenerateBuilderSyntaxReceiver.cs(29,13,29,91): error RS1035: The symbol 'Console' is banned for use by analyzers: Analyzers should not be reading / writing to the console
Diagnostics Logging
A better option is to use Diagnostics.
In an incremental generator, you typically use IncrementalGeneratorInitializationContext to register transformation steps. You don’t directly access GeneratorExecutionContext, but you can get access to it at the final step, and that’s where you can report diagnostics.
An example on generating an Warning:
context.RegisterSourceOutput(compilationAndClasses, (sourceProductionContext, pair) =>
{
var diagnostic = Diagnostic.Create(
new DiagnosticDescriptor(
id: "MYGEN001",
title: "Missing Namespace",
messageFormat: "The class '{0}' is not declared inside a namespace.",
category: "MyGenerator",
DiagnosticSeverity.Warning,
isEnabledByDefault: true),
classDecl.GetLocation(),
classDecl.Identifier.Text
);
sourceProductionContext.ReportDiagnostic(diagnostic);
// generate source for each class
});
Note that you can also provide the location where this Diagnostic warning is related to. This make it possible to provide exact feedback in Visual Studio for the message:

๐ How to Convert Existing Generators
Migrating a non-incremental generator is not difficult. See this step-by-step guide:
Step 1: Change Interface
Update the generator to use IIncrementalGenerator instead of ISourceGenerator.
public class MyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Your new pipeline here
}
}
Step 2: Use SyntaxProvider instead of scanning all syntax-trees
Avoid looping over SyntaxTrees. Instead, use context.SyntaxProvider.CreateSyntaxProvider() to register interest in specific nodes.
var candidates = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: IsCandidate,
transform: Transform);
– The IsCandidate is a callback method which provides a SyntaxNode which you can inspect to check if it should be handled by your Source Generator. Return true if it should be processed, else return false.
– The Transform is a callback method which receives a SyntaxNode which you can use to get some more information and return custom object with extra details which can be used further in the pipeline.
Step 3: Combine with Compilation
Use .Combine() if you need semantic information or compilation context.
var input = context.CompilationProvider.Combine(candidates.Collect());
Step 4: Register output
Register code generation via RegisterSourceOutput.
context.RegisterSourceOutput(input, (ctx, data) =>
{
// emit source
});
Step 5: Remove legacy code
Delete Execute() and Initialize(GeneratorInitializationContext) methods, as they are no longer supported.
๐งช Testing
Unit testing an Incremental Source Generator works the same as before. For a refresh on Unit Testing, see this link.
Note that this NuGet: CSharp.SourceGenerators.Extensions is still required to enable easy unit testing.
Make sure you use the most recent version which has built-in support for these new Incremental Source Generators.
Example
The next example shows a unit-test which tests an Incremental Source Generator:
// Example class which is invalid
public class ClassWithPrivateBuilderClass
{
[AutoGenerateBuilder(FluentBuilderAccessibility.PublicAndPrivate)]
private class PrivateClass
{
public int Test { get; set; }
}
}
// Unit test to test this scenario
[Fact]
public void GenerateFiles_ForClassWithPrivateBuilderClass_ShouldReturnDiagnostics()
{
// Arrange
var path = "./DTO/ClassWithPrivateBuilderClass.cs";
var sourceFile = new SourceFile
{
Path = path,
Text = File.ReadAllText(path)
};
// โน๏ธ Create an Incremental Source Generator
IIncrementalGenerator sut = new FluentBuilderSourceGenerator();
// Act
var result = sut.Execute(Namespace, [sourceFile]); // ๐ ISG is also supported by the `Exceute`-helper method
// Assert
result.InformationMessages.Should().HaveCount(1);
result.InformationMessages[0].Should().Be("Class modifier should be 'public' or 'internal'");
}
๐ Conclusion
Switching to Incremental Source Generators for the FluentBuilder project was a nice exercise. Converting the older generator and building the pipeline with all the separate steps takes some time, but it’s more clear in the end. Also adding diagnostics logging really adds some value for providing feedback in the IDE.
In this post, I shared why they matter, how to build one, and how to upgrade existing generators. If you’re still using the older model, I really recommend giving the incremental approach a tryโit’s worth it. As always, feel free to check out my earlier posts for more background.
๐ Links
๐ Notes
Some content in this blog is created with the help of an LLM. I did review and revise the content where needed.
Written by: Stef Heyenrath
Stef started writing software for the Microsoft .NET framework in 2007. Over the years, he has developed into a Microsoft specialist with experience in: backend technologies such as .NET, NETStandard, ASP.NET, Ethereum, Azure, and other cloud providers. In addition he worked with several frontend technologies such as Blazor, React, Angular, Vue.js.
He is the author from WireMock.Net.
Mission: Writing quality and structured software with passion in a scrum team for technically challenging projects.
Want to know more about our experts? Contact us!
