You are viewing a preview of this lesson. Sign in to start learning
Back to C# Programming

Source Generators

Generate code at compile-time based on analysis of existing code

Source Generators in C#

Source Generators revolutionize C# development by generating code at compile time, eliminating runtime reflection overhead and boilerplate. Master this powerful metaprogramming technique with free flashcards and spaced repetition practice. This lesson covers the Source Generator pipeline, syntax analysis APIs, incremental generators, and practical implementation patternsβ€”essential concepts for building high-performance, maintainable C# applications.

Welcome to Source Generators πŸ’»

Imagine writing code that writes code for youβ€”automatically, at compile time, with zero runtime cost. That's the magic of Source Generators. Introduced in C# 9.0 with .NET 5, Source Generators are compiler plugins that inspect your code and generate additional C# files during compilation. They're like having a tireless assistant who handles all your repetitive coding tasks while you focus on business logic.

Why Source Generators matter:

  • ⚑ Zero runtime overhead - Code is generated at compile time, not through reflection
  • πŸ” Full IntelliSense support - Generated code appears in your IDE immediately
  • πŸš€ Performance gains - Eliminate reflection-based frameworks (serialization, DI, ORMs)
  • πŸ›‘οΈ Type safety - Compile-time errors catch issues before runtime
  • πŸ“¦ Reduced dependencies - Replace runtime code generation libraries

Core Concepts: Understanding the Generator Pipeline πŸ”§

The Compilation Pipeline

Source Generators plug into the Roslyn compiler pipeline and execute during compilation:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         COMPILATION PIPELINE                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                 β”‚
β”‚  πŸ“ Source Code                                 β”‚
β”‚        ↓                                        β”‚
β”‚  πŸ” Parse (Syntax Trees)                       β”‚
β”‚        ↓                                        β”‚
β”‚  πŸ”¬ Semantic Analysis ← 🎯 GENERATOR RUNS HERE β”‚
β”‚        ↓                                        β”‚
β”‚  βš™οΈ  Generate Additional Source                β”‚
β”‚        ↓                                        β”‚
β”‚  πŸ”„ Re-analyze with New Code                   β”‚
β”‚        ↓                                        β”‚
β”‚  πŸ“¦ Emit IL (Assembly)                         β”‚
β”‚                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The ISourceGenerator Interface

Every Source Generator implements the ISourceGenerator interface with two key methods:

public interface ISourceGenerator
{
    void Initialize(GeneratorInitializationContext context);
    void Execute(GeneratorExecutionContext context);
}
  • Initialize: Register for syntax notifications (performance optimization)
  • Execute: Inspect compilation and generate source code

Incremental Generators: The Modern Approach

The newer IIncrementalGenerator interface (C# 10+) provides better performance through incremental compilation:

public interface IIncrementalGenerator
{
    void Initialize(IncrementalGeneratorInitializationContext context);
}

Key advantage: Only regenerates code when relevant inputs change, making IDE experience lightning-fast.

Syntax Trees and Semantic Models 🌳

Source Generators analyze code using two fundamental concepts:

ConceptPurposeWhat You Get
Syntax TreeStructure of codeNodes, tokens, trivia (comments/whitespace)
Semantic ModelMeaning of codeTypes, symbols, binding information

Example: For List<string> items;

  • Syntax Tree: Knows it's a variable declaration with a generic type
  • Semantic Model: Knows it's System.Collections.Generic.List<T> and resolves string to System.String

Example 1: Hello World Generator πŸ‘‹

Let's build the simplest possible generator that adds a HelloWorld class to your project:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // No initialization needed for this simple generator
    }

    public void Execute(GeneratorExecutionContext context)
    {
        // Generate source code as a string
        var sourceCode = @"
namespace GeneratedCode
{
    public static class HelloWorld
    {
        public static string GetMessage() => ""Hello from Source Generator!"";
    }
}
";

        // Add the source code to the compilation
        context.AddSource(
            "HelloWorldGenerator.g.cs", 
            SourceText.From(sourceCode, Encoding.UTF8));
    }
}

How it works:

  1. [Generator] attribute marks the class as a Source Generator
  2. Execute runs during compilation
  3. context.AddSource adds the generated file to the compilation
  4. The .g.cs suffix is a convention indicating generated code

