BrandGhost
C# Source Generators: A Complete Guide to Compile-Time Code Generation

C# Source Generators: A Complete Guide to Compile-Time Code Generation

C# Source Generators: A Complete Guide to Compile-Time Code Generation

If you've spent any time writing .NET code, you've felt the pain. Repetitive boilerplate, copy-pasted patterns, fragile reflection calls at runtime. Every new service needs the same structural scaffolding. Every new DTO demands the same mapping logic. It's mechanical work -- and mechanical work is exactly what computers are supposed to do for us.

That's where C# source generators come in. They let you automate code generation at compile time, producing new C# source files as part of your normal build process. No runtime overhead. No brittle reflection. No manual copy-paste. Just clean, generated code that looks like code you wrote yourself -- because in a very real sense, you did write it, just with a generator instead of your keyboard.

This guide is for .NET developers who have heard of C# source generators but haven't built one yet. You'll learn what they are, how they fit into the build pipeline, when to use them, and how to write your first working generator using the modern IIncrementalGenerator API.

What Are C# Source Generators?

C# source generators are a feature of the Roslyn compiler that allow you to inspect your code during compilation and inject new source files into the compilation pipeline. They were introduced in .NET 5 and have matured significantly -- the current best practice uses the incremental generator API introduced to address performance issues in the original design.

At the most basic level, a source generator is a class that implements a specific interface and gets invoked by the compiler. You hand it the syntax tree and semantic model of your codebase, and it hands back generated C# code. That generated code is compiled alongside your hand-written code as if you had typed it yourself. It shows up in your IDE with full IntelliSense support. "Go to Definition" works on generated members. Type errors in generated code surface as build errors -- not mysterious runtime failures.

C# source generators were introduced to solve a class of problems that previously required awkward workarounds:

  • Reflection-based code that is slow, opaque, and breaks under AOT (ahead-of-time) compilation
  • T4 templates that run outside the build pipeline and produce hard-to-maintain output
  • Manual boilerplate that creates maintenance nightmares as codebases grow
  • Runtime code generation that cannot be validated until the application is actually running

The key insight is that C# source generators work with the compiler, not around it. They are first-class participants in the build process, not afterthoughts bolted onto the side.

How Source Generators Fit Into the .NET Build Pipeline

Understanding where C# source generators plug into the build pipeline helps you reason about what they can and cannot do -- and why they are fundamentally different from other code generation approaches.

When you build a .NET project, the Roslyn compiler processes your code in several phases. It parses your source files into syntax trees, performs semantic analysis to understand types and relationships, and then emits the compiled output. Source generators participate between the semantic analysis phase and the emit phase.

Here is what happens step by step:

  1. Roslyn parses all your source files into syntax trees
  2. Roslyn performs semantic analysis, building the full symbol model
  3. Your source generators run -- they receive the complete compilation context and can inspect all syntax trees and semantic models
  4. Generators emit new source files back into the compilation
  5. Roslyn compiles everything together, including generated files, into the final assembly

This means generators have full access to the type system at the point they run. They can check whether a class implements a given interface, what attributes are applied to a type or member, what method signatures look like, and how types relate to each other. That's powerful analytical capability baked right into the build.

The generated files land at obj/{Configuration}/{TargetFramework}/generated/{GeneratorAssembly}/{GeneratorFullTypeName}/{HintName}.g.cs (for example, obj/Debug/net10.0/generated/MyLib/MyGen.MyGenerator/Output.g.cs). To persist generated files to disk for inspection, set <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> in your project file. Your IDE indexes these files automatically, so you get complete tooling support. One important constraint worth understanding: source generators can only add code. They cannot modify existing code. This design decision keeps generators composable and prevents conflicts when multiple generators target the same codebase.

Source Generators vs Reflection vs T4 Templates

Before committing to C# source generators, it's worth understanding how they compare to the alternatives you've probably already used.

