BrandGhost
What Are C# Source Generators? History, Concepts, and How They Fit in .NET

What Are C# Source Generators? History, Concepts, and How They Fit in .NET

If you have been writing C# for any meaningful amount of time, you have probably heard the phrase "source generators" pop up in conversations about performance, AOT compilation, or eliminating boilerplate. But what do they actually do -- and why does the .NET ecosystem keep talking about them? This article breaks down what C# source generators are, where they came from, how they work at a conceptual level, and why they matter more than ever in .NET 8, 9, and 10.

No prior experience with Roslyn or code generation required. We are starting from scratch.


The Problem Source Generators Solve

Every .NET developer eventually hits the same wall. You are writing the twentieth implementation of INotifyPropertyChanged. You are reflecting over a type at runtime to serialize it. You are maintaining a T4 template that nobody on the team understands. You are writing factory methods, builder classes, mapper implementations -- all by hand.

The pain is real. And it comes in three distinct flavors.

Boilerplate fatigue. Repetitive code is fragile code. The more you write by hand, the more places there are for subtle inconsistencies to creep in. Updating a model class means remembering to update all the downstream handwritten code that depends on it.

Reflection overhead. Reflection is powerful, but it is not free. Discovering types, reading attributes, and invoking methods dynamically at runtime carries a real cost -- both in time and in allocations. For hot paths, that cost matters. For startup-sensitive applications, it can be brutal.

AOT and trimming barriers. With Native AOT becoming a first-class citizen in modern .NET, reflection-heavy code is increasingly a problem. The AOT compiler needs to know ahead of time what types, methods, and members your application accesses. Reflection makes that analysis impossible without additional hints -- hints that are easy to forget and painful to maintain.

Source generators address all three. They run at compile time, they produce plain C# code that the compiler then compiles normally, and they leave zero runtime overhead.


A Brief History: From T4 Templates to Roslyn

To understand source generators in C#, it helps to know what came before them.

T4 templates (Text Template Transformation Toolkit) were introduced with Visual Studio 2008. They let you write a mix of C# and text directives that the IDE would execute to produce a .cs file. This solved the boilerplate problem in a limited way -- you could generate code. But T4 templates were awkward to write, difficult to integrate cleanly into CI pipelines, had no semantic awareness of your code, and produced output that required manual refresh. They felt bolted on, because they were.

Roslyn changed everything. Introduced alongside Visual Studio 2015 and C# 6, Roslyn is the open-source .NET compiler platform. It is not just a compiler -- it is a set of APIs that expose every stage of compilation as a programmable surface. Code analyzers, code fix providers, and refactoring tools all run on top of Roslyn. The key insight is that Roslyn gives you a fully typed semantic model of the code being compiled. You can ask questions like "give me all the classes that implement this interface" and get back a real answer, not a string-matched guess.