Usage in your project:

using GeneratedCode;

Console.WriteLine(HelloWorld.GetMessage()); 
// Output: Hello from Source Generator!

πŸ’‘ Pro tip: Generated files appear in Solution Explorer under Dependencies β†’ Analyzers β†’ YourGenerator β†’ YourGeneratorName

Example 2: Attribute-Driven Generation 🏷️

A common pattern is generating code based on custom attributes. Let's create a generator that automatically implements ToString() for classes marked with [AutoToString]:

Step 1: Define the attribute (in your main project):

[AttributeUsage(AttributeTargets.Class)]
public class AutoToStringAttribute : Attribute { }

Step 2: Create the generator:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Linq;
using System.Text;

[Generator]
public class ToStringGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not SyntaxReceiver receiver)
            return;

        foreach (var classDeclaration in receiver.CandidateClasses)
        {
            var model = context.Compilation.GetSemanticModel(classDeclaration.SyntaxTree);
            var classSymbol = model.GetDeclaredSymbol(classDeclaration);

            if (classSymbol == null) continue;

            // Check if class has [AutoToString] attribute
            if (!classSymbol.GetAttributes().Any(a => 
                a.AttributeClass?.Name == "AutoToStringAttribute"))
                continue;

            var source = GenerateToStringMethod(classSymbol);
            context.AddSource($"{classSymbol.Name}_ToString.g.cs", source);
        }
    }

    private string GenerateToStringMethod(INamedTypeSymbol classSymbol)
    {
        var properties = classSymbol.GetMembers()
            .OfType<IPropertySymbol>()
            .Where(p => p.DeclaredAccessibility == Accessibility.Public)
            .ToList();

        var sb = new StringBuilder();
        sb.AppendLine($"namespace {classSymbol.ContainingNamespace}");
        sb.AppendLine("{");
        sb.AppendLine($"    partial class {classSymbol.Name}");
        sb.AppendLine("    {");
        sb.AppendLine("        public override string ToString()");
        sb.AppendLine("        {");
        sb.AppendLine($"            return $\"{classSymbol.Name} {{ ");
        
        var propStrings = properties.Select(p => 
            $"{p.Name}={{{p.Name}}}");
        sb.AppendLine($"                {string.Join(", ", propStrings)} }}\");
        
        sb.AppendLine("        }");
        sb.AppendLine("    }");
        sb.AppendLine("}");
        
        return sb.ToString();
    }

    private class SyntaxReceiver : ISyntaxReceiver
    {
        public List<ClassDeclarationSyntax> CandidateClasses { get; } = new();

        public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
        {
            if (syntaxNode is ClassDeclarationSyntax classDeclaration &&
                classDeclaration.AttributeLists.Count > 0)
            {
                CandidateClasses.Add(classDeclaration);
            }
        }
    }
}

Step 3: Use it in your code:

[AutoToString]
public partial class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Generated code automatically provides:
var person = new Person { Name = "Alice", Age = 30 };
Console.WriteLine(person.ToString());
// Output: Person { Name=Alice, Age=30 }

Key concepts demonstrated:

  • ISyntaxReceiver: Efficiently filters syntax nodes during parsing
  • Partial classes: Generated code extends your class using partial
  • Semantic analysis: GetSemanticModel provides type information
  • Symbol inspection: Navigate properties, methods, attributes via symbols

⚠️ Common mistake: Forgetting partial keyword on your class. Generated code must extend an existing partial class.

Example 3: Incremental Generator with Value Provider πŸš€

Modern generators use the incremental API for better IDE performance:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Linq;