Approach When Code Runs AOT Compatible Performance IDE Support Type Safety
Reflection Runtime ❌ No Slow Limited Runtime only
T4 Templates Pre-build (manual) ✅ Yes Fast Partial Partial
C# Source Generators Compile time ✅ Yes Fast Full Compile time

Reflection is the most flexible approach but carries significant costs. It runs at runtime, adding latency on hot paths. It bypasses the type system in ways that surface errors only at runtime. Most importantly, it is fundamentally incompatible with native AOT compilation -- a growing concern as .NET scenarios expand to cloud functions, edge computing, and mobile targets where reflection support is limited or unavailable.

T4 templates have been around since .NET 3.5 and have a role in code generation, but they run outside the build pipeline, require manual invocation, and use their own templating syntax that is entirely separate from C#. Refactoring tools do not understand T4 output the way they understand source-generated code.

C# source generators give you the best of all approaches: compile-time execution with full semantic context, automatic integration with every build, zero runtime overhead, and complete IDE tooling support. The generated code is ordinary C# that your tools understand completely -- debuggable, navigable, and type-safe.

ISourceGenerator vs IIncrementalGenerator

If you have looked at older C# source generator examples, you have almost certainly seen the ISourceGenerator interface. That interface is now considered legacy. IIncrementalGenerator was introduced in .NET 6 / Roslyn 4.0 -- for any new generator targeting .NET 6+, prefer it over the legacy API.

The problem with ISourceGenerator is straightforward: it runs on every compilation, regardless of what changed. In a large project with multiple generators, each keystroke in the IDE triggers a full regeneration. The compiler has no way to know which generators are affected by a given change, so all of them run. This adds up to noticeable slowdowns, particularly in IDEs that run the compiler continuously in the background.

IIncrementalGenerator solves this with a pipeline-based model. Instead of writing imperative "inspect everything and generate code," you declare a graph of data transformations. You describe which inputs your generator depends on -- syntax nodes matching a certain shape, specific attribute applications, additional files -- and how those inputs are transformed to outputs. The Roslyn infrastructure tracks which inputs changed between builds and re-executes only the affected portions of the pipeline.

For incremental builds -- the kind you perform constantly during active development -- this difference is dramatic. Well-designed incremental generators add negligible overhead to compilation. The ISourceGenerator interface is not being removed, but it receives no investment and the official documentation directs developers to prefer IIncrementalGenerator for new generator work.

The practical rule is simple: for any new generator targeting .NET 6+, prefer IIncrementalGenerator. The incremental API requires a bit more upfront thought about data flow, but the performance payoff makes that investment worthwhile from the very first project that uses your generator.

Key Use Cases for C# Source Generators

C# source generators are a broadly applicable tool, but they deliver the most value in specific scenarios. Here are the ones you'll encounter most often.

Boilerplate Elimination

The most immediately obvious use case is eliminating repetitive structural code. If you write the same five-line property notification pattern for every bindable property, or the same equality implementation for every value type, that repetition is the smell of a generator opportunity. A generator can produce all of that scaffolding from a single attribute applied to your type.

The .NET runtime itself exemplifies this. System.Text.Json uses a source generator to produce type-specific serialization code, replacing reflection-based serialization with generated methods that are faster, fully debuggable, and AOT-compatible. The LoggerMessage source generator produces optimized logging calls. These are not experimental features -- they are the recommended approach for performance-sensitive code.

AOT Compatibility

Native AOT compilation trims the runtime, removes reflection support, and produces smaller, faster binaries. This deployment model is increasingly relevant for cloud functions, containers, and edge scenarios. Any code that relies on reflection -- discovering types at runtime, invoking methods by name, reading attributes dynamically -- breaks under native AOT.

C# source generators resolve this by moving type discovery and code branching from runtime to compile time. Instead of "find all implementations of IHandler at startup," a generator produces "here are all implementations, wired up, ready to call." The AOT-compiled output has no need for reflection because the generator already did that work.

