CLI Tool Internals
The refitter .NET tool (src/Refitter) is a thin host around RefitterRunner. Its job is to turn command line arguments or a .refitter file into RefitGeneratorSettings, validate them, run the generation, write files, and report what happened. It is also the engine behind Refitter.MSBuild, which runs it as a child process.
The CLI targets net8.0, net9.0 and net10.0 and is published as a .NET tool and as a Docker image.
End-to-end flow
sequenceDiagram
autonumber
actor User
participant Main as Program.Main
participant App as Spectre CommandApp
participant Cmd as GenerateCommand
participant Val as SettingsValidator
participant Map as SettingsMapper / RefitterSettingsLoader
participant Orc as GenerationOrchestrator
participant Run as RefitterRunner
participant FW as CliFileWriter
participant Rep as IGenerationReporter
User->>Main: refitter [spec] [options]
Main->>App: Run(args) (no args → --help)
App->>Cmd: Validate(context, settings)
Cmd->>Val: Validate(settings, out refitSettings)
Val-->>Cmd: ValidationResult (+ cached RefitGeneratorSettings)
App->>Cmd: ExecuteAsync(context, settings)
alt settings were cached by Validate
Cmd->>Cmd: use cached settings
else --settings-file
Cmd->>Map: RefitterSettingsLoader.LoadSettingsFile(json, path)
else command line only
Cmd->>Map: SettingsMapper.Map(settings)
end
Cmd->>Map: ApplyCliOverrides (settings-file runs only)
Cmd->>Cmd: CreateReporter (Silent / Simple / Rich)
Cmd->>Orc: RunAsync(refitSettings, settings, reporter)
Orc->>Rep: ReportHeader, ReportSupportKey
Orc->>Run: RunAsync(settings, CliFileWriter, OpenApiValidatorAdapter?)
Run->>FW: WriteAsync(path, content) per file
FW->>Rep: ReportFileWritten(path)
Run-->>Orc: RunResult
alt ExitCode == 0
Orc->>Orc: Analytics.LogFeatureUsage
Orc->>Orc: write .refitter file (command line runs only)
Orc->>Rep: warnings, ReportSuccess, banner
Orc-->>User: exit code 0
else failure
Orc->>Rep: ReportGenerationFailed + diagnostics + help
Orc->>Orc: Analytics.LogError
Orc-->>User: non-zero exit code
end
Parsing arguments
Program.Main creates a Spectre.Console.Cli CommandApp<GenerateCommand>. Every command line option is a property with a [CommandOption] attribute on Settings. Spectre calls GenerateCommand.Validate first and ExecuteAsync only if validation succeeds.
Validating settings
SettingsValidator has two paths:
flowchart TD
V["SettingsValidator.Validate"] --> AH["check authentication header style"]
AH --> ONE{"exactly one of<br/>OpenAPI path / --settings-file?"}
ONE -- no --> ERR["ValidationResult.Error"]
ONE -- "--settings-file" --> FILE
ONE -- "OpenAPI path" --> CLI
subgraph FILE["Settings file path"]
direction TB
F1["deserialize .refitter JSON<br/>(friendly message for bad enum values)"] --> F2["validate file and output settings"]
F2 --> F3["RefitterSettingsLoader.PrepareSettings<br/>apply defaults, resolve relative spec paths"]
F3 --> F4["check every spec file exists"]
F4 --> F5["check JSON library version conflicts"]
end
subgraph CLI["Command line path"]
direction TB
C1["validate multi-file output options"] --> C2["validate operation name template / URL"]
end
FILE --> OK["ValidationResult.Success<br/>+ RefitGeneratorSettings cached"]
CLI --> OK
The settings produced during validation are cached on the command so ExecuteAsync doesn't parse the file twice.
Building RefitGeneratorSettings
There are two sources of settings:
- A
.refitterfile.RefitterSettingsLoader(inRefitter.Core, shared with the source generator) deserializes the JSON and applies defaults. A positional OpenAPI path can't be combined with--settings-file: validation rejects it, so change the file instead:openApiPath, oropenApiPathsif the file lists several specifications (setting both fails validation). For--json-library-version, validation rejects a run where both the CLI and the file set a value other than8.0. OtherwiseSettingsMapper.ApplyCliOverridesapplies the CLI value when the file's value is8.0(whether set explicitly or by default), and keeps the file's value when only the file sets something else. - Command line options only.
SettingsMapper.MapusesConventionMapper, which copies every public property that has the same name and type on both CLISettingsandRefitGeneratorSettingsby reflection. Options whose meaning is inverted or combined are mapped explicitly afterwards, for exampleGenerateContracts = !InterfaceOnly,GenerateClients = !ContractOnly, andGenerateMultipleFilesis turned on by--multiple-filesor--contracts-output.
Because of the convention mapper, a new CLI option that matches an existing RefitGeneratorSettings property by name and type needs no extra mapping code. The ConventionMapperTests guard this.
Reporters
All console output goes through IGenerationReporter. The command picks one implementation per run:
classDiagram
class IGenerationReporter {
<<interface>>
ReportHeader(version)
ReportSupportKey(supportKey)
ReportFileWritten(path)
ReportSuccess(duration, multipleFiles)
ReportGenerationFailed()
ReportValidationDiagnostic(error, isError)
}
class RichGenerationReporter {
Spectre.Console markup
spinners, tables, panels
}
class SimpleGenerationReporter {
plain text
prints GeneratedFile marker lines
}
class SilentGenerationReporter {
no output
}
IGenerationReporter <|.. RichGenerationReporter : default
IGenerationReporter <|.. SimpleGenerationReporter : --simple-output
IGenerationReporter <|.. SilentGenerationReporter : --silent
| Reporter | Selected by | Used for |
|---|---|---|
RichGenerationReporter |
default | Interactive use: ASCII-art header, spinners, tables with OpenAPI statistics and generated files, panels for success and errors |
SimpleGenerationReporter |
--simple-output |
CI logs and other tools. Prints one GeneratedFile: <full path> line per written file |
SilentGenerationReporter |
--silent or "silent": true in the .refitter file |
No output at all |
The GeneratedFile: line is a contract with Refitter.MSBuild: the MSBuild task always passes --simple-output and reads those lines to know which files to compile.
Writing files
CliFileWriter implements IFileWriter. For every PlannedFile from OutputPlanner, it creates the directory, writes the content and tells the reporter. Writing only starts after validation has succeeded.
When the CLI was run without --settings-file, GenerationOrchestrator also serializes the effective settings to a .refitter file next to the output, so the same generation can later be repeated with --settings-file, Refitter.MSBuild or Refitter.SourceGenerator.
Errors and exit codes
RefitterRunner never throws; it returns a RunResult with an exit code. GenerationOrchestrator rethrows a captured exception so it can report it consistently:
flowchart TD
R["RunResult"] --> E{"ExitCode == 0?"}
E -- yes --> OK["success output<br/>exit 0"]
E -- no --> T{"exception type"}
T -- "OpenApiValidationException" --> VF["validation failed<br/>errors + warnings listed<br/>suggest --skip-validation"]
T -- "OpenApiUnsupportedSpecVersionException" --> UV["unsupported OpenAPI version"]
T -- "other" --> GE["exception details"]
VF --> SUP["support help + Analytics.LogError"]
UV --> SUP
GE --> SUP
SUP --> X["exit ToProcessExitCode(exception)"]
ToProcessExitCode returns the exception's HResult, unless its low 8 bits are zero, in which case it returns 1. Unix keeps only the low byte of a process exit code, and common HResults such as 0x80131500 (the default for System.Exception) end in 0x00. Without this mapping a failed generation would look like success to shells, CI systems and the MSBuild task.
Telemetry
Unless --no-logging is passed, Analytics records which options were used and reports errors. Feature-usage events don't include values for paths, URLs or namespaces. The exceptions are the telemetry options themselves: the --telemetry-source string is added to events as given, and MSBuild invocations also send --telemetry-runtime and the file count. Error reports are different: Analytics.LogError attaches the complete CLI Settings object (including the OpenAPI path, output path and namespace) to the exception sent to Exceptionless and Application Insights, without sanitizing it. Use --no-logging to send nothing. When the CLI is started by Refitter.MSBuild, extra --telemetry-source msbuild, --telemetry-file-count and --telemetry-runtime options tag the events so MSBuild usage can be told apart from direct CLI usage. For feature-usage events, settings files are passed through SettingsSanitizer.Sanitize, which keeps only known setting names and redacts free-text values.