Source Generator Internals
Refitter.SourceGenerator runs the Refitter code generator inside the C# compiler. Usage is described in Source Generator. This page explains how it is built, how the incremental pipeline works, and how it cooperates with Refit's own source generator.
Package and assembly layout
Roslyn loads analyzers and source generators into the compiler process (and the IDE) with limited support for dependencies. The generator therefore ships as a self-contained analyzer:
flowchart LR
subgraph Source["Refitter.SourceGenerator project"]
GSRC["RefitterSourceGenerator.cs<br/>HintNames · LoadedGeneratorLocator<br/>RefitGeneratorOptionsProvider · Telemetry"]
CSRC["Refitter.Core source files<br/>(compiled in as source)"]
end
Source --> DLL["Refitter.SourceGenerator.dll<br/>netstandard2.0"]
subgraph PKG["Refitter.SourceGenerator.nupkg"]
AN["analyzers/dotnet/cs/<br/>Refitter.SourceGenerator.dll<br/>+ NSwag, NJsonSchema, OasReader,<br/>Microsoft.OpenApi, YamlDotNet, ..."]
BLD["build/<br/>Refitter.SourceGenerator.props<br/>Refitter.SourceGenerator.targets"]
end
DLL --> AN
Refitter.Coreis not referenced as a package. Its source files are included folder by folder (<Compile Include="../Refitter.Core/Pipeline/*.cs" />and so on), so the generator contains its own copy of the code generator.- Third-party dependencies are referenced with
PrivateAssets="all"and packed next to the generator inanalyzers/dotnet/cs(using H.Generators.Extensions), where the compiler can load them. Refitis referenced privately. At build time the generator does not use its own copy of Refit's generator: it uses the one the consuming project'sRefitpackage already loaded into the compiler. This is why consuming projects must referenceRefitdirectly.- The package is a development dependency, so it doesn't flow to consumers of your library.
MSBuild integration
The package's build files do two small things before the compiler runs:
| File | What it does |
|---|---|
Refitter.SourceGenerator.props |
<AdditionalFiles Include="**/*.refitter" />, so every .refitter file in the project reaches the generator |
Refitter.SourceGenerator.targets |
Unless the project already disabled it, sets DisableRefitSourceGenerator=true and RefitterRunsRefitGenerator=true (exposed to the generator as a CompilerVisibleProperty). Also registers the post-build telemetry task |
The DisableRefitSourceGenerator logic lives in a .targets file on purpose: .targets files are imported after the project body, so a project that sets DisableRefitSourceGenerator to true itself (for example when using Refit.Reflection) is detected and left alone.
The incremental pipeline
RefitterSourceGenerator is an IIncrementalGenerator. Roslyn caches every stage, so when an unrelated .cs file changes, the .refitter files are not reprocessed.
flowchart TD
AT["AdditionalTextsProvider"] --> W["Where: path ends with .refitter"]
W --> COL1["Collect paths"]
COL1 --> EMPTY{"any?"}
EMPTY -- no --> D3["REFITTER003 warning:<br/>no .refitter files found"]
W --> GC["Select: GenerateCode(file)<br/>one per .refitter file"]
subgraph GCD["GenerateCode"]
direction TB
R1["read text"] --> R2["RefitterSettingsLoader.LoadSettingsFile(json, path)"]
R2 --> R3["RefitterRunner.RunAsync(settings,<br/>writer: null, validator: null)"]
R3 --> R4["GeneratedFiles → GeneratedSource(planned hint, content)<br/>exceptions → diagnostics"]
end
GC --> GCD
GCD --> COL2["Collect all results"]
COL2 --> UH["AssignUniqueHintNames<br/>(sorted by settings path)"]
UH --> OUT["RegisterSourceOutput:<br/>AddSource(hint, code) + REFITTER001"]
UH --> NEST["Combine with CompilationProvider,<br/>ParseOptions, AnalyzerConfigOptions"]
NEST --> IMPL["RegisterImplementationSourceOutput:<br/>RunNestedGenerators"]
RefitterRunner is called without an IFileWriter, so nothing is written to disk from inside the compiler, and without an IValidator, so the specification is not validated with Microsoft.OpenApi. The outputFolder / outputFilename settings are only used to compute hint names. Use <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> to see the output on disk.
Hint names
Roslyn requires every AddSource hint name in a compilation to be unique. HintNames derives them from the .refitter file name and the planned output file name:
flowchart LR
A["settings: Petstore.refitter<br/>planned: Contracts.cs"] --> B{"planned name ==<br/>settings name?"}
B -- yes --> C["Petstore"]
B -- no --> D["Petstore.Contracts"]
C --> E["replace characters other than<br/>letters, digits, . _ - with _"]
D --> E
E --> F["+ .g.cs"]
F --> G{"already used?<br/>(case-insensitive)"}
G -- yes --> H["Petstore.Contracts2.g.cs, 3, ..."]
G -- no --> I["Petstore.Contracts.g.cs"]
Results are sorted by settings file path (ordinal) before names are assigned, so hint names are stable between builds even when two .refitter files in different folders share a name.
Running Refit and System.Text.Json generators
Roslyn runs all source generators against the same input compilation. Generators don't see each other's output. Without extra work, Refit's source generator would never see the interfaces Refitter adds, so they would get no generated Refit implementations. Likewise the System.Text.Json source generator would not see a generated JsonSerializerContext.
Refitter solves this by running those generators itself, over an augmented compilation:
sequenceDiagram
autonumber
participant Roslyn
participant RSG as RefitterSourceGenerator
participant Loc as LoadedGeneratorLocator
participant STJ as System.Text.Json generator
participant RG as Refit generator (InterfaceStubGeneratorV2)
participant RA as Refit analyzer
Roslyn->>RSG: compilation + Refitter sources + options
RSG->>RSG: parse Refitter sources into syntax trees
RSG->>RSG: augmented = compilation.AddSyntaxTrees(...)
opt a generated class has a JsonSerializable attribute
RSG->>Loc: find loaded System.Text.Json generator
Loc-->>RSG: JsonSourceGenerator instance
RSG->>STJ: run on augmented compilation
STJ-->>RSG: sources for the generated context only
end
opt build_property.RefitterRunsRefitGenerator == true
RSG->>Loc: find loaded Refit generator
Loc-->>RSG: InterfaceStubGeneratorV2 instance
RSG->>RG: CSharpGeneratorDriver.RunGenerators(augmented)<br/>with RefitGeneratorOptionsProvider
RG-->>RSG: Refit implementations (hand-written + generated interfaces)
RSG->>RA: run analyzer with real options, then with generator re-enabled
RA-->>RSG: report only diagnostics new in the second run (e.g. RF006)
end
RSG->>Roslyn: AddSource("Refit." + hint / "System.Text.Json." + hint)
The details that make this work:
- The consumer's Refit generator is disabled by the package's
.targets(DisableRefitSourceGenerator=true), so Refit implementations are not generated twice. Because Refitter's nested run covers the whole augmented compilation, hand-written Refit interfaces in the same project still get implementations. RefitGeneratorOptionsProviderwraps the project's analyzer config options and answers"false"forbuild_property.DisableRefitSourceGenerator, re-enabling Refit's generator only inside the nested run.LoadedGeneratorLocatorfinds the generator types in assemblies already loaded in the compiler process by the consumer's packages, so the Refit and System.Text.Json versions always match the ones the project references. A shared compiler server can have several versions loaded at once, so it picks the newest loaded version with the same major version as the compilation's reference. If the generator can't be found,REFITTER006/REFITTER008is reported.- Refit analyzer diagnostics such as
RF006are normally only reported while Refit's generator is enabled. Refitter runs the Refit analyzer twice, with the real options and with the generator re-enabled, and reports only the extra diagnostics from the second run, so nothing is reported twice. - Diagnostic locations: diagnostics from nested generators may point into syntax trees that only exist inside the nested run. Those are moved to
Location.None, with the originalpath:lineappended to the message.
Diagnostics
All diagnostics use the category Refitter.
| ID | Severity | Meaning |
|---|---|---|
REFITTER000 |
Error | A .refitter file couldn't be read or deserialized, or generation failed |
REFITTER001 |
Info | A source file was generated |
REFITTER002 |
Warning | dateFormat is ignored because useIsoDateFormat is enabled |
REFITTER003 |
Warning | No .refitter files were found |
REFITTER004 |
Info | A .refitter file was found |
REFITTER005 |
Info | Contents of a .refitter file |
REFITTER006 |
Warning | Refit's source generator couldn't be found; add a direct Refit package reference |
REFITTER007 |
Error | A nested generator or the Refit analyzer threw an exception |
REFITTER008 |
Warning | The System.Text.Json source generator couldn't be found |
Telemetry
The source generator never sends telemetry itself. (It can still make network calls: when openApiPath is an http(s) URL, the specification is downloaded during compilation, whether telemetry is enabled or not.) Telemetry is sent by RefitterTelemetryTask, an MSBuild task in the same assembly that runs after Build:
flowchart TD
B["Build finished"] --> C{"DesignTimeBuild, RefitterNoLogging<br/>or DOTNET_CLI_TELEMETRY_OPTOUT?"}
C -- yes --> X["skip"]
C -- no --> FP["fingerprint = SHA-256 of<br/>package version + .refitter paths and contents"]
FP --> ST{"same as stamp file in obj?"}
ST -- yes --> X
ST -- no --> WS["write stamp file first"]
WS --> SEND["send sanitized settings events<br/>3 second timeout"]
SEND --> DONE["always succeeds<br/>(ContinueOnError, errors logged at low importance)"]
The stamp is written before sending so machines without network access don't retry on every build.
How it is tested
The source generator has its own test project that references the generator as an analyzer and imports its .targets, so the tests exercise the real package behaviour, including the nested Refit generator. See Source generator tests.