Dependency Injection Registration

Manual DI registration is tedious and error-prone. Adding a new service class means remembering to register it, and forgetting causes runtime failures that are confusing to diagnose. A source generator can scan your assembly at compile time, find all classes with a registration attribute, and generate the registration code automatically.

When working with patterns like the factory method pattern, a generator can automatically produce factory registration code -- no manual container configuration required for each new factory you add. The generator sees the attribute, emits the registration call, and your DI container is always in sync with your implementation classes.

Similarly, the builder pattern is a great candidate for generation. A generator can produce a complete builder class from a simple attribute applied to your target type, including all the fluent With* methods and the Build method -- reducing an entire class file to a single attribute declaration.

Design Pattern Scaffolding

Several classical design patterns involve significant structural boilerplate that is highly regular. C# source generators can produce that boilerplate reliably and consistently.

Consider the decorator pattern. A decorator wraps an interface and forwards all calls to an inner implementation, adding behavior before or after each call. For an interface with many members, writing this by hand is tedious and error-prone -- any new method added to the interface needs a corresponding forwarding call in every decorator. A generator can produce the entire forwarding layer from the interface definition, keeping decorators in sync automatically.

The singleton pattern benefits from generators too. A [Singleton] attribute could trigger generation of a thread-safe lazy initialization implementation, eliminating the risk of subtle concurrency bugs in hand-rolled singletons.

For the strategy pattern, a generator can produce a dispatch table that routes to the correct strategy implementation based on a discriminator value -- removing switch statements from your hot path and keeping the dispatch logic synchronized with the set of registered strategies.

Plugin and Module Discovery

C# source generators are particularly well-suited to plugin-based architectures. Instead of scanning assemblies at runtime to discover plugin types -- which requires reflection and is incompatible with AOT -- a generator can enumerate all types implementing a plugin interface at compile time and generate a registration method. The runtime cost drops to a single method call with no reflection involved.

What You'll Learn in This Series

This article is the hub of a series on C# source generators. Each topic builds on the foundation established here, taking you from basic understanding to production-ready implementations.

The next articles dig into the incremental generator pipeline in depth -- how to model inputs as IncrementalValueProvider<T> and IncrementalValuesProvider<T>, how to structure transformations to maximize cache reuse, and how to avoid the common mistakes that undermine the performance benefits of the incremental model.

Attribute-driven generation gets dedicated coverage. Most real-world generators use custom attributes as metadata -- a marker that says "generate code for this type." You'll learn how to define and apply custom attributes, how to find them efficiently using the syntax and semantic APIs, and how to validate attribute usage and emit diagnostics when developers misuse your generator.

Testing generators is a topic that deserves its own treatment. The Roslyn testing infrastructure lets you write unit tests that verify the exact text of generated code, assert that specific diagnostics are produced, and confirm that invalid usage surfaces as build errors. These tests run fast and give you high confidence in generator correctness without requiring a live build.

Later articles cover practical use cases in depth: a strongly-typed enum extension generator, an interface proxy generator, a compile-time validated configuration generator, and a performance comparison that puts generated serialization up against reflection-based alternatives. Each article is self-contained but builds on the vocabulary and mental models introduced here.

Getting Started: Your First Source Generator

Let's write a real, working source generator. This example generates a static helper class with a greeting method -- simple enough to understand the structure, real enough to build on.

Step 1: Create the Generator Project

Source generator projects must target netstandard2.0. This is a Roslyn requirement: generators run inside the compiler process, which may itself be running on older runtimes. Your consumer project can target any framework.

<!-- MyGenerators/MyGenerators.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Library</OutputType>
    <!-- Generators MUST target netstandard2.0 to run in all host environments -->
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>latest</LangVersion>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <!-- PrivateAssets="all" prevents these from becoming transitive dependencies -->
    <!-- Use the version bundled with your SDK; for .NET 10, that is 4.12.0 or later -->
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
  </ItemGroup>
