Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

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.InitializeAdditionalTextsProvider, 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 DiagnosticAnalyzer and DiagnosticDescriptor

Conventions

Every technical chapter from Part 3 onward uses the same paired layout:

  1. A csharp block of the hand-written generator, emitter, or analyzer code
  2. A csharp block of the matching .g.cs output (or YAML / JSON for Part 8)
  3. 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.

Diagram
One pipeline, two consumers — the same schemas drive both the build-time generator and the runtime validator, with no second source of truth.

The arrows are deliberate: schemas drive both the build-time generator pipeline and the runtime validator. There is no second source of truth.

⬇ Download