[Generator]
public class IncrementalToStringGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // Create a pipeline that finds classes with attributes
        var classDeclarations = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: static (node, _) => IsSyntaxTargetForGeneration(node),
                transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx))
            .Where(static m => m is not null);

        // Generate source for each class
        context.RegisterSourceOutput(classDeclarations, 
            static (spc, source) => Execute(source, spc));
    }

    private static bool IsSyntaxTargetForGeneration(SyntaxNode node)
    {
        return node is ClassDeclarationSyntax { AttributeLists.Count: > 0 };
    }

    private static INamedTypeSymbol? GetSemanticTargetForGeneration(
        GeneratorSyntaxContext context)
    {
        var classDeclaration = (ClassDeclarationSyntax)context.Node;
        var symbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration);

        if (symbol?.GetAttributes().Any(a => 
            a.AttributeClass?.Name == "AutoToStringAttribute") == true)
        {
            return symbol;
        }

        return null;
    }

    private static void Execute(INamedTypeSymbol? classSymbol, 
        SourceProductionContext context)
    {
        if (classSymbol == null) return;

        var source = GenerateToStringMethod(classSymbol);
        context.AddSource($"{classSymbol.Name}_ToString.g.cs", source);
    }

    private static string GenerateToStringMethod(INamedTypeSymbol classSymbol)
    {
        // Same implementation as Example 2
        // ... (omitted for brevity)
        return "/* generated code */";
    }
}

Incremental advantages:

  • Caching: Only reprocesses changed files
  • Parallel execution: Multiple pipelines run concurrently
  • IDE responsiveness: Minimal recomputation on typing

Pipeline flow:

Syntax Provider β†’ Filter (predicate) β†’ Transform (semantic) 
                                              ↓
                                         Cache Results
                                              ↓
                                     Source Output (generate)

Example 4: Real-World Pattern - JSON Serializer πŸ“¦

Let's build a practical generator that creates optimized JSON serializers:

[Generator]
public class JsonSerializerGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var classDeclarations = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: static (node, _) => node is ClassDeclarationSyntax cls &&
                    cls.AttributeLists.Any(),
                transform: static (ctx, _) => GetJsonSerializableClass(ctx))
            .Where(static m => m is not null);

        context.RegisterSourceOutput(classDeclarations,
            static (spc, source) => GenerateSerializer(source!, spc));
    }

    private static INamedTypeSymbol? GetJsonSerializableClass(
        GeneratorSyntaxContext context)
    {
        var classDeclaration = (ClassDeclarationSyntax)context.Node;
        var symbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration);

        if (symbol?.GetAttributes().Any(a => 
            a.AttributeClass?.Name == "JsonSerializableAttribute") == true)
        {
            return symbol;
        }
        return null;
    }

    private static void GenerateSerializer(INamedTypeSymbol classSymbol,
        SourceProductionContext context)
    {
        var properties = classSymbol.GetMembers()
            .OfType<IPropertySymbol>()
            .Where(p => p.DeclaredAccessibility == Accessibility.Public)
            .ToList();

        var source = $$"""
            using System.Text;
            
            namespace {{classSymbol.ContainingNamespace}}
            {
                public static class {{classSymbol.Name}}JsonSerializer
                {
                    public static string ToJson({{classSymbol.Name}} obj)
                    {
                        var sb = new StringBuilder();
                        sb.Append("{");
                        {{GeneratePropertySerializers(properties)}}
                        sb.Append("}");
                        return sb.ToString();
                    }
                }
            }
            """;

        context.AddSource($"{classSymbol.Name}JsonSerializer.g.cs", source);
    }

    private static string GeneratePropertySerializers(
        List<IPropertySymbol> properties)
    {
        var sb = new StringBuilder();
        for (int i = 0; i < properties.Count; i++)
        {
            var prop = properties[i];
            var comma = i < properties.Count - 1 ? "," : "";
            
            sb.AppendLine($"sb.Append(\"\\\"{prop.Name}\\\":\");");
            
            if (prop.Type.SpecialType == SpecialType.System_String)
            {
                sb.AppendLine($"sb.Append('\"');");
                sb.AppendLine($"sb.Append(obj.{prop.Name});");
                sb.AppendLine($"sb.Append('\"');");
            }
            else
            {
                sb.AppendLine($"sb.Append(obj.{prop.Name});");
            }
            
            sb.AppendLine($"sb.Append(\"{comma}\");");
        }
        return sb.ToString();
    }
}

Usage:

[JsonSerializable]
public partial class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
}

// Generated serializer available at compile time:
var product = new Product 
{ 
    Name = "Widget", 
    Price = 29.99m, 
    Stock = 100 
};

string json = ProductJsonSerializer.ToJson(product);
// {"Name":"Widget","Price":29.99,"Stock":100}

