The Design CLI
The whole pipeline starts with a few JSON files on disk. Where do they come from? Not from a hand-maintained copy — from a tiny .NET console app that pulls them from SchemaStore on demand. Here is the entire program (Bundle.Design/Program.cs):
using FrenchExDev.Net.Wrapper.Versioning;
var outputDir = Path.GetFullPath(Path.Combine(
AppContext.BaseDirectory, "..", "..", "..", "..",
"FrenchExDev.Net.Traefik.Bundle", "schemas"));
var schemas = new (string Name, string Url)[]
{
("static", "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/traefik-v3.json"),
("file-provider", "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/traefik-v3-file-provider.json"),
};
var pipeline = new DesignPipeline<(string Name, string Url)>()
.UseHttpDownload(item => item.Url)
.UseSave()
.Build();
return await new DesignPipelineRunner<(string Name, string Url)>
{
ItemCollector = new StaticItemCollector<(string Name, string Url)>(schemas),
Pipeline = pipeline,
KeySelector = item => item.Name,
OutputDir = outputDir,
OutputFilePattern = "traefik-v3-{key}.json",
}.RunAsync(args);using FrenchExDev.Net.Wrapper.Versioning;
var outputDir = Path.GetFullPath(Path.Combine(
AppContext.BaseDirectory, "..", "..", "..", "..",
"FrenchExDev.Net.Traefik.Bundle", "schemas"));
var schemas = new (string Name, string Url)[]
{
("static", "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/traefik-v3.json"),
("file-provider", "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/traefik-v3-file-provider.json"),
};
var pipeline = new DesignPipeline<(string Name, string Url)>()
.UseHttpDownload(item => item.Url)
.UseSave()
.Build();
return await new DesignPipelineRunner<(string Name, string Url)>
{
ItemCollector = new StaticItemCollector<(string Name, string Url)>(schemas),
Pipeline = pipeline,
KeySelector = item => item.Name,
OutputDir = outputDir,
OutputFilePattern = "traefik-v3-{key}.json",
}.RunAsync(args);That's it. dotnet run --project src/FrenchExDev.Net.Traefik.Bundle.Design writes:
src/FrenchExDev.Net.Traefik.Bundle/schemas/
├── traefik-v3-static.json
├── traefik-v3-file-provider.json
└── traefik-v3.1-file-provider.json ← added by hand for the v3.1 testsrc/FrenchExDev.Net.Traefik.Bundle/schemas/
├── traefik-v3-static.json
├── traefik-v3-file-provider.json
└── traefik-v3.1-file-provider.json ← added by hand for the v3.1 testThis is intentionally a one-shot tool, not a build step. Schemas don't change every CI run. When Traefik publishes a new schema, a maintainer runs the Design CLI, eyeballs the diff, and commits the result. The build is reproducible because the schema files are in source control; the Design CLI is the human-driven refresh button.
DesignPipeline<T> and DesignPipelineRunner<T> come from FrenchExDev.Net.Wrapper.Versioning, a sibling utility in the monorepo that gives you UseHttpDownload / UseSave building blocks plus version-aware OutputFilePattern substitution. It's reused by every "go fetch the upstream schema" tool in the FrenchExDev stack.
The csproj wiring
Once the JSON files are on disk, the consuming Bundle project needs to do two things with them. First, hand them to the source generator at build time. Second, embed them in the assembly so the runtime serializer can validate against the same bytes.
Here is the relevant slice of FrenchExDev.Net.Traefik.Bundle.csproj:
<ItemGroup>
<ProjectReference
Include="..\FrenchExDev.Net.Traefik.Bundle.SourceGenerator\FrenchExDev.Net.Traefik.Bundle.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<ProjectReference
Include="..\..\..\Builder\src\FrenchExDev.Net.Builder.SourceGenerator.Lib\FrenchExDev.Net.Builder.SourceGenerator.Lib.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="YamlDotNet" />
<PackageReference Include="JsonSchema.Net" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="schemas\traefik-v*.json" />
<EmbeddedResource Include="schemas\traefik-v*.json" />
</ItemGroup><ItemGroup>
<ProjectReference
Include="..\FrenchExDev.Net.Traefik.Bundle.SourceGenerator\FrenchExDev.Net.Traefik.Bundle.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<ProjectReference
Include="..\..\..\Builder\src\FrenchExDev.Net.Builder.SourceGenerator.Lib\FrenchExDev.Net.Builder.SourceGenerator.Lib.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="YamlDotNet" />
<PackageReference Include="JsonSchema.Net" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="schemas\traefik-v*.json" />
<EmbeddedResource Include="schemas\traefik-v*.json" />
</ItemGroup>Three things matter here:
OutputItemType="Analyzer" ReferenceOutputAssembly="false"— the source generator and the sharedBuilderEmitterlibrary are loaded into the Roslyn analyzer host, not added as runtime references. Roslyn can only loadnetstandard2.0analyzers, and the runtimeBundledoesn't need either DLL once code is generated.<AdditionalFiles>— this is what makes the JSON files visible toIIncrementalGeneratorviacontext.AdditionalTextsProvider. Without this line, the generator runs but sees no schemas, and reports the TFK004 diagnostic at compile time (see Part 7). The glob istraefik-v*.jsonso addingtraefik-v3.1-file-provider.jsonto the folder is all you have to do — no.csprojedit.<EmbeddedResource>with the same glob — the runtimeTraefikSerializer.LoadEmbeddedSchema()does anasm.GetManifestResourceNames().FirstOrDefault(n => n.EndsWith(fileName))lookup, so the build-time and runtime sides see the byte-identical schema files. One folder, one glob, two consumers. No drift possible.
Why this matters for the rest of the series
Everything from Part 3 onward assumes the generator can see traefik-v3-static.json, traefik-v3-file-provider.json, and traefik-v3.1-file-provider.json as AdditionalFile instances. The Design CLI is how they get there. The AdditionalFiles glob is how Roslyn finds them. The EmbeddedResource glob is how the runtime serializer loads them. Three lines of .csproj are the entire pipeline plumbing.
← Part 1: Why Strongly-Type Traefik? · Next: Part 3 — Reading JSON Schemas →