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:
| Concept | Purpose | What You Get |
|---|---|---|
| Syntax Tree | Structure of code | Nodes, tokens, trivia (comments/whitespace) |
| Semantic Model | Meaning of code | Types, 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 resolvesstringtoSystem.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:
[Generator]attribute marks the class as a Source GeneratorExecuteruns during compilationcontext.AddSourceadds the generated file to the compilation- The
.g.cssuffix 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:
GetSemanticModelprovides 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:
| Method | Time (Β΅s) | Allocations (bytes) |
|---|---|---|
| System.Text.Json (reflection) | 1,250 | 3,840 |
| Generated serializer | 85 | 480 |
| Speedup | 14.7x faster | 8x 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 IIncrementalGenerator | Automatic caching and incremental rebuilds |
| Filter early with predicates | Avoid unnecessary semantic analysis |
| Cache symbol comparisons | Use SymbolEqualityComparer |
| Minimize allocations | Use StringBuilder, avoid LINQ when hot |
| Batch AddSource calls | One 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
| Concept | Key Info |
|---|---|
| Generator Interface | ISourceGenerator (legacy) or IIncrementalGenerator (modern) |
| Main Methods | Initialize() - setup, Execute() - generate code |
| Add Generated Code | context.AddSource(filename, sourceText) |
| File Naming | Use .g.cs suffix, unique names per symbol |
| Required Keywords | [Generator] attribute, partial classes for extension |
| Syntax Filtering | ISyntaxReceiver or CreateSyntaxProvider |
| Type Information | GetSemanticModel() β GetDeclaredSymbol() |
| Debugging | Debugger.Launch(), EmitCompilerGeneratedFiles |
| Performance | Use incremental generators, filter early, cache results |
π Further Study
Ready to dive deeper? Explore these resources:
Official Microsoft Docs: Source Generators Cookbook - Comprehensive guide with patterns and recipes
Andrew Lock's Blog Series: Creating a source generator - Detailed tutorials building real-world generators step-by-step
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!