Performance comparison:

MethodTime (Β΅s)Allocations (bytes)
System.Text.Json (reflection)1,2503,840
Generated serializer85480
Speedup14.7x faster8x less memory

πŸ’‘ Real-world applications:

  • ORMs: Generate SQL queries and mapping code (e.g., Dapper.AOT)
  • Dependency Injection: Build container registrations at compile time
  • Validation: Create validators from data annotation attributes
  • APIs: Generate client/server code from OpenAPI specs
  • Logging: Structured logging with compile-time message formatting

Common Mistakes to Avoid ⚠️

1. Generating Invalid C# Code

❌ Wrong:

var source = $"public class {className} {{ }}";
// If className contains special characters, this breaks!

βœ… Right:

var source = $"public class {SyntaxFacts.IsValidIdentifier(className) ? className : "GeneratedClass"} {{ }}";
// Or use Roslyn's syntax factory:
var classDeclaration = ClassDeclaration(className);

2. Forgetting Namespaces in Generated Code

❌ Wrong:

var source = "public class MyClass { public List<string> Items; }";
// List<string> won't resolve without System.Collections.Generic

βœ… Right:

var source = @"
using System.Collections.Generic;

public class MyClass { public List<string> Items; }
";

3. Not Using Partial Classes

❌ Wrong:

// User code:
public class Person { }

// Generated code tries to redefine:
public class Person { /* generated members */ }
// Compilation error: duplicate definition!

βœ… Right:

// User code:
public partial class Person { }

// Generated code extends:
public partial class Person { /* generated members */ }

4. Poor Performance in ISourceGenerator

❌ Wrong:

public void Execute(GeneratorExecutionContext context)
{
    // Iterating ALL syntax trees on EVERY compilation!
    foreach (var tree in context.Compilation.SyntaxTrees)
    {
        // Slow, runs even when nothing changed
    }
}

βœ… Right:

public void Initialize(GeneratorInitializationContext context)
{
    // Register syntax receiver to filter early
    context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
}

// Or better: use IIncrementalGenerator for automatic caching

5. Not Handling Nullable Reference Types

❌ Wrong:

var model = context.Compilation.GetSemanticModel(tree);
var symbol = model.GetDeclaredSymbol(node); // Could be null!
var name = symbol.Name; // NullReferenceException!

βœ… Right:

var model = context.Compilation.GetSemanticModel(tree);
if (model.GetDeclaredSymbol(node) is not INamedTypeSymbol symbol)
    return;
var name = symbol.Name; // Safe

6. Generating Colliding Filenames

❌ Wrong:

context.AddSource("Generated.cs", source);
// If generator runs multiple times, same filename causes issues

βœ… Right:

context.AddSource($"{classSymbol.Name}_{Guid.NewGuid()}.g.cs", source);
// Or use deterministic naming:
context.AddSource($"{classSymbol.ContainingNamespace}.{classSymbol.Name}.g.cs", source);

Debugging Your Generators πŸ”

Debugging is tricky because generators run inside the compiler process. Here's how:

Method 1: Debugger.Launch()

[Generator]
public class MyGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        #if DEBUG
        if (!Debugger.IsAttached)
        {
            Debugger.Launch();
        }
        #endif
        
        // Your generator code
    }
}

Method 2: EmitCompilerGeneratedFiles

Add to your .csproj:

<PropertyGroup>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Generated files appear in obj/Generated/ for inspection.

Method 3: Unit Testing Generators

[Fact]
public void Generator_CreatesExpectedOutput()
{
    var source = @"
[AutoToString]
public partial class TestClass
{
    public string Name { get; set; }
}
";

    var compilation = CreateCompilation(source);
    var generator = new ToStringGenerator();

    GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
    driver = driver.RunGeneratorsAndUpdateCompilation(
        compilation, out var outputCompilation, out var diagnostics);

    Assert.Empty(diagnostics);
    Assert.Contains("public override string ToString()", 
        outputCompilation.SyntaxTrees.Last().ToString());
}

Advanced Patterns 🎯

Pattern 1: Configuration via AdditionalFiles

Generators can read external config files:

