Code Generation Pipeline
All code generation happens in Refitter.Core. The CLI, the MSBuild task (through the CLI) and the source generator all call the same pipeline. This page follows a RefitGeneratorSettings object from the moment it reaches RefitterRunner until generated C# source comes back.
The big picture
flowchart TD
S["RefitGeneratorSettings"] --> L
subgraph Create["RefitGenerator.CreateAsync"]
L["Load OpenAPI document(s)<br/>OpenApiDocumentFactory"] --> F1["Filter by tags<br/>RefitDocumentFilter.FilterByTags"]
F1 --> F2["Filter by path<br/>RefitDocumentFilter.FilterByPath"]
F2 --> T["Trim unused schemas<br/>SchemaCleaner (optional)"]
end
T --> RP
subgraph Run["RefitGenerator.RunPipeline"]
RP["CSharpClientGeneratorFactory<br/>mutators + NSwag settings"] --> GP["GeneratorPipeline.Run"]
GP --> C["Contracts<br/>NSwag GenerateFile + post-processors"]
GP --> J["JsonSerializerContext<br/>(optional)"]
GP --> I["Refit interfaces<br/>InterfaceGenerator"]
GP --> D["DI / Apizr registration"]
end
C --> FMT
J --> FMT
I --> FMT
D --> FMT
FMT{"GenerateMultipleFiles?"}
FMT -- no --> SF["FormatSingleFile<br/>one string"]
FMT -- yes --> MF["FormatMultipleFiles<br/>one GeneratedCode per type"]
SF --> OP["OutputPlanner.Plan"]
MF --> OP
OP --> PF["PlannedFile[]<br/>path + content"]
RefitterRunner
RefitterRunner.RunAsync is the single entry point used by every host. The order of steps matters: code is generated and the output is planned before the specification is validated, but files are only written after validation succeeds. A spec that fails validation never leaves half-written output behind.
sequenceDiagram
autonumber
participant Host as Host (CLI / Source Generator)
participant Runner as RefitterRunner
participant Gen as RefitGenerator
participant Planner as OutputPlanner
participant Val as IValidator (optional)
participant Writer as IFileWriter (optional)
Host->>Runner: RunAsync(settings, writer, validator, settingsFilePath, outputPath)
Runner->>Gen: CreateAsync(settings)
Gen-->>Runner: generator (loaded, filtered, trimmed document)
alt GenerateMultipleFiles
Runner->>Gen: GenerateMultipleFiles()
else single file
Runner->>Gen: Generate()
Note over Runner: normalize line endings to CRLF
end
Gen-->>Runner: GeneratorOutput
Runner->>Planner: Plan(output, settings, settingsFilePath, outputPath)
Planner-->>Runner: PlannedFile[]
opt validator != null
Runner->>Val: validate every OpenAPI path
Val-->>Runner: errors / warnings → RunnerDiagnostic
Note over Runner: ThrowIfInvalid()
end
opt writer != null
Runner->>Writer: WriteAsync(path, content) per planned file
end
Runner->>Runner: DetectWarnings (date format override, deprecated usePolly)
Runner->>Runner: DetectPathFilteringDiagnostics (all paths filtered)
Runner-->>Host: RunResult(GeneratedFiles, Warnings, Diagnostics, Elapsed, ExitCode)
RunAsync catches every exception. On failure it returns an empty file list, appends the exception message as an error RunnerDiagnostic, and sets ExitCode to the exception's HResult. The host decides how to surface that: the CLI prints it and exits with a non-zero code, and the source generator reports REFITTER000.
Loading the OpenAPI document
Loading lives in the Document folder. The result is always a single NSwag OpenApiDocument, regardless of whether the input was one file, a URL, or several specifications.
flowchart TD
IN["OpenApiPath or OpenApiPaths[]"] --> MULTI{"More than one path?"}
MULTI -- yes --> EACH["Load each document"] --> MERGE["DocumentMerger.Merge"]
MULTI -- no --> KIND
EACH -.-> KIND
KIND{"http(s) URL?"}
KIND -- yes --> HTTP["HttpClient GET<br/>gzip/deflate, 30s timeout"] --> RG1["ReferenceGuard.ValidateAsync"] --> PARSE
KIND -- no --> RG2["ReferenceGuard.ValidateAsync"] --> LOADER
subgraph LOADER["DocumentLoader strategy chain (first non-null wins)"]
direction TB
S1["FileDocumentStrategy"] --> S2["HttpDocumentStrategy"] --> S3["OpenApiReaderDocumentStrategy<br/>only when the spec has external refs"]
end
LOADER --> PARSE
subgraph PARSE["OpenApiDocumentParser"]
direction TB
P1["YAML → JSON (YamlDotNet)"] --> P2["OpenApiJsonNormalizer"] --> P3["NSwag OpenApiDocument.FromJsonAsync<br/>with sanitizing reference resolvers"]
end
PARSE --> DOC["OpenApiDocument"]
MERGE --> DOC
Reference guard
Before a specification is parsed, ReferenceGuard scans its $ref values. It enforces two separate rules:
- Remote
http(s)references are rejected, to avoid server-side request forgery when Refitter runs on a build server. SettingallowRemoteReferencesin the.refitterfile allows them. - Local file references (absolute or
../) that resolve outside the input specification's directory tree are always rejected.allowRemoteReferencesdoes not change this; move the referenced files under the specification's directory instead. The check compares normalized paths and does not resolve symbolic links, so a link inside the directory that points elsewhere is followed. Treat it as a guard against accidental or crafted../references, not as complete filesystem confinement.
Loading strategies
DocumentLoader tries a chain of IDocumentLoadingStrategy implementations:
FileDocumentStrategyparses a local file with NSwag.HttpDocumentStrategydownloads the document with arefitter/<version>user agent.OpenApiReaderDocumentStrategyonly acts when the specification contains external references. It uses Microsoft.OpenApi's multi-file reader (via OasReader) to inline them into one document, fills in a missinginfo.title/info.version, serializes the result back in its original OpenAPI version and re-parses it with NSwag.
Normalizing the JSON
NSwag was written for OpenAPI 2.0 and 3.0. OpenApiJsonNormalizer parses the raw JSON once and rewrites constructs NSwag can't handle before NSwag sees them:
flowchart LR
RAW["raw JSON"] --> N1["NumericBoundsSanitizer<br/>clamp min/max to decimal range"]
N1 --> N2["EnumExtensionsSanitizer<br/>object-valued enum extensions → arrays"]
N2 --> N3["ItemSchemaSanitizer<br/>3.1 itemSchema → array schema"]
N3 --> N4["PathItemReferenceInliner<br/>inline 3.1 components/pathItems refs<br/>(root document only)"]
N4 --> OUT["normalized JSON"]
Externally referenced files go through the same normalization, because the parser installs SanitizingJsonReferenceResolver / SanitizingYamlReferenceResolver as NSwag's reference resolvers.
Merging multiple specifications
When openApiPaths lists several specifications, DocumentMerger clones the first document and merges paths, component schemas, definitions and security definitions from the rest. Tags are de-duplicated by name. A key that appears in two documents is only accepted when DocumentEquivalenceComparer considers both definitions identical; otherwise merging fails fast with an InvalidOperationException instead of silently picking one.
Filtering and trimming
RefitGenerator.CreateAsync then narrows the document:
RefitDocumentFilter.FilterByTagskeeps only operations with one of theincludeTags.RefitDocumentFilter.FilterByPathkeeps only paths matching one of theincludePathMatchesregular expressions (evaluated with a 1 second regex timeout).SchemaCleaner.RemoveUnreferencedSchemaruns whentrimUnusedSchemais enabled.SchemaWalkercollects every schema reachable from the remaining operations, and everything else is removed, except schemas matchingkeepSchemaPatterns. WithincludeInheritanceHierarchydisabled, discriminator mappings that point at removed types are pruned too.
Each filter works on a clone of the document (ToJson() / FromJsonAsync()), so the original is never mutated. If path filtering removes every path, RefitterRunner reports an AllPathsFiltered diagnostic instead of silently producing an empty interface.
Preparing NSwag
CSharpClientGeneratorFactory first runs a fixed chain of document mutators. Each mutator rewrites schemas into a shape NSwag generates good contracts for:
flowchart LR
M1["DisableAdditionalProperties"] --> M2["ForceAdditionalProperties"]
M2 --> M3["FlattenPrimitiveAllOf"]
M3 --> M4["OneOfDiscriminatorToAllOf"]
M4 --> M5["InlineOneOfDerivedTypesToBaseType"]
M5 --> M6["FixMissingIntegerTypes"]
M6 --> M7["CustomIntegerType"]
It then builds NSwag's CSharpClientGeneratorSettings. NSwag is used only for contracts: GenerateClientClasses, GenerateClientInterfaces and GenerateExceptionClasses are all false, GenerateDtoTypes is true, and the JSON library is System.Text.Json. Refitter also plugs in its own naming components (UniquePropertyNameGenerator, SafeSchemaTypeNameGenerator, UniqueEnumNameGenerator, optionally PreserveOriginalPropertyNameGenerator) and a CustomTemplateFactory that patches NSwag's class template, for example for polymorphic serialization attributes. The codeGeneratorSettings section of the .refitter file is copied onto these settings.
The result is a CustomCSharpClientGenerator, a subclass of NSwag's CSharpClientGenerator that exposes NSwag's operation models and type resolver to Refitter.
GeneratorPipeline
GeneratorPipeline.Run produces the four parts of the output:
flowchart TD
G["CustomCSharpClientGenerator"] --> GF["GenerateFile()<br/>NSwag contracts (DTOs, enums)"]
GF --> PP1["Swagger2OptionalReferenceNullabilityNormalizer<br/>(Roslyn rewriter, Swagger 2 + nullable refs)"]
PP1 --> PP2["EnumStringConverterInjector<br/>JsonStringEnumConverter"]
PP2 --> CON["contracts"]
CON --> JSC{"generateJsonSerializerContext<br/>and generateContracts?"}
JSC -- yes --> JSG["JsonSerializerContextGenerator<br/>(Roslyn)"]
JSC -- no --> NOJ["(none)"]
IG["InterfaceGenerator.Generate(partitioning)"] --> IFACES["interfaces + wrapper types"]
IFACES --> OBS["ObsoleteContractAttributeRemover<br/>(when generating clients)"]
CON --> OBS
IFACES --> DI{"apizrSettings set?"}
DI -- yes --> APZ["ApizrRegistrationGenerator"]
DI -- no --> DIG["DependencyInjectionGenerator"]
OBS --> RES["GenerationResult<br/>Contracts · Interfaces · SerializerContext<br/>DependencyInjectionCode · InterfaceNames"]
JSG --> RES
APZ --> RES
DIG --> RES
Interface partitioning
multipleInterfaces decides how operations are grouped into Refit interfaces. Each mode is an IInterfacePartitioning implementation that returns a group key per operation.
flowchart LR
OPS["All operations<br/>(TRACE dropped: Refit has no attribute for it)"] --> P{"multipleInterfaces"}
P -- "Unset" --> S["SingleInterfacePartitioning<br/>key: empty<br/>one interface: I{Title}"]
P -- "ByEndpoint" --> E["ByEndpointInterfacePartitioning<br/>key: path#verb<br/>I{OperationName}, method Execute"]
P -- "ByTag" --> T["ByTagInterfacePartitioning<br/>key: first tag (or title)<br/>I{Tag}"]
Interface names are de-duplicated against the generated contract type names (adding an Endpoint or Api suffix where needed), and in multi-interface modes, when generateDeprecatedOperations is false, a group whose operations are all deprecated is skipped.
Generating a method
For every operation, InterfaceGenerator asks NSwag for its CSharpOperationModel and then builds the Refit method around it:
flowchart TD
OP["OpenApiOperation"] --> RT["ReturnTypeGenerator<br/>Task / Task of T / IApiResponse / IObservable"]
OP --> OM["NSwag CreateOperationModel"]
OM --> PLB
subgraph PLB["ParameterListBuilder.Build"]
direction TB
X1["RouteParameterExtractor"] --> X2["QueryParameterExtractor<br/>(may emit a dynamic querystring wrapper type)"]
X2 --> X3["BodyParameterExtractor"]
X3 --> X4["HeaderParameterExtractor"]
X4 --> X5["FormParameterExtractor<br/>(may emit a form object wrapper type)"]
X5 --> X6["OptionalParameterReorderer"]
X6 --> X7["append CancellationToken<br/>or Apizr request options"]
X7 --> X8["ParameterNameDeduplicator"]
X8 --> X9["RouteTemplateDisambiguator"]
end
RT --> EMIT
PLB --> EMIT
OP --> DOCS["XmlDocumentationGenerator"] --> EMIT
OP --> ATTR["MethodAttributeGenerator<br/>headers, multipart, etc."] --> EMIT
EMIT["[Verb(path)] ReturnType Name(parameters);"]
When Apizr is enabled together with optionalParameters, an extra overload without the optional parameters is emitted. Wrapper types created for dynamic querystrings and form objects are returned alongside the interfaces as non-interface GeneratedCode entries, so they end up in their own files in multi-file mode.
Dependency injection registration
When dependencyInjectionSettings is set, DependencyInjectionGenerator emits an IServiceCollection extension method that registers every generated interface:
flowchart LR
R["AddRefitClient (or AddRefitGeneratedClient)"] --> B["ConfigureHttpClient<br/>BaseAddress"]
B --> H["AddHttpMessageHandler<br/>for each configured handler"]
H --> T{"transientErrorHandler"}
T -- "Polly" --> PO["AddPolicyHandler<br/>WaitAndRetryAsync + DecorrelatedJitterBackoffV2"]
T -- "HttpResilience" --> HR["AddStandardResilienceHandler<br/>HttpRetryStrategyOptions"]
T -- "None" --> N["(nothing)"]
With apizrSettings, ApizrRegistrationGenerator emits either a Configure...ApizrManager(s) extension (DI) or a static Build...ApizrManager(s) method, composed from adapters for caching, mapping, mediation, file transfer, retry, priority and handlers.
Formatting the output
RefitGenerator formats the GenerationResult in one of two ways.
Single file (FormatSingleFile) concatenates, in order: interfaces (if generateClients), contracts (if generateContracts, with namespace prefixes already imported by the interface file removed), the serializer context, and the DI code. The contract type suffix is then applied, and RefitterRunner normalizes line endings to CRLF.
Multiple files (FormatMultipleFiles) returns one GeneratedCode per interface or wrapper type, plus Contracts, the serializer context type and DependencyInjection. Every file is named {TypeName}.cs.
Planning the output paths
OutputPlanner turns generated code into PlannedFile objects. For settings-file runs, paths are resolved against the .refitter file's directory. For direct CLI runs, paths are used as given and may be relative to the current directory: in single-file mode --output is the file path (default Output.cs); in multi-file mode --output is a directory (default the current directory) and each file is written as {TypeName}.cs inside it. The planner only computes paths; whether they are written is up to the host.
flowchart TD
START["GeneratorOutput"] --> MF{"multiple files?"}
MF -- no --> SF{"settings file run?"}
SF -- no --> CLI1["CLI --output value<br/>or Output.cs"]
SF -- yes --> SFP["outputFolder / outputFilename<br/>relative to the .refitter file"]
MF -- yes --> EACH["for each GeneratedCode"]
EACH --> CF{"Contracts file and<br/>contractsOutputFolder set and<br/>different from outputFolder?"}
CF -- yes --> CPATH["contractsOutputFolder/Contracts.cs"]
CF -- no --> OPATH["outputFolder (or CLI output dir)/TypeName.cs"]
Setting contractsOutputFolder implies multiple files: RefitterSettingsLoader.ApplyDefaults turns on generateMultipleFiles in that case, and defaults outputFilename to <name of the .refitter file>.cs.
Warnings and diagnostics
After generation, RefitterRunner adds:
| Kind | When |
|---|---|
WarningKind.DateFormatOverride |
dateFormat is set but ignored because useIsoDateFormat is enabled |
WarningKind.DeprecatedSetting |
The deprecated usePolly setting is used |
RunnerDiagnosticKind.AllPathsFiltered |
includePathMatches removed every path from the specification |
| Validation diagnostics | Errors and warnings from the optional IValidator |
Hosts decide which of these to surface. On success, the CLI prints the configuration warnings and the all-paths-filtered warning; validation errors and warnings are only printed when validation fails. Refitter.MSBuild inherits that output through the CLI. The source generator reports error diagnostics as REFITTER000 and the date-format warning as REFITTER002, but it does not report AllPathsFiltered, so a filter that removes every path silently produces an empty interface there.
Validation
Validation is optional and pluggable through IValidator. The CLI's OpenApiValidatorAdapter runs the reference guard, loads the specification with Microsoft.OpenApi (independently of NSwag), walks it to collect statistics (paths, operations, schemas and so on, shown by the CLI), and checks strings that end up inside C# attributes. Errors make ThrowIfInvalid() throw an OpenApiValidationException before anything is written.