Module 1 · Incremental source generators · Lesson 1 of 3
What a source generator actually emits
A source generator is a compiler extension. During compilation it receives the user's syntax and the compilation, then it adds new C# source files. It does not rewrite the files the developer typed.
Those added files are compiled with the rest of the project. A generated partial class can add a method that ordinary application code calls directly. This can replace a particular use of runtime reflection. Generated code is still ordinary C#: generating it does not by itself make the application compatible with Native AOT. Check the emitted code, its dependencies, and the AOT/trimming warnings.
The generator lives in a separate project. For a project-to-project reference, use OutputItemType="Analyzer" and ReferenceOutputAssembly="false". The first metadata value supplies an analyzer input; the second keeps that project output out of the consuming compiler's ordinary assembly references. A normal project reference does not tell the compiler to discover a generator. The generator runs in the compiler host, including supported IDE design-time compilations, rather than when the application starts.
ISourceGenerator exposes Execute for generation work. Prefer IIncrementalGenerator for new generators: register a pipeline in Initialize so the host can reuse intermediate results. Equality at each boundary controls that reuse. An upstream step can rerun yet produce an equal value, allowing downstream work to stay cached. This is not a promise that nothing runs after an unrelated edit.
A generator is marked with [Generator] and implements IIncrementalGenerator. ForAttributeWithMetadataName finds attributed syntax by the bound attribute's fully qualified metadata name, including its Attribute suffix. RegisterSourceOutput can then call AddSource with a unique hint name and the new source text. To add members to an existing class through another declaration of that class, the declarations must be partial. A generator that creates a separate helper type does not require the user's type to be partial.
If a generator promises to extend a class and its declarations are not partial, report a diagnostic at the target type's declaration instead of emitting a conflicting class. Check the declaration syntax through the symbol's DeclaringSyntaxReferences; there is no general INamedTypeSymbol.IsPartial property. Reject other unsupported shapes explicitly too.
Treat generated source as output, not the place to make a fix. IDEs can expose it under their analyzer/generated-source views. Disk output is optional: set EmitCompilerGeneratedFiles to true in the consuming project to inspect files under its intermediate output directory. Leaving the default location under obj avoids accidentally compiling a second copy from the project source tree.
Watch
Follow one value from input to generated code
The next lesson supplies the complete Tin.Generator project. This consumer is its small, deliberate contract: a marked class gets a static GeneratedName method. The marker attribute is also emitted by the generator. These input and output listings explain the contract; do not manually copy the generated output into the application.
using System;
Console.WriteLine(Demo.Parcel.GeneratedName());
namespace Demo
{
[Tin.GenerateName]
public partial class Parcel { }
}Expected generated class, identified by the hint name Demo.Parcel.GeneratedName.g.cs:
namespace @Demo
{
partial class @Parcel
{
public static string GeneratedName() => "Demo.Parcel";
}
}The @ prefixes escape identifiers; @Demo and Demo name the same namespace here. The original Parcel declaration and generated declaration become one type. The call binds to a real static method. Its expected console output after a successful build is:
Demo.Parcel
Changing the input class name to Shipment must change both the hint name and returned string; the previous Parcel output must disappear. Deleting the attribute must remove the generated member. If the application still calls that member, a compiler error is the correct result. A generator cannot quietly preserve a stale member to make a broken caller compile.
Check your understanding
- Does removing partial always prevent source generation? No. It prevents this generator from extending that existing class. Generators can instead produce independent helper types. Our example reports TINSG001 and emits no additional class for an unsupported target.
- Why not edit the .g.cs file to change the returned name? It is derived output. Fix the input or generator, rebuild, and inspect the replacement output.
- Why can a generator compile while the application fails? The generator program and the text it emits are different programs. Both need checking. The third lesson compiles the user source together with the generated source.
The attached practice reinforces this lesson and retains two bridge questions about determinism and testing. Continue with the next two lessons for their focused practice.
Video companion
Watch C# Source Generators, presented by Kathleen Dollard and Jared Parsons on Microsoft's dotnet channel. Start at 01:15 for the compiler-extension model and 05:24 for the generated-class demo. The video uses the older .NET 5-era API; use this course's IIncrementalGenerator example for current implementation guidance.
Analogy
A source generator is a clerk inside the print shop. You hand in a form. Before the book is bound, the clerk types an extra page and slips it into the stack. Readers never see the clerk. They only see the finished book. The extra page still has to pass the same checks as the original pages; adding it does not let the clerk rewrite the submitted form.
Quick reference
- Generator output is C# source, compiled with the project.
- Reference the generator as an analyzer, not as a runtime assembly.
- Prefer IIncrementalGenerator over ISourceGenerator.
- Existing type declarations extended with generated partial declarations must be partial.
- Report unsupported user input with diagnostics. Honor cancellation; do not hide generator faults.