← Open in the full interactive course (progress tracking, search & more)

Reflection asks "what does this type look like?" at runtime, every time, at a real cost. A source generator asks the same question once — at compile time — and writes the answer down as ordinary code.

The previous two lessons built up a real, working pattern: attributes as declarative metadata, reflection as the mechanism that reads them back and acts on them at runtime. That pattern is genuinely useful — and it has a genuine cost, paid on every single call, that matters once you're in a hot path or a startup-time-sensitive, AOT-compiled application.

What if the same discovery — "which properties does this type have, and what do their names/attributes say" — could happen exactly once, at compile time, instead of over and over at runtime? That's precisely what a source generator does. You've almost certainly used one already without realizing it: System.Text.Json's source-generated serialization mode, and the [LoggerMessage]-based logging pattern in modern ASP.NET Core code, are both source generators quietly doing this trick.

In this lesson, at an introductory level: what a source generator actually is, why it exists (directly motivated by reflection's runtime cost and AOT-compatibility limits), a real example you've likely already used, and the basic mental model of "runs at compile time, adds files to your compilation" — without a deep API walkthrough of writing one yourself, which belongs to a dedicated, more advanced lesson.

What Is It?

The Simple Explanation

A source generator is a piece of code that plugs into the C# compiler itself. While your project is being compiled, the generator gets to look at your actual source code — your classes, your attributes, your method signatures — and, based on what it sees, write brand-new C# source files that get compiled right alongside the code you wrote by hand. You never see these generated files unless you go looking for them, but they're real, ordinary C# — compiled, type-checked, and just as fast as anything you typed yourself.

The Technical Definition

A source generator is a component (most commonly implemented as an IIncrementalGenerator) that hooks into the Roslyn (C# compiler) compilation pipeline. During compilation, it receives access to the syntax and semantic model of your source code, can inspect it (typically looking for specific attributes, type shapes, or patterns), and produces additional C# source text that the compiler adds to the same compilation and compiles normally — before the final assembly is produced. The generated code becomes part of your program exactly as if you'd written it by hand, but it was written for you, automatically, based on what your actual code looks like.

Reflection (runtime)

Source generator (compile time)

Why Does It Exist?

The Problem

Reflection gives you enormous flexibility — as the previous two lessons showed, it's what makes serializers, DI containers, and validators work generically. But that flexibility isn't free, in two concrete ways:

The Need

Developers needed a way to get the same generic, "works automatically based on what my type looks like" convenience that reflection provides — without paying reflection's runtime cost on every call, and without the AOT-compatibility problems that come from needing to discover and invoke code dynamically at runtime.

The Solution

Move the discovery work from runtime to compile time. A source generator inspects your types once, while the project is being built — the exact same kind of question reflection would otherwise ask at runtime ("what properties does this type have?") — and, instead of answering that question dynamically on every call, it writes the answer down as ordinary, hand-looking, directly-callable C# code. That generated code has zero reflection overhead and is fully visible to the AOT compiler, because it's just... normal code, sitting in your compilation like anything else.

Big Picture

SAME GOAL, TWO VERY DIFFERENT TIMES TO PAY THE COST

Reflection-based approach

Your code compiles

Program runs

Discovery happens HERE, every call

Result produced

Source-generator approach

Discovery happens HERE, once, at compile time

Generated code compiles alongside yours

Program runs

Result produced — no discovery left to do

How It Works

THE BASIC MENTAL MODEL — STEP BY STEP
1. YOU WRITE ORDINARY C#, OFTEN WITH A MARKER (LIKE AN ATTRIBUTE, OR A partial CLASS)
[JsonSerializable(typeof(Customer))]
public partial class AppJsonContext : JsonSerializerContext { }
2. THE GENERATOR INSPECTS YOUR CODE DURING COMPILATION
3. THE GENERATOR EMITS NEW C# SOURCE FILES
4. THE COMPILER COMPILES THE GENERATED CODE TOGETHER WITH YOUR OWN

Simple Example

System.Text.Json's source-generated serialization mode, the most common source generator most .NET developers encounter directly:

public class Customer
{
    public string Name { get; set; } = "";
    public decimal Balance { get; set; }
}

// The marker: a partial class, attributed with the types to generate serialization code for
[JsonSerializable(typeof(Customer))]
public partial class AppJsonContext : JsonSerializerContext { }

// Usage — looks almost identical to the plain reflection-based API...
var customer = new Customer { Name = "Alice", Balance = 100m };
string json = JsonSerializer.Serialize(customer, AppJsonContext.Default.Customer);
// ...but under the hood, AppJsonContext.Default.Customer resolves to GENERATED, non-reflective
// serialization code that the source generator wrote for Customer specifically, at compile time.

Meaning: Compare this to plain JsonSerializer.Serialize(customer) with no context — that call uses reflection at runtime to discover Customer's properties, exactly like the reflection lesson described. The source-generated version does that same discovery once, at compile time, and AppJsonContext ends up containing real, generated, directly-callable serialization code for Customer — no runtime property discovery left to do at all.

Real-World Example

A second real, widely-used source generator: [LoggerMessage]-based logging, common in modern ASP.NET Core code, which replaces a reflection- and boxing-heavy logging call pattern with generated, allocation-minimal code.

public partial class OrderService
{
    private readonly ILogger<OrderService> _logger;

    [LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} shipped to {Destination}")]
    partial void LogOrderShipped(int orderId, string destination);

    public void ShipOrder(int orderId, string destination)
    {
        // ...shipping logic...
        LogOrderShipped(orderId, destination); // looks like an ordinary method call — because it now IS one
    }
}

You write only the partial void LogOrderShipped(...) declaration and the [LoggerMessage] attribute describing the log message template — you never write the method body. The source generator sees the attribute, inspects the method's declared parameters (orderId, destination), and generates the actual implementation: efficient, allocation-minimal logging code matching the message template to the parameters, with no runtime string-formatting or reflection-based argument boxing involved. Calling LogOrderShipped(orderId, destination) is, after compilation, exactly as fast as calling any ordinary method you wrote entirely by hand — because, after the generator runs, that's precisely what it is.

Analogy

A translator who works ahead of time vs. one who works live

Reflection is like a live interpreter standing beside a speaker, translating every single sentence in real time, for every single listener, every single time the speech is given. It's flexible — it works for a speech nobody's heard before — but it's genuinely slower than reading, and it has to be redone from scratch at every performance.

A source generator is like translating the entire speech into a printed script once, ahead of time, before the event even starts. Every audience member afterward just reads the printed script directly — no live interpretation happening at all during the actual event. The upfront translation work (the generator running at compile time) is exactly the same kind of work the live interpreter does — reading the source and producing a translation — it's just done once, in advance, instead of repeatedly, live, under time pressure.

Under the Hood

GENERATOR RUNS AT COMPILE TIME, ADDS FILES TO YOUR COMPILATION — THE CORE MECHANIC
WHERE A SOURCE GENERATOR SITS IN THE BUILD

Common Confusion

1. "Source generators generate code while my program runs" — no, strictly at compile time

This is worth stating plainly because the name invites the mistake: a source generator never runs as part of your deployed application. All of its work happens on the machine (or build server) compiling your project. By the time you have a finished executable or published application, the generator has already done everything it's going to do — what ships is ordinary, already-generated C#, compiled to ordinary IL, with the generator itself nowhere in the runtime picture at all.

2. Source generators don't replace reflection everywhere — they replace it where the types are known at compile time

A tool that must genuinely discover and act on types it has never seen before, chosen dynamically at runtime (a plugin system loading arbitrary assemblies at startup, for instance) still needs reflection — a source generator can't generate code for a type it doesn't know exists yet at compile time. Source generators shine specifically when the relevant types are known up front, in your source code, which covers a large share — though not all — of what reflection is used for today.

3. Generated code is not "hidden magic" — it's real, inspectable C#

Because the generated files genuinely exist and go through genuine compilation, most tooling lets you view them directly (for example, IDE features to inspect generated files, or MSBuild settings to emit them to disk). This is quite different from reflection's runtime behavior, which has no equivalent "look at the generated code" step — there's simply no generated code to look at, because reflection discovers everything dynamically, on the fly.

Common Mistakes

Mistake 1 — Assuming a source generator can handle types only known at runtime

Expecting [JsonSerializable(typeof(...))]-style generation to somehow work for a type loaded dynamically from a plugin assembly the generator never saw at compile time. Source generators require compile-time knowledge of the target types — for genuinely dynamic scenarios, reflection (or a hybrid approach) is still the right tool.

Mistake 2 — Forgetting the target member/class needs to be partial

Writing [LoggerMessage(...)] on a method inside a non-partial class and being confused why the generator's contribution doesn't compile alongside it. Many source generators (including [LoggerMessage]) rely on partial classes/methods specifically so the hand-written declaration and the generator's contribution can be split across two files that the compiler merges — this is a real, common setup requirement worth double-checking.

Mistake 3 — Treating "uses a source generator" and "uses reflection" as mutually exclusive facts about a whole library

Assuming that because a library offers a source-generated mode, reflection is entirely gone from it everywhere. Many libraries (like System.Text.Json) offer both a reflection-based mode (maximally flexible, works with types not registered ahead of time) and a source-generated mode (faster, AOT-friendly, requires upfront registration) — you often choose which one fits your scenario, rather than one having fully replaced the other across the board.

When Should I Use It?

Mental Model

Reflection = discover the type's shape while the program runs, every time, at a real per-call cost.
Source generator = discover the type's shape while the project compiles, once, and write ordinary code for the answer.
The generated code = indistinguishable, at the IL level, from anything you'd have typed by hand.

Remember: a source generator never runs inside your deployed application — by the time your program is running, all the "figuring out what the type looks like" work is already finished and baked into ordinary, already-compiled code.

Key Takeaway


Check Your Understanding

You've seen what a source generator is, why it exists, and two real examples you may have already used. Let's check your understanding.

1. When does a source generator's code actually run?

Show answer

Correct: B

Why B is correct: This is the central distinction from reflection — a source generator does all of its work during compilation. By the time your application is running, the generator has already finished; what's running is ordinary, already-generated code.

Why A is incorrect: This describes how reflection-based discovery works (lazily, at runtime) — the exact behavior a source generator is designed to avoid, as covered in "Common Confusion."

Why C is incorrect: Source generators have nothing to do with exception handling — they run unconditionally as a normal part of every build.

Why D is incorrect: There's no generator activity happening "in the background" once the application is running — the generator's job is finished the moment the build completes.

Reinforcement: "Compile time, not runtime" is the single most important fact about source generators — it's the direct source of both their performance benefit and their AOT-compatibility.

2. Why does source-generated JSON serialization avoid the runtime performance cost that plain reflection-based JsonSerializer.Serialize(obj) pays?

Show answer

Correct: B

Why B is correct: The generator moves exactly the work described in the reflection lesson — discovering a type's properties — from runtime (paid every call) to compile time (paid once). The generated code that results is ordinary, direct property access, with none of reflection's per-call overhead.

Why A is incorrect: There's nothing about threading involved here — the performance win is about eliminating reflective lookup and boxing, not about parallelism.

Why C is incorrect: The generator doesn't change the JSON output format or compression — it changes how the serialization code itself is produced and executed.

Why D is incorrect: JsonSerializerContext holds generated, in-memory serialization code — there's no database involved anywhere in this mechanism.

Reinforcement: The performance win is entirely about when the "what does this type look like" question gets answered — once at compile time, versus every time at runtime.

3. A plugin system needs to load assemblies chosen by the user at runtime and discover types inside them that were completely unknown when the host application was compiled. Is a source generator a good fit for this specific requirement?

Show answer

Correct: B

Why B is correct: As covered in "Common Confusion" and "Common Mistakes," source generators require the relevant types to be known at compile time. A plugin loaded dynamically at runtime, with types the host never saw while compiling, is exactly the scenario reflection remains the right tool for.

Why A is incorrect: A source generator's inspection window is strictly the compile-time compilation — it has no visibility into assemblies loaded later, at runtime, that didn't exist as part of that compilation.

Why C is incorrect: The source language of the plugin assembly is irrelevant — the fundamental limitation is about compile-time versus runtime knowledge, not language.

Why D is incorrect: This reverses the relationship entirely — source generators are a newer complement to reflection for compile-time-known scenarios, not a wholesale replacement, and reflection remains fully relevant for genuinely dynamic cases like this one.

Reinforcement: Source generators and reflection solve overlapping but distinct problems — compile-time-known types are a great fit for generators; genuinely dynamic, runtime-discovered types still need reflection.

You now have the mental model for source generators, and you can see exactly how they connect back to reflection's real runtime cost and attributes' role as declarative metadata — the throughline for this entire module.


dotnetmadeeasy.com — Learn C# and .NET, the right way.