public void Execute(GeneratorExecutionContext context)
{
    var configFile = context.AdditionalFiles
        .FirstOrDefault(f => f.Path.EndsWith("generator.config"));
    
    if (configFile != null)
    {
        var configText = configFile.GetText()?.ToString();
        // Parse and use configuration
    }
}

In .csproj:

<ItemGroup>
    <AdditionalFiles Include="generator.config" />
</ItemGroup>

Pattern 2: Marker Interfaces

Generate code for types implementing specific interfaces:

private static INamedTypeSymbol? GetTargetType(GeneratorSyntaxContext context)
{
    var typeSymbol = context.SemanticModel.GetDeclaredSymbol(context.Node) 
        as INamedTypeSymbol;
    
    if (typeSymbol?.AllInterfaces.Any(i => 
        i.Name == "ISerializable") == true)
    {
        return typeSymbol;
    }
    return null;
}

Pattern 3: Multi-Stage Generation

Generate code that other generators consume:

[Generator]
public class Stage1Generator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(ctx =>
        {
            // This code is available to other generators
            ctx.AddSource("SharedAttributes.g.cs", @"
namespace Generated
{
    [System.AttributeUsage(System.AttributeTargets.Class)]
    public class Stage2Attribute : System.Attribute { }
}
");
        });
    }
}

Performance Optimization Tips πŸš€

⚑ Performance Checklist

Use IIncrementalGeneratorAutomatic caching and incremental rebuilds
Filter early with predicatesAvoid unnecessary semantic analysis
Cache symbol comparisonsUse SymbolEqualityComparer
Minimize allocationsUse StringBuilder, avoid LINQ when hot
Batch AddSource callsOne file per symbol, not many tiny files

Before optimization:

foreach (var cls in context.Compilation.SyntaxTrees
    .SelectMany(t => t.GetRoot().DescendantNodes())
    .OfType<ClassDeclarationSyntax>())
{
    // Processes EVERY class in EVERY compilation
}

After optimization:

var provider = context.SyntaxProvider.CreateSyntaxProvider(
    predicate: static (node, _) => 
        node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
    transform: static (ctx, _) => /* only process candidates */
);
// Only processes classes with attributes, caches results

Key Takeaways πŸŽ“

βœ… Source Generators generate code at compile time, eliminating reflection overhead

βœ… Use IIncrementalGenerator for modern, performant generators with automatic caching

βœ… Filter early with syntax predicates before expensive semantic analysis

βœ… Partial classes are required for generators to extend user-defined types

βœ… Semantic models provide type information; syntax trees provide structure

βœ… Test generators using CSharpGeneratorDriver and unit tests

βœ… Debug with Debugger.Launch() or EmitCompilerGeneratedFiles property

βœ… Common use cases: serialization, DI, validation, ORM mapping, logging

βœ… Follow naming conventions: *.g.cs suffix, deterministic filenames

βœ… Handle nullability properly when working with symbols and semantic models

πŸ“‹ Quick Reference Card: Source Generator Essentials

ConceptKey Info
Generator InterfaceISourceGenerator (legacy) or IIncrementalGenerator (modern)
Main MethodsInitialize() - setup, Execute() - generate code
Add Generated Codecontext.AddSource(filename, sourceText)
File NamingUse .g.cs suffix, unique names per symbol
Required Keywords[Generator] attribute, partial classes for extension
Syntax FilteringISyntaxReceiver or CreateSyntaxProvider
Type InformationGetSemanticModel() β†’ GetDeclaredSymbol()
DebuggingDebugger.Launch(), EmitCompilerGeneratedFiles
PerformanceUse incremental generators, filter early, cache results

πŸ“š Further Study

Ready to dive deeper? Explore these resources:

  1. Official Microsoft Docs: Source Generators Cookbook - Comprehensive guide with patterns and recipes

  2. Andrew Lock's Blog Series: Creating a source generator - Detailed tutorials building real-world generators step-by-step

  3. Source Generator Samples Repository: dotnet/roslyn-sdk - Official Microsoft examples covering common scenarios


πŸŽ‰ Congratulations! You now understand how Source Generators work, from basic attribute-driven generation to advanced incremental patterns. Start by creating a simple generator for your next projectβ€”automate that repetitive code and enjoy the compile-time performance boost!