A Schema-Driven Source Generator, an Analyzer, and a Round-Trip Serializer
This series walks through FrenchExDev.Net.Traefik.Bundle — a Roslyn incremental source generator that consumes Traefik's official JSON schemas as AdditionalFiles and emits compile-safe C# models, fluent builders, version metadata, and a flat-discriminated-union analyzer (TFK001, TFK004). The runtime side is a TraefikSerializer that round-trips YAML and JSON through the typed models with embedded JsonSchema.Net validation and atomic file writes.
The whole thing fits into a single repository with five projects, two NuGet packages (one runtime + one analyzer), two test projects, and a realistic sample. Every technical chapter shows the hand-written generator code paired with the .g.cs output it produces, so the relationship between schema → IR → emitter → consumer-visible API is never abstract.
Part 1: Why Strongly-Type Traefik?
Frame the pain — hand-edited YAML drift, no IntelliSense, no compile-time safety, schema churn between Traefik versions — and introduce the five-project layout: Bundle, Bundle.Attributes, Bundle.SourceGenerator, Bundle.Design, Bundle.Tests. The schema is the single source of truth; everything else is derived.
Part 2: Harvesting the Schema — the Design CLI
A short standalone .NET tool that pulls Traefik's official JSON schemas from SchemaStore and lands them on disk as traefik-v*.json. The consuming .csproj then includes them as <AdditionalFiles> and <EmbeddedResource> in one glob.
Part 3: Reading JSON Schemas in an Incremental Generator
Anatomy of TraefikBundleGenerator.Initialize — AdditionalTextsProvider, the parse Select, the merge Collect, and the final RegisterSourceOutput. The chapter highlights how each stage's value type implements structural equality so the cache actually fires.
Part 4: Value-Equal IR — Making the Generator Truly Incremental
Why a parsed SchemaModel graph that doesn't override equality silently defeats the whole IIncrementalGenerator cache, and the IrEquality helper that fixes it. This is the single highest-impact correctness change in the codebase, paired with IrEqualityTests.
Part 5: Emitting Models and Discriminated Unions
TraefikModelClassEmitter producing partial classes, XML doc comments lifted from JSON schema description fields, and the 25-property TraefikHttpMiddleware flat discriminated-union pattern with the [TraefikDiscriminatedUnion] marker the analyzer keys off of.
Part 6: Fluent Builders via a Shared Helper
How TraefikBuilderHelper bridges into FrenchExDev.Net.Builder.SourceGenerator.Lib to emit With{Property} overloads, and the __setCount == 1 epilogue on every flat-union builder so misuse fails at BuildAsync time instead of inside Traefik.
Part 7: Catching Misuse at Edit-Time — the DiscriminatedUnionAnalyzer
TFK001 flags object initializers that set more than one branch on a [TraefikDiscriminatedUnion] type. TFK004 is reported by the generator itself when the consuming project applies [TraefikBundle] but forgot to wire any schemas as AdditionalFiles. Layered defense, edit-time + build-time + runtime.
Part 8: YAML & JSON Round-Tripping, with Schema Validation
TraefikSerializer covers four directions: YAML ↔ models, JSON ↔ models, plus async file I/O with atomic rename for the Traefik file-provider use case. The Try* API runs JsonSchema.Net Evaluate() against the embedded schema before returning a typed model, and the realistic dynamic sample exercises the whole pipeline.
Part 9: Tests, Property Checks, and Quality Gates
Two test projects: runtime (Bundle.Tests) and generator (Bundle.SourceGenerator.Tests). xUnit + Shouldly fixtures, hand-rolled CSharpCompilation for the analyzer (no Microsoft.CodeAnalysis.Testing dependency), EmitterTests pinning generated source as plain strings, and IrEqualityTests defending the cache contract.
Part 10: Versioning, Packaging, and Lessons Learned
Two real Traefik versions are loaded side-by-side (v3 and v3.1), VersionMetadataEmitter produces a typed TraefikSchemaVersions registry, and the source generator + its shared BuilderEmitter library both pack to analyzers/dotnet/cs so consumers get one nupkg pull. Plus the honest "next steps" list.
How to Read This Document
.NET architects should read Part 1 and Part 10 for the full motivation and the trade-offs that shaped the project, then Part 7 for the layered-defense pattern.
Source generator authors should focus on Part 3, Part 4, Part 5, and Part 6 — these are where the cache discipline, emitter design, and shared-library reuse pattern live.
Roslyn analyzer authors should jump to Part 7 for the analyzer + diagnostic descriptor catalog and Part 9 for the no-Microsoft.CodeAnalysis.Testing testing style.
DevOps engineers configuring Traefik from .NET should read Part 8 for the serializer surface, including the atomic-rename file-provider use case, and Part 10 for the NuGet packaging model.
Prerequisites
- C# 12+ / .NET 10
- Working knowledge of Roslyn
IIncrementalGenerator(the official cookbook is enough) - Familiarity with Traefik static vs dynamic configuration is helpful but not required
- For Part 7: basic familiarity with
DiagnosticAnalyzerandDiagnosticDescriptor
Conventions
Every technical chapter from Part 3 onward uses the same paired layout:
- A
csharpblock of the hand-written generator, emitter, or analyzer code - A
csharpblock of the matching.g.csoutput (or YAML / JSON for Part 8) - One paragraph explaining the relationship
All code excerpts are read directly from the project at Net/FrenchExDev/Traefik — nothing in this series is fabricated. File references use markdown link syntax like TraefikBuilderHelper.cs:59-66. Generated files live under obj/Generated/.../FrenchExDev.Net.Traefik.Bundle.SourceGenerator.TraefikBundleGenerator/ because EmitCompilerGeneratedFiles=true is set in the consuming .csproj.
The arrows are deliberate: schemas drive both the build-time generator pipeline and the runtime validator. There is no second source of truth.