Incremental Source Generators: A Practical Guide
Source generators let you emit C# code at compile time, eliminating boilerplate and enabling patterns that would otherwise require reflection. The original ISourceGenerator interface worked but had a serious problem: it re-ran on every keystroke, making the IDE sluggish in large projects. Incremental source generators solve this by caching intermediate results and only re-executing when inputs actually change.
Why Incremental?
The original ISourceGenerator.Execute method received a GeneratorExecutionContext containing the entire compilation. The generator had no way to know what changed since the last run, so it had to redo all its work every time. In a project with hundreds of files, this meant significant CPU work on every keystroke.
Incremental generators use a pipeline model. You declare your inputs, apply transformations, and Roslyn handles caching. If a file has not changed, its pipeline stage is skipped entirely.
Your First Incremental Generator
Create a netstandard2.0 class library and add the required packages:
dotnet new classlib -n MyGenerator -f netstandard2.0
dotnet add package Microsoft.CodeAnalysis.CSharp
Mark it as an analyser in the .csproj:
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
Now implement IIncrementalGenerator:
using Microsoft.CodeAnalysis;
[Generator]
public class HelloGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Register a static source that doesn't depend on user code
context.RegisterPostInitializationOutput(ctx =>
{
ctx.AddSource("HelloGenerated.g.cs", """
namespace Generated;
public static class Hello
{
public static string Greet(string name)
=> $"Hello, {name}! (generated)";
}
""");
});
}
}
This is the simplest possible generator — it emits a fixed file. The RegisterPostInitializationOutput method is for code that does not depend on the user's source at all, such as marker attributes.
Reacting to User Code
The real power comes from building a pipeline that responds to user code. Let us generate a ToString override for every class decorated with a [AutoToString] attribute.
First, emit the marker attribute:
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(ctx =>
{
ctx.AddSource("AutoToStringAttribute.g.cs", """
namespace Generated;
[System.AttributeUsage(System.AttributeTargets.Class)]
public class AutoToStringAttribute : System.Attribute { }
""");
});
Then build a pipeline that finds classes with this attribute:
var classDeclarations = context.SyntaxProvider
.ForAttributeWithMetadataName(
"Generated.AutoToStringAttribute",
predicate: (node, _) => node is ClassDeclarationSyntax,
transform: (ctx, _) =>
{
var symbol = (INamedTypeSymbol)ctx.TargetSymbol;
var properties = symbol.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.IsStatic)
.Select(p => p.Name)
.ToArray();
return new ClassInfo(
symbol.ContainingNamespace.ToDisplayString(),
symbol.Name,
properties);
});
context.RegisterSourceOutput(classDeclarations, (ctx, info) =>
{
var props = string.Join(
", ",
info.Properties.Select(p => $"{p}={{{p}}}"));
ctx.AddSource($"{info.ClassName}.ToString.g.cs", $$"""
namespace {{info.Namespace}};
partial class {{info.ClassName}}
{
public override string ToString()
=> $"{{info.ClassName}}({{props}})";
}
""");
});
}
private record ClassInfo(
string Namespace,
string ClassName,
string[] Properties);
ForAttributeWithMetadataName
The ForAttributeWithMetadataName method, introduced in .NET 7, is the preferred way to find attributed types. It is significantly more efficient than manually filtering syntax nodes because Roslyn can short-circuit the search using metadata before parsing full syntax trees.
Consuming the Generator
In the consuming project, reference the generator:
<ItemGroup>
<ProjectReference Include="..\MyGenerator\MyGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
Then use it:
using Generated;
[AutoToString]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// Person.ToString() => "Person(Name=Alice, Age=30)"
Note the partial keyword — the generated code adds a method to the same partial class.
Key Takeaways
- Always use
IIncrementalGeneratorover the olderISourceGenerator. - Use
RegisterPostInitializationOutputfor static code like marker attributes. - Use
ForAttributeWithMetadataNameto efficiently find decorated types. - Extract data into simple records or value types for the pipeline — this enables Roslyn's caching.
- Generated classes should be
partialso they merge with user-written code.