</Project>

Step 2: Implement IIncrementalGenerator

Here's a minimal generator using the modern IIncrementalGenerator interface:

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

namespace MyGenerators;

// The [Generator] attribute tells Roslyn to include this class in the generator pipeline
[Generator]
public sealed class HelloWorldGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // RegisterPostInitializationOutput runs unconditionally on every build.
        // Use this for generated code that doesn't depend on user-written code.
        context.RegisterPostInitializationOutput(ctx =>
        {
            // AddSource injects a new file into the compilation.
            // The hint name ("HelloWorld.g.cs") becomes the generated file's name in the output path.
            ctx.AddSource(
                "HelloWorld.g.cs",
                SourceText.From("""
                    // <auto-generated/>
                    // Generated at compile time -- do not edit manually.

                    namespace MyApp;

                    public static partial class Greetings
                    {
                        // This method exists entirely due to source generation.
                        // No reflection. No runtime lookup. Just compiled C#.
                        public static string GetWelcome() => "Hello from a C# source generator!";
                    }
                    """, Encoding.UTF8));
        });
    }
}

Step 3: Reference the Generator in Your Consumer Project

The consumer project references the generator using a special ProjectReference with additional attributes that tell MSBuild how to treat the dependency:

<!-- MyApp/MyApp.csproj -->
<ItemGroup>
  <!-- OutputItemType="Analyzer" tells MSBuild this is a generator, not a runtime reference. -->
  <!-- ReferenceOutputAssembly="false" ensures the DLL isn't included in your app's output. -->
  <ProjectReference Include="..MyGeneratorsMyGenerators.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

Step 4: Use the Generated Code

After building, the generated class is available with full IDE support:

// Program.cs
using MyApp;

// Greetings.GetWelcome() was generated at compile time.
// IntelliSense works, "Go to Definition" navigates to the generated file.
Console.WriteLine(Greetings.GetWelcome());
// Output: Hello from a C# source generator!

The generated file appears at obj/{Configuration}/{TargetFramework}/generated/MyGenerators/MyGenerators.HelloWorldGenerator/HelloWorld.g.cs (for example, obj/Debug/net10.0/generated/MyGenerators/MyGenerators.HelloWorldGenerator/HelloWorld.g.cs). Your IDE reads it automatically -- no manual steps required.

This is the simplest possible generator. Real generators are more interesting: they inspect your compilation looking for specific patterns -- attribute applications, interface implementations, naming conventions -- and generate code tailored to what they find. The RegisterPostInitializationOutput approach here generates code unconditionally. Production generators use CreateSyntaxProvider or ForAttributeWithMetadataName (Roslyn 4.3+, available from the .NET 7 SDK / Visual Studio 17.3+) to generate code conditionally based on user code. But the structural skeleton here -- [Generator], IIncrementalGenerator, Initialize, AddSource -- is what every C# source generator is built on.

Frequently Asked Questions

Do C# source generators work with .NET Framework?

Yes, with a caveat. C# source generators work in projects that use the Roslyn compiler via SDK-style project files. Modern SDK-style .csproj files support generators regardless of target framework -- you can generate code for a .NET Framework 4.8 project. The generator project itself must target netstandard2.0, but the consuming project can target any framework. Legacy non-SDK .csproj formats do not support generators.

Can source generators read external files like JSON or XML?

Yes. The AdditionalTextsProvider on IncrementalGeneratorInitializationContext gives you access to files included in the project via <AdditionalFiles> in the .csproj. This is how strongly typed resource generators, OpenAPI client generators, and configuration schema generators work -- they read an external file, parse it, and emit C# code that represents the file's structure as types or constants.

Why must generator projects target netstandard2.0?