ISourceGenerator arrived in .NET 5 (C# 9). This was the first official source generator API. It exposed a GenerationContext that contained the compilation's syntax trees and semantic model. Generators implementing ISourceGenerator were invoked once per compilation and could add new source files. It worked, but it had a serious performance problem -- every keystroke in the IDE could trigger a full regeneration, and with complex generators this created noticeable IDE lag.

IIncrementalGenerator arrived in .NET 6 and is the current standard. Instead of re-running on every compilation, incremental generators describe a pipeline of transformations over the syntax tree. Only the parts of the pipeline that are actually affected by a change need to re-execute. This makes IDE responsiveness fast and generator execution far more efficient. If you are writing a source generator today -- or reading code that contains one -- IIncrementalGenerator is what you should expect to see.


What Source Generators Are (Exactly)

Let's be precise. A C# source generator is a component that:

  1. Implements IIncrementalGenerator (or the older ISourceGenerator)
  2. Is packaged as a Roslyn analyzer NuGet package
  3. Runs as part of the Roslyn compilation pipeline
  4. Receives a view of the syntax tree and semantic model for your project
  5. Emits additional C# source files that are added to the compilation

That last point is worth emphasizing. Source generators do not modify your existing code. They only add new code. The added code is compiled normally alongside everything you wrote by hand. There is no runtime trickery involved.

Here is a concrete mental model. When you run dotnet build, Roslyn starts compiling your project. Before emitting IL, it runs any source generators that are referenced. Each generator inspects the syntax tree -- looking for attributes, interface implementations, naming patterns, whatever the generator is designed to detect -- and produces new .cs files as strings. Those strings are added to the compilation as if you had written them yourself. The final IL contains the output of both your code and the generated code.

The generated files are typically visible in your IDE under Dependencies > Analyzers > [GeneratorName]. You can read them, which is enormously helpful for debugging.


What Source Generators Are NOT

This is equally important.

Source generators are not runtime code generation. They do not use System.Reflection.Emit, AssemblyBuilder, or DynamicMethod. Nothing is generated at runtime. All code is generated at compile time and compiled to IL before your application ever starts.

Source generators are not macros. C and C++ macros are textual substitutions performed before compilation. Source generators have full semantic awareness -- they work with the compiler's type system, not raw text.

Source generators are not T4 templates. T4 templates run outside the compiler and produce files you commit to source control. Source generators run inside the compiler pipeline and their output is transient -- not committed, always regenerated fresh from your source on each build.

Source generators are not post-build steps. They run during compilation, not after. The generated code participates in the same type-checking, nullability analysis, and optimization passes as all other code.

Source generators cannot modify existing code. This is a hard constraint of the API. If you want to modify a class you wrote, the pattern is to write a partial class -- you provide the user-facing portion, the generator provides the generated portion, and the compiler merges them.


How Source Generators Differ from Reflection

Reflection and source generators are often used to accomplish similar goals -- discovering type structure, generating adapters, reading attributes -- but they are fundamentally different in when and how they operate.

Consider a simple example. Suppose you want to implement a method that returns the names of all properties on a type.

Reflection-based approach (runtime):

// runtime reflection
public static IReadOnlyList<string> GetPropertyNames<T>()
{
    return typeof(T)
        .GetProperties(BindingFlags.Public | BindingFlags.Instance)
        .Select(p => p.Name)
        .ToArray();
}

// Usage
var names = GetPropertyNames<MyModel>();

This works, but it allocates on every call (unless you cache), it cannot be trimmed safely for AOT, and it fails silently if a property is trimmed away in a Native AOT build.

Source generator equivalent (compile-time):

// What a consumer writes
[GeneratePropertyNames]
public partial class MyModel
{
    public string FirstName { get; set; } = "";
    public string LastName { get; set; } = "";
    public int Age { get; set; }
}

A source generator inspects MyModel at compile time and emits:

// Generated by the source generator -- not written by hand
public partial class MyModel
{
    public static IReadOnlyList<string> GetPropertyNames() =>
        ["FirstName", "LastName", "Age"];
}

The generated method returns a pre-computed array literal. No reflection. No allocations. Fully AOT-safe. The values are baked in at compile time, so if MyModel changes, the next build regenerates the output automatically.

The core difference: reflection asks questions about types at runtime, source generators ask the same questions at compile time and bake the answers into your binary.


How Source Generators Differ from T4 Templates

T4 templates and source generators both produce C# code. The similarities end there.

T4 templates are disconnected from the compiler. They run in the IDE (or via a separate tool in CI) and produce .cs files that you typically commit to source control. Updating a T4 template requires running the transform manually, or configuring your build to do it. If the template output gets out of sync with your source, you get subtle bugs. T4 templates can read files or databases, but they have no semantic model -- they do string matching, not type-system-aware analysis.

Source generators, by contrast, are first-class participants in the compilation. They always run. Their output is always fresh. They have full access to the Roslyn semantic model, meaning they can resolve type names, check interface implementations, read XML documentation comments, and understand nullability annotations. In a normal build, you cannot accidentally skip them -- the compiler always invokes registered generators. The only exception is a generator that fails silently, which is why testing your generators matters.

The practical implication: T4 templates are a manual process you can skip. Source generators are automatic. That reliability is a significant engineering advantage.


Real Examples in the Wild

The best way to understand source generators in C# is to look at examples already shipping in the .NET runtime itself.

System.Text.Json source generation. The [JsonSerializable] attribute triggers a source generator that produces type-specific serialization logic. Instead of reflecting over your types at runtime to emit JSON, the serializer uses pre-generated code. The result is faster startup, lower memory usage, and full AOT compatibility.

// .NET 6+
[JsonSerializable(typeof(WeatherForecast))]
internal partial class AppJsonContext : JsonSerializerContext { }

That single attribute causes a generator to emit an entire partial class containing optimized serialization logic for WeatherForecast. You never write it. You never maintain it.

[GeneratedRegex] -- the compiled regex generator. Regular expressions have always been compilable at runtime via RegexOptions.Compiled, but that compilation happens on first use and the result is not AOT-safe. The [GeneratedRegex] attribute generates a regex implementation entirely at compile time.

// C# 11 / .NET 7+
public partial class EmailValidator
{
    [GeneratedRegex(@"^[^@s]+@[^@s]+.[^@s]+$", RegexOptions.IgnoreCase)]
    private static partial Regex EmailPattern();
}

// Usage -- zero runtime compilation
bool isValid = EmailValidator.EmailPattern().IsMatch(userInput);

This is a great example of the partial method pattern. You declare the method signature, the generator fills in the body.

[LoggerMessage] -- high-performance logging. The LoggerMessage source generator eliminates the overhead of string interpolation in logging hot paths. You define a logging method with a signature and an attribute -- the generator produces an implementation that avoids boxing and allocation.

// .NET 6+
public partial class OrderProcessor
{
    private readonly ILogger<OrderProcessor> _logger;

    public OrderProcessor(ILogger<OrderProcessor> logger)
    {
        _logger = logger;
    }

    [LoggerMessage(Level = LogLevel.Information, Message = "Processing order {OrderId} for customer {CustomerId}")]
    private partial void LogOrderProcessing(string orderId, string customerId);
}

The generator produces a LogOrderProcessing method body that caches the LogDefineOptions and avoids any allocation on the hot path. The logger only does string formatting if the log level is actually enabled.


Why .NET 10 Makes Source Generators Even More Relevant

Source generators were already useful in .NET 5. They are practically essential in .NET 10.

The driving forces are Native AOT and aggressive trimming. .NET 8 made Native AOT a supported deployment target for ASP.NET Core applications. .NET 9 improved it further. .NET 10 continues that trajectory with broader AOT support across more workloads and framework features.

The fundamental challenge for AOT is that it must analyze your entire application statically -- before execution -- and decide what code to keep and what to eliminate. Reflection breaks that analysis. When you call Type.GetType("SomeNamespace.SomeClass") with a string computed at runtime, the AOT compiler cannot know which type you mean. It has to make conservative assumptions, or require you to add [DynamicallyAccessedMembers] annotations everywhere.

Source generators eliminate the problem at the source. Instead of discovering types at runtime via reflection, you discover them at compile time and generate concrete code. The AOT compiler sees plain method calls and array literals -- things it can analyze perfectly.

This also connects directly to plugin architectures and DI-heavy systems. Frameworks like Needlr leverage source generators for plugin discovery in modular .NET applications -- replacing runtime assembly scanning with compile-time registration. That approach is both faster and AOT-safe.

Design patterns also benefit. Source generators can auto-generate the boilerplate for factory method implementations and eliminate the tedious hand-maintenance of factory switch statements. They can generate builder class implementations from annotated model classes, removing the chore of keeping builders in sync with their targets. They can even enforce and generate singleton patterns consistently across a codebase, preventing the accidental violation of singleton semantics that hand-rolled implementations invite.

In more complex scenarios, source generators can wire up strategy pattern registration -- scanning for all types implementing a strategy interface and generating a dispatch table or DI registration block automatically. They can generate decorator wrappers for cross-cutting concerns, removing the mechanical work of forwarding every method call through a proxy.

The pattern is consistent: wherever you have repetitive structural code derived from your type definitions, a source generator can replace the maintenance burden with a compile-time automation.


FAQ

What is the difference between ISourceGenerator and IIncrementalGenerator?

ISourceGenerator is the original API introduced in .NET 5. It re-runs on every compilation, which causes IDE lag with complex generators. IIncrementalGenerator was introduced in .NET 6 and defines a pipeline that only re-executes the stages affected by a source change. For any new generator today, you should use IIncrementalGenerator. Microsoft strongly recommends IIncrementalGenerator for all new generators, and ISourceGenerator is effectively legacy -- but it has not been formally marked [Obsolete] as of .NET 10. Expect it to remain available for backward compatibility.

Do source generators slow down my build?

Well-written incremental generators have minimal impact on build time. They run in parallel with normal compilation and only re-process the parts of the syntax tree that changed. Poorly written generators that perform expensive work without proper caching can cause slowdowns -- but that is an implementation quality issue, not an inherent limitation of the feature.

Can I debug the code that source generators produce?

Yes. The generated files are added to your compilation and the debugger treats them like any other source. In Visual Studio, generated files appear under Dependencies > Analyzers > [YourGeneratorName]. You can set breakpoints in generated code and step through it normally.

Are source generators supported in .NET Framework projects?

No. Source generators require the Roslyn compiler pipeline and are a .NET (Core/5+) feature. Projects targeting .NET Framework that use the old-style .csproj format (non-SDK-style) cannot use source generators. If your .NET Framework project uses the modern SDK-style format (<Project Sdk="Microsoft.NET.Sdk">), source generators work fine -- the generator runs in the build toolchain, not in the target runtime. If you are on legacy project files, migrate to SDK-style first, or fall back to T4 templates or hand-written code.

Can source generators read files outside the project (like JSON or XML config)?

Yes -- via additional files. The Roslyn API exposes AdditionalFiles as part of the generator context. You can include arbitrary files in your project with <AdditionalFiles> MSBuild items and read their content from within the generator. This is how tools like StronglyTypedId and some localization generators work.

What happens if a source generator produces code with a bug?

The generated code is compiled like any other C# code. If it contains a type error, the build will fail with a compiler error pointing to the generated file. The generated files are visible in the IDE, so you can inspect them. If the generator itself has a bug (throws an exception), the generator will fail silently in some configurations or produce a diagnostic warning -- another reason to write tests for your generators.

Do I need to understand source generators to use them?

No. Most developers interact with source generators purely as consumers -- you add a NuGet package, put an attribute on a class, and the magic happens. You only need to understand how to write generators if you are building one yourself. The concepts in this article are primarily useful for understanding what is happening under the hood and for evaluating whether a generator-based library is right for your scenario.


Conclusion

Understanding what source generators in C# are -- and what they are not -- is increasingly important for .NET developers in 2026. They are not a niche performance trick. They are the mechanism behind some of the most significant improvements in the .NET runtime itself: faster JSON serialization, AOT-safe regex, zero-allocation logging, and more.

The core idea is simple: move decisions from runtime to compile time. Fewer runtime surprises. Better performance. AOT compatibility. Automatic consistency between your type definitions and the derived code that depends on them.

If you are just discovering source generators, start by noticing the ones you are already using -- [JsonSerializable], [GeneratedRegex], [LoggerMessage]. They are the clearest demonstration that compile-time code generation is not an exotic concept. It is already woven into the fabric of modern .NET.

When you are ready to go deeper, the natural next step is writing your own IIncrementalGenerator. But that is a topic for another article.

Iterators - An Elementary Perspective on How They Function

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.

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

Master C# source generators with this complete guide. Learn how compile-time code generation works in .NET 10, why it beats reflection, and how to get started.

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