How C# Source Generators Work at Compile-Time
C# Source Generators are a Roslyn compiler feature that inspects a codebase during compilation and emits additional C# source files directly into the build pipeline. This article explains the underlying mechanics of how source generators hook into the Roslyn compiler, how they analyze existing code via syntax trees and semantic models, and how they produce new, strongly typed code without relying on runtime reflection or external post-build tools.
The Roslyn Compilation Hook
A source generator runs as an active phase within the Roslyn compilation pipeline rather than as a separate pre-build or post-build script. When you compile a project, Roslyn processes source files in distinct stages:
- Syntax Analysis: The compiler parses source files into Abstract Syntax Trees (ASTs).
- Semantic Analysis: The compiler resolves types, symbols, and references across all syntax trees.
- Generator Execution: The compiler hands the syntax trees and semantic models over to the source generator.
- Final Compilation: The compiler takes both original source files and generator-produced source files, emits the final Intermediate Language (IL), and produces the binary.
Because generators execute directly inside Roslyn, the generated code has access to the same language features, namespaces, and dependencies as handwritten code.
Additive-Only Execution
A foundational rule of C# source generators is that they are strictly additive. A generator cannot:
- Modify existing user code.
- Delete existing code.
- Alter method bodies or signatures defined in other files.
Instead, generators augment existing code. They achieve this
primarily through partial classes and partial
methods. If a user defines
public partial class UserViewModel, the generator can emit
another file containing public partial class UserViewModel
to add properties, methods, or interface implementations.
How the Generator Processes Code
Modern C# source generators typically implement the
IIncrementalGenerator interface, which optimizes build
performance through caching. The generation process works in three
distinct phases:
1. Filtering Syntax (Syntactic Phase)
The generator first registers a syntax provider that listens for specific code constructs, such as classes with a specific attribute or specific interface implementations. This step operates solely on raw syntax nodes, avoiding expensive semantic checks on code that does not matter to the generator.
context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax c && c.AttributeLists.Count > 0,
transform: static (ctx, _) => GetSemanticTarget(ctx)
);2. Inspecting Symbols (Semantic Phase)
Once candidate syntax nodes are isolated, the generator accesses
Roslyn's SemanticModel. This model provides full type
information, allowing the generator to resolve:
- Fully qualified type names.
- Applied attributes and their constructor arguments.
- Property types, accessibility modifiers, and inheritance hierarchies.
3. Emitting Source Code (Generation Phase)
Using the extracted metadata, the generator constructs new C# source code as text or a formatted string. It then passes this string back to the compilation context:
context.RegisterSourceOutput(source, static (spc, sourceText) =>
{
spc.AddSource("GeneratedClass.g.cs", SourceText.From(sourceText, Encoding.UTF8));
});The compiler adds this emitted text directly into memory as a new syntax tree for the remaining compilation steps.
Integration with the IDE
Source generators integrate directly with Visual Studio and the .NET Language Server. As a developer types, the IDE triggers the incremental generator in the background. Because the output is added to the compilation in memory, features like IntelliSense, code completion, and inline error diagnostics immediately recognize the generated members without requiring a manual project rebuild.
Key Advantages over Traditional Alternatives
- Zero Runtime Overhead: Boilerplate code, serialization logic, or dependency mappings are resolved at compile-time, eliminating the CPU and memory costs of runtime reflection.
- Native AOT Compatibility: Because all code is generated before IL generation, the resulting application is fully compatible with Ahead-Of-Time (AOT) compilation and tree-shaking.
- Type Safety: Any errors in generated code produce standard compile-time errors (CS codes), preventing silent runtime failures.