Repository navigation
Development Notes
This guide is for developers contributing to the Atc.Rest.Api.SourceGenerator project. It covers the architecture, testing setup, and common development tasks.
The project uses a three-layer architecture that separates shared logic from Roslyn-specific code and CLI:
flowchart TB
subgraph SourceGen["Atc.Rest.Api.SourceGenerator"]
direction LR
ApiServer["ApiServerGenerator"]
ApiClient["ApiClientGenerator"]
ApiDomain["ApiDomainGenerator"]
end
subgraph RoslynHelpers["Roslyn-Specific Helpers"]
direction LR
DiagHelpers["🔧 DiagnosticHelpers"]
RoslynExtractor["🔍 RoslynSchemaExtractor"]
end
subgraph Generator["📦 Atc.Rest.Api.Generator"]
subgraph Extractors["🔧 Extractors (45+)"]
direction LR
Schema["SchemaExtractor"]
Enum["EnumExtractor"]
Handler["HandlerExtractor"]
More["...9 more"]
end
subgraph Services["⚙️ Services"]
CodeGen["CodeGenerationService"]
end
subgraph Support["Supporting Components"]
direction LR
Config["⚙️ Configurations"]
Valid["✅ Validators"]
Models["📋 Models"]
end
end
SourceGen --> Generator
RoslynHelpers --> Generator
✨ Benefits of this architecture:
- 🧪 Testability: Shared logic tested without Roslyn dependencies
- 🔄 Reusability: Generator library powers both Source Generator AND CLI tool
- 🛠️ Maintainability: Clear separation of concerns
- ⚡ Build performance: Smaller source generator assembly
atc-rest-api-source-generator/
├── 📁 src/
│ ├── 📦 Atc.NetStandard20/ # Core utilities
│ ├── 📦 Atc.CodeGeneration.CSharp/ # Code gen library
│ ├── 📦 Atc.CodeGeneration.TypeScript/ # TypeScript code gen primitives
│ ├── 📦 Atc.OpenApi/ # OpenAPI extensions
│ ├── 📦 Atc.Rest.Api.Generator/ # Shared library (ALL business logic)
│ │ ├── 📁 Abstractions/ # ICodeGenerator, IDiagnosticReporter
│ │ ├── 📁 Configurations/ # ServerConfig, ClientConfig, etc.
│ │ ├── 📁 Extractors/ # ALL 45+ extractors
│ │ ├── 📁 Services/ # CodeGenerationService
│ │ ├── 📁 Validators/ # CasingHelper, OpenApiDocumentValidator
│ │ └── 📁 Helpers/ # PathSegmentHelper, RuleIdentifiers
│ ├── 🔮 Atc.Rest.Api.SourceGenerator/ # Roslyn generators (thin wrappers)
│ ├── 🧪 Atc.Rest.Api.Client.Testing/ # HTTP test doubles for consumers (shipped package)
│ └── 💻 Atc.Rest.Api.Generator.Cli/ # CLI tool (net10.0)
├── 📁 sample/
│ ├── 🐕 PetStoreSimple/ # Basic 3-endpoint example
│ ├── 🐕 PetStoreFull/ # Extended example
│ ├── 🎪 Showcase/ # Full-featured demo
│ ├── 📑 MultipartDemo/ # Multi-file specification example
│ ├── 🔌 NexusSample/ # IoT/Edge device management API
│ ├── 🔌 MontaClient/ # Third-party spec, client only
│ ├── 🧪 ThirdParty-EPO-Clients/ # EndpointPerOperation client + unit tests
│ └── 🧪 ThirdParty-Typed-Clients/ # TypedClient client + unit tests
└── 📁 test/
├── 🧪 Atc.Rest.Api.Generator.Tests/ # Shared library unit tests
├── 🧪 Atc.Rest.Api.Generator.IntegrationTests/# Generator integration tests
├── 🧪 Atc.Rest.Api.SourceGenerator.Tests/ # SourceGenerator tests
└── 🧪 Atc.Rest.Api.Generator.Cli.Tests/ # CLI command testsThe generators follow the Extractor Pattern:
📄 OpenAPI Document -> 🔧 Extractors -> 📋 Parameter Records -> ⚙️ Code Generators -> 💻 C# CodeCore Extractors — Generate primary code artifacts:
| Extractor | Input | Output |
|---|---|---|
SchemaExtractor |
OpenAPI schemas |
RecordsParameters (models) |
EnumExtractor |
OpenAPI string enums | EnumParameters |
InlineEnumExtractor |
Inline enum schemas | Enums co-located with models |
InlineSchemaExtractor |
Inline object schemas | Inline record types |
PolymorphicTypeExtractor |
oneOf/anyOf schemas | Abstract records + converters |
TupleExtractor |
prefixItems (3.1) | Tuple record types |
HandlerExtractor |
OpenAPI operations | InterfaceParameters |
OperationParameterExtractor |
Operation params | ClassParameters |
ResultClassExtractor |
Operation responses | Result class code |
EndpointDefinitionExtractor |
Operations | EndpointDefinitionParameters |
EndpointRegistrationExtractor |
Operations | Endpoint route mappings |
HandlerScaffoldExtractor |
Operations | Handler stub code |
HttpClientExtractor |
Operations | TypedClient code |
EndpointPerOperationExtractor |
Operations | Per-operation endpoint classes |
ConstantsExtractor |
Project name + config | Constants class (HttpClientName) |
DI & Cross-Cutting Extractors — Generate dependency injection and middleware setup:
| Extractor | Input | Output |
|---|---|---|
DependencyRegistrationExtractor |
Handler/validator list | ServiceCollectionExtensions |
ServerDependencyInjectionExtractor |
Server config | Server DI setup code |
UnifiedServiceCollectionExtractor |
All DI fragments | Combined service registration |
SecurityDependencyInjectionExtractor |
Security schemes | Auth DI setup code |
SecurityPoliciesExtractor |
Security config | Authorization policy constants |
RateLimitDependencyInjectionExtractor |
x-ratelimit-* extensions | Rate limit DI setup |
RateLimitPoliciesExtractor |
x-ratelimit-* extensions | Rate limit policy constants |
ResilienceDependencyInjectionExtractor |
x-retry-* extensions |
AddApiResilience() setup |
ResiliencePoliciesExtractor |
x-retry-* extensions | Resilience policy constants |
HybridCacheDependencyInjectionExtractor |
x-cache-* extensions | HybridCache DI setup |
HybridCachePoliciesExtractor |
x-cache-* extensions | Cache policy constants |
OutputCacheDependencyInjectionExtractor |
x-cache-* extensions | OutputCache DI setup |
OutputCachePoliciesExtractor |
x-cache-* extensions | Output cache policy constants |
VersioningDependencyInjectionExtractor |
Versioning config | API versioning DI setup |
ValidatorDependencyRegistrationExtractor |
Validator types | Validator DI registration |
OAuth / OpenID Connect Extractors:
| Extractor | Input | Output |
|---|---|---|
OAuthConfigExtractor |
OAuth2 scheme | OAuth configuration class |
OAuthDependencyInjectionExtractor |
OAuth2 scheme | OAuth DI setup |
OAuthHandlerExtractor |
OAuth2 scheme | Token refresh handler |
OAuthOptionsExtractor |
OAuth2 scheme | OAuth options record |
OAuthTokenProviderExtractor |
OAuth2 scheme | Token provider service |
OpenIdConnectConfigExtractor |
OIDC scheme | OpenID Connect configuration |
OpenIdConnectDependencyInjectionExtractor |
OIDC scheme | OIDC DI setup |
Webhook Extractors (OpenAPI 3.1):
| Extractor | Input | Output |
|---|---|---|
WebhookHandlerExtractor |
Webhook operations | Webhook handler interfaces |
WebhookEndpointExtractor |
Webhook operations | Webhook endpoint registrations |
WebhookParameterExtractor |
Webhook parameters | Webhook parameter records |
WebhookResultExtractor |
Webhook responses | Webhook result types |
WebhookDependencyInjectionExtractor |
Webhook config | Webhook DI setup |
Shared Utilities:
| Extractor | Input | Output |
|---|---|---|
CustomErrorResponseExtractor |
Error config | Custom error response class |
ApiOptionsExtractor |
Generation config | Api options record |
WebApplicationExtensionsExtractor |
All endpoints |
app.Map{Project}Api() method |
ClassSchemaExtractor |
Complex schemas | Class-based models |
💡 Note: Only
RoslynSchemaExtractor(requires Roslyn for assembly scanning) andPolymorphicTypeEmitter(emitsoneOf/anyOfbase types straight into aSourceProductionContext, shared by the server and client generators) live in SourceGenerator. The CLI project has an additional ~22 TypeScript extractors inExtractors/TypeScript/.
test/
├── 🧪 Atc.Rest.Api.Generator.Tests/ # Shared library unit tests
├── 🧪 Atc.Rest.Api.Generator.IntegrationTests/ # Integration tests + TS-Client-* snapshots
├── 🧪 Atc.Rest.Api.SourceGenerator.Tests/ # C# generator snapshots + compilation gate
├── 🧪 Atc.Rest.Api.Generator.Cli.Tests/ # CLI command tests
└── 📁 Scenarios/ # Shared test scenarios
├── 🎪 Demo/ # Comprehensive demo scenario
├── 🐕 PetStoreSimple/ # Minimal 3-endpoint API
├── 🚦 RateLimit/ # x-ratelimit-* extensions
├── 🔄 Retry/ # x-retry-* resilience extensions
├── 🔐 Security*/ # Security scheme variations
└── 🔢 Versioning*/ # API versioning strategies# 🧪 Run all tests
dotnet test
# 🎯 Run specific test category
dotnet test --filter "FullyQualifiedName~GeneratorComparisonTests"
# 📋 Run tests for specific scenario
dotnet test --filter "DisplayName~PetStoreSimple"
# ✅ Run validation tests only
dotnet test --filter "FullyQualifiedName~ValidationTests"
# 📝 Run with verbose output
dotnet test -v detailedWe use Verify for snapshot testing generated code.
- 🧪
GeneratorScenarioSnapshotTestsruns the real Roslyn generator over a scenario - 📊 Each emitted source is compared against its
.verified.csbaseline file - ❌ Differences are shown in
.received.csfiles - 👀 Developer reviews and accepts/rejects changes
Snapshots live under {Scenario}/{MasterFolder}/ and are named after the generator hint name, which is the fully-qualified type name — flat in the master folder, with no Contracts/{Segment}/ nesting. Folder placement is a scaffolding concern the generator has no notion of; the hint name is the only identity it assigns.
Each scenario folder holds one subfolder per master folder, and the marker file lives inside it — so a scenario opts into a master folder simply by placing the marker there:
test/Scenarios/PetStoreSimple/
├── PetStoreSimple.yaml
├── Server/ .atc-rest-api-server + *.verified.cs
├── ServerDomain/ .atc-rest-api-server-handlers + *.verified.cs
├── Client-Typed/ .atc-rest-api-client + *.verified.cs
├── Client-Operation/ .atc-rest-api-client + *.verified.cs
└── TS-Client-*/ (TypeScript baselines)| Master folder | Marker file | Generator | Driven by |
|---|---|---|---|
Server |
.atc-rest-api-server |
ApiServerGenerator |
GeneratorScenarioSnapshotTests |
ServerDomain |
.atc-rest-api-server-handlers |
ApiServerDomainGenerator |
GeneratorScenarioSnapshotTests |
Client-Typed |
.atc-rest-api-client |
ApiClientGenerator |
GeneratorScenarioSnapshotTests |
Client-Operation |
.atc-rest-api-client |
ApiClientGenerator |
GeneratorScenarioSnapshotTests |
TS-Client-* |
— | CLI TypeScript path |
ScenarioTests (integration project) |
Client-Typed and Client-Operation share a generator and a marker file name; they differ only in the generationMode set inside their own copy of the marker.
The four C# folders are converted: they are snapshotted from the generators, and ScenarioDiscovery.ConvertedMasterFolders makes the integration-test suite skip them so exactly one suite owns each. TS-Client-* stays on the old path because there is no TypeScript incremental generator to converge onto.
⚠️ Never add a converted master folder back to the integration suite. Two suites verifying the same files against two implementations means whichever runs second rewrites the other's snapshots — which is how the two generation paths silently diverged in the first place.
Three tests guard each converted combination:
| Test | Guards |
|---|---|
Generator_MatchesSnapshot |
One test per emitted file, so a mismatch does not hide every later file |
Generator_EmitsSnapshots |
An empty generator run cannot quietly verify an empty set |
Snapshots_HaveNoOrphans |
Verify never deletes, so a stale baseline would otherwise pass forever |
Snapshots compare text and never compile, so a snapshot set can reference undefined types indefinitely. GeneratorCompilationGateTests closes that blind spot: it runs each generator over every scenario declaring its marker file and compiles the result for real.
Combinations that do not compile today are recorded in KnownNonCompilingCombinations as a ratchet, not a skip list — they are asserted to still fail, so a combination that starts compiling fails the test asking to be removed. The list can only shrink.
dotnet test --filter "FullyQualifiedName~GeneratorCompilationGateTests"ℹ️
ServerDomainis compiled together with the handler scaffolds it registers and the server output they implement against, so a{Scenario}/ServerDomainentry in the ratchet is usually derivative — it inherits its{Scenario}/Serverrow's failure and goes green when that row does.
When tests fail due to generated code changes:
# 📋 Review all pending changes
dotnet verify review
# ✅ Accept all changes (update baselines)
dotnet verify accept
# 📄 Accept specific file
dotnet verify accept --file "path/to/test.cs"-
📁 Create folder in
test/Scenarios/{ScenarioName}/ -
📄 Add OpenAPI spec:
{ScenarioName}.yaml -
⚙️ Add a subfolder per master folder you want covered, each holding its own marker file:
Server/ .atc-rest-api-server ServerDomain/ .atc-rest-api-server-handlers Client-Typed/ .atc-rest-api-client ("generationMode": "TypedClient") Client-Operation/ .atc-rest-api-client ("generationMode": "EndpointPerOperation")
-
▶️ Run tests to generate.received.csfiles -
✅ Review and accept:
dotnet verify accept
-
💾 Commit
.verified.csfiles to source control -
🚦 Make sure the new scenario passes the compilation gate:
dotnet test --filter "FullyQualifiedName~GeneratorCompilationGateTests"
A new scenario is expected to compile. Only add it to
KnownNonCompilingCombinationswhen it reproduces a known open generator defect, and say which one in a comment — the list is a ratchet that should be shrinking, not a parking space for new debt.
💡 Snapshot files are written under
{Scenario}/{MasterFolder}/for the four C# master folders. Delete a stale.verified.cswhen a type stops being generated;Snapshots_HaveNoOrphanswill fail until you do.
# 🐛 Debug build
dotnet build
# 📦 Release build (warnings as errors)
dotnet build -c Release# 🐕 PetStoreSimple
dotnet run --project sample/PetStoreSimple/PetStoreSimple.Api/PetStoreSimple.Api.csproj
# 🐕 PetStoreSimple with Aspire
dotnet run --project sample/PetStoreSimple/PetStoreSimple.Aspire/PetStoreSimple.Aspire.csproj
# 🎪 Showcase with Aspire
dotnet run --project sample/Showcase/Showcase.Aspire/Showcase.Aspire.csprojdotnet clean
rm -rf **/obj **/bin
dotnet buildBy default, source generator output only exists in memory during compilation. To inspect the generated code on disk, enable file emission in your .csproj:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>Generated files appear in:
obj/Debug/net9.0/generated/Atc.Rest.Api.SourceGenerator/
├── 📁 ApiServerGenerator/
│ └── *.g.cs
├── 📁 ApiClientGenerator/
│ └── *.g.cs
└── 📁 ApiServerDomainGenerator/
└── *.cs (physical files)To output generated files to a more accessible location, use CompilerGeneratedFilesOutputPath:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>This outputs to obj/Generated/ instead of the deep nested path, making it easier to browse and diff generated code.
For development, you may want to emit files only in Debug builds:
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>💡 Tip: Add
**/obj/Generated/to your.gitignoreto avoid committing generated files.
- 🔍 Inspect generated code: See exactly what the source generator produces
- 🐛 Debug compilation errors: Easier to understand errors when you can see the full generated file
- 📊 Compare changes: Track how generator updates affect output
- 🧪 Verify correctness: Manually review generated handlers, endpoints, and models
-
🛑 Add
Debugger.Launch()in generator:public void Initialize(IncrementalGeneratorInitializationContext context) { #if DEBUG if (!Debugger.IsAttached) { Debugger.Launch(); } #endif // ... }
-
🏗️ Build a project that uses the generator
-
🔗 Visual Studio will prompt to attach debugger
For a complete reference of all diagnostic rules, see Analyzer Rules.
| Rule ID | Description | Solution |
|---|---|---|
| ATC_API_DEP001 | Server requires ASP.NET Core | Add <FrameworkReference Include="Microsoft.AspNetCore.App" />
|
| ATC_API_DEP003 | EndpointPerOperation requires Atc.Rest.Client | Install Atc.Rest.Client NuGet package |
| ATC_API_DEP007 | useMinimalApiPackage requires package | Install Atc.Rest.MinimalApi or set to "disabled"
|
| ATC_API_OPR001 | Missing operationId | Add operationId to each operation (Strict mode); Standard mode synthesizes a name |
| ATC_API_VAL002 | OpenAPI 2.0 not supported | Upgrade to OpenAPI 3.0.x specification |
<PackageReference Include="Microsoft.OpenApi.YamlReader" Version="3.0.1" />
<PackageReference Include="Microsoft.OpenApi" Version="3.0.1" />
<PackageReference Include="SharpYaml" Version="2.1.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />🔮 Atc.Rest.Api.SourceGenerator
├── 📦 Atc.Rest.Api.Generator # Shared library
├── 📦 Atc.NetStandard20 # String utilities
├── 📦 Atc.CodeGeneration.CSharp # Code generation
└── 📦 Atc.OpenApi # OpenAPI extensions-
Microsoft.OpenApi v3.0.1: Breaking changes from v1.x
- Uses
IOpenApiSchemainterface - Pattern match
OpenApiSchemaReferencevsOpenApiSchema -
schemaRef.Reference.Idinstead ofschemaRef.Id -
JsonSchemaTypeenum instead of string types
- Uses
-
sample/projects are deliberately pinned toMicrosoft.OpenApi2.x (2.9.0), not 3.x. This is separate from — and independent of — thesrc/generator's ownMicrosoft.OpenApi3.x usage above.Microsoft.AspNetCore.OpenApi(ASP.NET Core's built-in OpenAPI generator, used by any sample with a Scalar/Swagger host UI) depends onMicrosoft.OpenApi2.0.0, which is vulnerable to GHSA-v5pm-xwqc-g5wc (NU1903). The pin lives insample/Directory.Build.propsso every sample project resolves to a patched version.-
Do not bump this pin to 3.x to "match
src/" —Microsoft.AspNetCore.OpenApi's built-inXmlCommentGeneratorsource generator assumes the 2.x API shape (IOpenApiMediaType.Exampleis settable there; 3.x made it read-only), and fails withCS0200against 3.x. See FAQ and Troubleshooting#-nu1903-microsoftopenapi-has-a-known-high-severity-vulnerability for the consumer-facing version of this. -
If you add a new sample project that references
Microsoft.AspNetCore.OpenApidirectly, it inherits the pin automatically viasample/Directory.Build.props— no per-project action needed. The CLI's project scaffolding (ProjectScaffoldingService.GenerateHostProjectContent) applies the equivalent pin to freshly generated consumer projects. - This pin has been silently lost from the working tree before (an uncommitted edit got dropped). If NU1903 warnings reappear across all sample projects, check
sample/Directory.Build.propsfirst.
-
Do not bump this pin to 3.x to "match
<PackageReference Include="Atc.Analyzer " Version="0.1.22" PrivateAssets="All" />
<PackageReference Include="AsyncFixer" Version="2.1.0" PrivateAssets="All" />
<PackageReference Include="Asyncify" Version="0.9.7" PrivateAssets="All" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.286" PrivateAssets="All" />
<PackageReference Include="SecurityCodeScan.VS2019" Version="5.6.7" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="All" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.18.0.131500" PrivateAssets="All" />- ✅ Use
#nullable enablein all generated code - 📝 Use records for models, classes for services
- ➡️ Prefer expression-bodied members
- 📖 XML documentation on public APIs
- 🔤 camelCase for private fields (no underscore prefix)
- 🍴 Fork the repository
- 🌿 Create a branch for your feature
- 💻 Implement following existing patterns
- 🧪 Add tests (validation tests, scenario tests)
- 🏗️ Run
dotnet build -c Release(must pass with 0 warnings) - ✅ Run
dotnet test(must pass) - 🚀 Submit PR with description of changes
| Resource | Path |
|---|---|
| 📦 Shared Library | src/Atc.Rest.Api.Generator/ |
| 🔮 Source Generators | src/Atc.Rest.Api.SourceGenerator/ |
| 🔧 Extractors (ALL 45+) | src/Atc.Rest.Api.Generator/Extractors/ |
| ⚙️ Services | src/Atc.Rest.Api.Generator/Services/ |
| ✅ Validators | src/Atc.Rest.Api.Generator/Validators/ |
| ⚙️ Configurations | src/Atc.Rest.Api.Generator/Configurations/ |
| 📄 OpenAPI Extensions | src/Atc.OpenApi/Extensions/ |
| 🧪 Test Scenarios | test/Scenarios/ |
| 📋 Samples | sample/ |
🏠 Home
- 🚀 Getting Started with Basic
- 🛠️ Getting Started with CLI
- 🔄 Migration Guide
- ⬆️ Upgrading to v2
- 📝 Working with OpenAPI
- 🕳️ Working with Nullability
- 🛠️ Working with CLI
- 📋 How-To Guides
- 🔒 Working with Security
- 🚦 Working with Rate Limiting
- 🔄 Working with Resilience
- 🗄️ Working with Caching
- 🔢 Working with Versioning
- ✅ Working with Validations
- 🔔 Working with Webhooks
- ☁️ Working with Aspire
- 🛣️ Working with Endpoint Definitions
- 📄 Working with Multi-Part Specs
- 🧪 Working with Code Coverage
- 🔌 Working with C# Client
- 🧪 Working with C# Client Testing
- ⚙️ Working with Configuration
- 📁 Marker Files
- 📖 API Reference
- 📊 Analyzer Rules
- ❓ FAQ and Troubleshooting
- 🗺️ Roadmap
- 🔧 Development Notes