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

The pain

Traefik is configured by YAML. Two YAML documents, in fact: a static config (entrypoints, providers, API, logging — read once at startup) and a dynamic config (routers, services, middlewares, TLS — hot-reloaded by the file provider). Both are validated against the JSON schemas Traefik publishes on SchemaStore.

In practice, every team that runs Traefik in production ends up writing one of three things:

  1. Hand-edited YAML in a text editor. No autocompletion, no type checking. A typo on entryPoints: vs entryPoint: produces a silently-empty section. A passHostHeader: yes instead of true is a string, not a bool, and the loadbalancer behaves differently from what you read.
  2. A Go templating layer. Helm, envsubst, Kustomize. The template engine doesn't know what Traefik expects either; it just substitutes strings. The validator runs at deploy time.
  3. A homegrown C# (or Python, or…) wrapper. Someone wrote class TraefikRouter once, three years ago, and it has drifted: it knows about EntryPoints but not the priority field added in 3.1. Nobody has updated it because nobody remembers it exists.

All three approaches share the same property: the Traefik schema is not the source of truth in your code. Something else is — a template, a hand-written class, your memory of yesterday's docs — and it has to be kept in sync with the schema by hand.

The schema is the source of truth

FrenchExDev.Net.Traefik.Bundle flips the dependency. The official Traefik JSON schema is checked into the repo, and every C# model, builder, validator, and version constant is derived from it at compile time, by a Roslyn incremental source generator. There is no second copy. There is no manual catch-up step when Traefik 3.1 adds a property: the generator picks it up the next time the schema file changes.

The runtime side is symmetric: the same JSON schema ships as an embedded resource, and the YAML/JSON serializer can validate any input or output against it via JsonSchema.Net before returning a typed POCO. A model that fails its own schema is a generator bug, surfaced at write time, not at Traefik's startup.

The five-project layout

Net/FrenchExDev/Traefik/
├── src/
│   ├── FrenchExDev.Net.Traefik.Bundle/                         (net10.0)   ← consumer-facing
│   │   ├── TraefikSerializer.cs
│   │   ├── YamlToJson.cs
│   │   └── schemas/
│   │       ├── traefik-v3-static.json
│   │       ├── traefik-v3-file-provider.json
│   │       └── traefik-v3.1-file-provider.json
│   ├── FrenchExDev.Net.Traefik.Bundle.Attributes/              (multi-target) ← marker attributes
│   │   ├── TraefikBundleAttribute.cs
│   │   └── TraefikDiscriminatedUnionAttribute.cs
│   ├── FrenchExDev.Net.Traefik.Bundle.SourceGenerator/         (netstandard2.0) ← Roslyn SG + analyzer
│   │   ├── TraefikBundleGenerator.cs
│   │   ├── TraefikSchemaReader.cs
│   │   ├── TraefikModelClassEmitter.cs
│   │   ├── TraefikBuilderHelper.cs
│   │   ├── VersionMetadataEmitter.cs
│   │   ├── SchemaModels.cs
│   │   ├── IrEquality.cs
│   │   └── Analyzers/
│   │       ├── DiscriminatedUnionAnalyzer.cs
│   │       └── TraefikDiagnostics.cs
│   └── FrenchExDev.Net.Traefik.Bundle.Design/                  (net10.0) ← schema downloader CLI
│       └── Program.cs
├── test/
│   ├── FrenchExDev.Net.Traefik.Bundle.Tests/                   (runtime tests)
│   └── FrenchExDev.Net.Traefik.Bundle.SourceGenerator.Tests/   (generator + analyzer tests)
├── samples/
│   └── realistic-dynamic.yaml
├── doc/
├── quality-gate.yml
└── coverage.runsettings

Each project has exactly one job:

Project Job TFM
Bundle Runtime API: serializer, embedded schemas, generated models net10.0
Bundle.Attributes [TraefikBundle] and [TraefikDiscriminatedUnion] markers netstandard2.0 + net10.0
Bundle.SourceGenerator Incremental generator + DiscriminatedUnionAnalyzer netstandard2.0
Bundle.Design One-shot CLI that downloads schemas from SchemaStore net10.0
Bundle.Tests xUnit + Shouldly fixtures, realistic round-trip net10.0
Bundle.SourceGenerator.Tests EmitterTests, AnalyzerTests, IrEqualityTests net10.0

The split is not academic. The generator must target netstandard2.0 to load into Roslyn; the consumer-facing library targets net10.0 so it can use Result<T>, JsonSerializerOptions, and modern async file I/O. The attributes project multi-targets so it can be referenced from both. The analyzer ships inside the source generator NuGet package under analyzers/dotnet/cs, so installing one package gives you the generator, the analyzer, and the shared BuilderEmitter library all at once — see Part 10.

What you get as a consumer

// Strongly typed, IDE-aware, schema-derived
var dynamic = new TraefikDynamicConfig
{
    Http = new TraefikDynamicHttp
    {
        Routers = new()
        {
            ["api-router"] = new TraefikHttpRouter
            {
                Rule = "Host(`api.example.com`) && PathPrefix(`/v1`)",
                EntryPoints = new() { "websecure" },
                Service = "api-backend",
                Middlewares = new() { "api-auth", "api-strip-prefix" }
            }
        },
        Middlewares = new()
        {
            ["api-auth"] = new TraefikHttpMiddleware
            {
                BasicAuth = new TraefikBasicAuthMiddleware
                {
                    Users = new() { "admin:$apr1$..." },
                    Realm = "API"
                }
                // Setting StripPrefix = ... on this same object would
                // be flagged by TFK001 in the IDE — see Part 7.
            }
        }
    }
};

// Schema-validated YAML emission, atomic file rename
await TraefikSerializer.WriteDynamicToFileAsync(
    "/etc/traefik/dynamic.yml", dynamic);

Every type in that snippet — TraefikDynamicConfig, TraefikHttpRouter, TraefikHttpMiddleware, TraefikBasicAuthMiddleware — is generated from the JSON schema. The Traefik 3.1 priority field on TraefikHttpRouter showed up automatically when the v3.1 schema was added to the schemas/ folder.

The next nine chapters take that pipeline apart, top to bottom, with the generator code on the left and the .g.cs output on the right.

Next: Part 2 — Harvesting the Schema →

⬇ Download