Source generators run inside the Roslyn compiler process. That process may be running inside Visual Studio (which uses the .NET Framework host), JetBrains Rider, the dotnet CLI, or MSBuild. Targeting netstandard2.0 ensures your generator is compatible with all of these hosts. If you try to use .NET-specific APIs in your generator project, it will fail to load in some environments. This is one of the reasons generator projects are separate from the libraries they support.

How do I debug a source generator?

The most direct approach is to add System.Diagnostics.Debugger.Launch() inside your Initialize method temporarily. When the generator runs, it prompts you to attach a debugger. Remove this before shipping. For a more systematic approach, the Microsoft.CodeAnalysis.CSharp.Testing NuGet package provides CSharpIncrementalGeneratorTest<TGenerator, XunitVerifier> (with using Microsoft.CodeAnalysis.CSharp.Testing;) that lets you write xUnit tests specifying input source and expected generated output. These tests run entirely in-process and support standard debugging.

Are C# source generators slow to build with?

Well-designed incremental generators have minimal compile-time overhead. The Roslyn caching infrastructure tracks which inputs changed and skips re-executing pipeline stages whose inputs are unchanged. The key to keeping generators fast is structuring your pipeline so that expensive operations -- semantic analysis, string generation -- are gated on specific, narrow inputs rather than running on every syntax node. Poorly designed generators that ignore the incremental model can add noticeable overhead, which is one of the primary motivations for the IIncrementalGenerator API.

Can a source generator access the filesystem or network?

Source generators should not access the filesystem or network beyond AdditionalTextsProvider. Generators run inside the compiler process, and non-deterministic I/O breaks the incremental caching model -- if a generator's output can change based on external state that Roslyn doesn't track, cache invalidation becomes unreliable and builds can differ between machines. Stick to the compilation context and additional files that are part of the project. If you need external data to drive generation, add it as an <AdditionalFiles> item.

What happens to generated code in version control?

Generated files land in the obj/ folder, which is gitignored by default. This is intentional and correct -- generated files are build artifacts, not source files, and should not be committed any more than compiled binaries. The generator source code (your IIncrementalGenerator implementation) is what belongs in version control. To inspect what a generator produces, build the project and look in obj/{Configuration}/{TargetFramework}/generated/{GeneratorAssembly}/{GeneratorFullTypeName}/.

Conclusion

C# source generators are one of the most powerful tools in the modern .NET developer's arsenal. They move code generation from a fragile runtime concern into the compilation pipeline, where it is fast, type-safe, AOT-compatible, and completely supported by IDE tooling.

The IIncrementalGenerator interface is the right foundation for all new generator work. It delivers the performance characteristics needed for a smooth development experience -- incremental caching means generators stay fast even in large codebases -- while keeping the API focused on describing data flows rather than imperative code execution.

Getting started is genuinely approachable: create a netstandard2.0 class library, implement IIncrementalGenerator, reference it from your consumer project with OutputItemType="Analyzer", and build. From there, the capabilities of C# source generators grow as fast as your use cases demand. Whether you are eliminating boilerplate, enabling AOT compilation, automating DI registration, or generating pattern scaffolding, the investment in learning source generators pays dividends across every project they touch.

The rest of this series covers all of that in depth. Start here, build the mental model, and the more advanced techniques will click much more quickly.

Source Generation vs Reflection in Needlr: Choosing the Right Approach

Compare Needlr's source generation and reflection strategies for dependency injection in C# to choose the right approach for AOT, performance, and flexibility.

RAG with Semantic Kernel in C#: Complete Guide to Retrieval-Augmented Generation

Master RAG with Semantic Kernel in C# using vector stores, embeddings, and InMemoryVectorStore. Complete guide with working .NET code examples.

Automatic Dependency Injection in C#: The Complete Guide to Needlr

Learn how Needlr simplifies dependency injection in C# with automatic service discovery, source generation, and a fluent API for .NET applications.

An error has occurred. This application may no longer respond until reloaded. Reload