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:

terminal
dotnet new classlib -n MyGenerator -f netstandard2.0
dotnet add package Microsoft.CodeAnalysis.CSharp

Mark it as an analyser in the .csproj:

MyGenerator.csproj
<PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

Now implement IIncrementalGenerator:

Example.cs
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:

Example.cs
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:

Example.cs
    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:

config.xml
<ItemGroup>
    <ProjectReference Include="..\MyGenerator\MyGenerator.csproj"
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />
</ItemGroup>

Then use it:

Example.cs
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