Source generators at a glance
Compile-time source, analyzer references, incremental caching, and what a driver test must assert.
Quick reference
A source generator adds C# files during compilation. Those files are compiled with the project. The generator does not edit the files you typed, and it does not run when the program starts.
For a sibling generator project, use OutputItemType="Analyzer" and ReferenceOutputAssembly="false". This supplies an analyzer input without adding an ordinary compiler assembly reference. A normal project reference does not register the generator.
Use IIncrementalGenerator. Register a pipeline in Initialize. Each step must return the same value for the same input. Clock reads and randomness are undeclared inputs: cached work can retain an old value, while rerunning can produce a different one. Sort otherwise unordered output by a stable key, and define value equality for each pipeline model. A record containing array or list fields does not automatically compare their elements. Separate collections with identical contents can make the models compare unequal unless you define suitable equality.
Existing types extended through another partial declaration must be partial. If they are not, report a diagnostic instead of emitting a second definition of the class. A generator that emits an independent helper type does not require the user’s class to be partial.
Replacing a particular reflection path with a generated direct call can help, but it does not establish Native AOT compatibility for the whole application. Check the emitted code, its dependencies, and AOT/trimming warnings.
Test with CSharpGeneratorDriver. Keep the returned driver; assert the emitted text and promised diagnostic ID, severity and location. Use RunGeneratorsAndUpdateCompilation and check the updated compilation for errors. A no-throw run or a file under obj is not enough.
Picture it: a print shop with reusable job tickets
Your handwritten pages are the application's source. Before binding the book, a clerk reads the order and adds a typeset page. The clerk is the generator; the new page is generated C#; checking and binding all pages is compilation. Reading the finished book is running the application.
For incremental work, the clerk keeps a small job ticket containing only the details needed for that page. The ticket is the equatable output model. The clerk may reread an order, find an equal ticket, and reuse the typeset page. Equality explains reuse; it does not promise that discovery never runs again.
API cheat sheet
- Find marked targets:
ForAttributeWithMetadataName, using the full attribute metadata name, includingAttribute. - Fixed marker declaration:
RegisterPostInitializationOutput. - Emit a model:
RegisterSourceOutputfollowed byAddSource, with a unique hint name. - Declare external inputs:
AdditionalTextsProviderfor files;AnalyzerConfigOptionsProviderfor exposed configuration. - Read reuse correctly:
Cachedreuses a step's output;Unchangedmeans the step ran and produced an equal value. - Test three contracts: generated source, generator diagnostics, and compilation of user plus generated source.
Three changes, worked through
1. Add an unrelated class
The course example extracts Demo and Parcel to decide its generated method. Adding Unrelated does not change those output values. Discovery may rerun and allocate a fresh Candidate; projecting it to an equal namespace/name tuple can stop the change from propagating downstream. Expect identical generated source. Use tracked steps to investigate reuse rather than guessing from the edit alone.
2. Rename or remove the target
Rename the marked Parcel and its caller to Shipment. The model changes, so both the returned name and the hint become Demo.Shipment-based. The old Parcel output must disappear. If you instead remove the marker while leaving a call to GeneratedName, the method should disappear and that caller should fail compilation. Keeping stale output would hide the problem.
3. Remove partial
The course generator promises to add a member to the existing class. Without partial, it should report TINSG001 at the target's name and emit no class extension; its marker attribute may still be generated. Test the diagnostic ID, severity and location separately from emitted-file counts. A successful text comparison alone cannot prove that the combined program compiles.
These are reasoning examples for the existing course implementation. The complete generator and test harness remain in the lessons below.
Video companions
C# Source Generators — Kathleen Dollard and Jared Parsons, On .NET: start at 01:15 for the concept and 05:24 for generated classes. Microsoft's episode page dates this to the .NET 5 era. Use it for the mental model; follow this course's incremental API examples for implementation.
C# Source Generators – Why and How — Jim Wooley, Microsoft Visual Studio: 43:10 incremental generators, 45:30 caching, 47:25 value semantics, and 54:00 xUnit/Verify snapshot testing. Use that segment for snapshot-testing ideas; lesson 3 separately explains direct-driver tests and assertions about the resulting compilation.
Continue in the course
- What a source generator actually emits: trace the input class, generated member and application call.
- Why an incremental generator must be deterministic: build the generator and examine its equality boundary.
- Testing the source a generator emits: check source, diagnostics, compilation and change propagation.
Then try the source-generators quick-reference practice.