Home Lab — C# All the Way Down
[TO HIRE] Senior Solution Architect — Seeking team — 2026 — Full-Time Remote
I am working toward a reproducible development platform described and controlled from C#. The FrenchExDev ecosystem already contains libraries, typed CLI wrappers, configuration models and VM orchestration components. Bringing them together into a complete HomeLab remains integration work.
Anything-as-Code connects this work to Requirements-as-Code and Diem. Requirements-as-Code represents software intent and its verification relationships. Diem extends the approach to applications and their environment; the HomeLab is the infrastructure integration target. C# becomes the language for describing an instance, composing its modules and operating its services.
The Big Picture
The target is a self-hosted software development platform: GitLab as the forge, typed tool integrations, and a CI/CD path for building, testing and packaging the monorepo. Existing components make parts of that path expressible in C#. The complete instance model, coordinated lifecycle and end-to-end integration are still being developed.
The diagrams describe how the components are intended to fit together. The code snippets illustrate composition patterns, including proposed pipeline APIs; the Diem building-block references identify current source implementations. They do not establish that the complete platform is running.
Foundational Patterns: Result & Builder
Result and Builder provide reusable conventions for error handling and object construction across the ecosystem. Those conventions also inform the planned Lab composition layer.
Result — Explicit Error Handling Everywhere
Result<T> and Result<T, TError> make success and failure explicit in the APIs that use them. Consumers can inspect or compose the result through a shared vocabulary.
// BinaryWrapper uses Result throughout
Result<Reference<PodmanClient>> client = await new PodmanClientBuilder()
.WithBinding(binding)
.BuildAsync();
// Pipeline orchestration: chain operations that may fail
Result<BuildReport> report = await ParsePipeline(config)
.Bind(pipeline => RunStages(pipeline))
.Bind(stages => CollectArtifacts(stages))
.Map(artifacts => GenerateReport(artifacts));// BinaryWrapper uses Result throughout
Result<Reference<PodmanClient>> client = await new PodmanClientBuilder()
.WithBinding(binding)
.BuildAsync();
// Pipeline orchestration: chain operations that may fail
Result<BuildReport> report = await ParsePipeline(config)
.Bind(pipeline => RunStages(pipeline))
.Bind(stages => CollectArtifacts(stages))
.Map(artifacts => GenerateReport(artifacts));This matters as the monorepo grows. Scraper failures, parser errors and rejected options can use the same typed result channel. Match, Bind, Map and Recover make the handling visible to consumers and tools.
Builder — Typed Construction for Complex Objects
Builder<T> supports async object construction with validation accumulation, circular reference detection and SemaphoreSlim-based caching. The following pipeline example illustrates how that composition style could serve the integrated Lab.
// Building a GitLab pipeline configuration
var pipeline = await new PipelineConfigBuilder()
.WithProject("FrenchExDev")
.WithStages(stages => stages
.Add(s => s.WithName("build").WithImage("mcr.microsoft.com/dotnet/sdk:10.0"))
.Add(s => s.WithName("test").WithImage("mcr.microsoft.com/dotnet/sdk:10.0"))
.Add(s => s.WithName("package").WithImage("mcr.microsoft.com/dotnet/sdk:10.0")))
.WithArtifacts(a => a.WithExpiry(TimeSpan.FromDays(30)))
.BuildAsync(ct);// Building a GitLab pipeline configuration
var pipeline = await new PipelineConfigBuilder()
.WithProject("FrenchExDev")
.WithStages(stages => stages
.Add(s => s.WithName("build").WithImage("mcr.microsoft.com/dotnet/sdk:10.0"))
.Add(s => s.WithName("test").WithImage("mcr.microsoft.com/dotnet/sdk:10.0"))
.Add(s => s.WithName("package").WithImage("mcr.microsoft.com/dotnet/sdk:10.0")))
.WithArtifacts(a => a.WithExpiry(TimeSpan.FromDays(30)))
.BuildAsync(ct);Result and Builder work together: BuildAsync() returns Result<Reference<T>>, validation errors accumulate into the Result's failure channel, and the builder's Reference<T> handles lazy resolution for object graphs with circular dependencies.
These patterns provide reusable foundations. The Lab composition work builds on them alongside the existing wrappers and configuration models.
GitLab as the Forge
GitLab CE is the intended forge for the integrated HomeLab: source control, CI/CD pipelines, package registry and issue tracking. The target stack brings GitLab and its runners together through the container and configuration modules. Their coordinated provisioning and operation remain integration work.
GLab: C# Talking to GitLab
To orchestrate GitLab programmatically from C#, I scraped GLab (GitLab's official CLI) using BinaryWrapper — the same framework that powers the Podman (ouvre dans une nouvelle fenêtre), Podman Compose (ouvre dans une nouvelle fenêtre), Docker (ouvre dans une nouvelle fenêtre), Docker Compose (ouvre dans une nouvelle fenêtre), Packer (ouvre dans une nouvelle fenêtre), and Vagrant (ouvre dans une nouvelle fenêtre) wrappers.
[BinaryWrapper("glab", FlagPrefix = "--")]
public partial class GLabDescriptor;
// Full IntelliSense for every glab command
var client = GLab.Create(binding);
// Create a merge request
var cmd = client.MrCreate(mr => mr
.WithTitle("feat: add quality gate to CI pipeline")
.WithDescription("Adds QualityGate step after test stage")
.WithSourceBranch("feature/quality-gate")
.WithTargetBranch("main")
.WithSquashBeforeMerge(true));
// Trigger a pipeline
var pipelineCmd = client.PipelineRun(p => p
.WithBranch("main")
.WithVariables(["DEPLOY_TARGET=staging"]));
// List project releases
var releasesCmd = client.ReleaseList(r => r
.WithPerPage(10));[BinaryWrapper("glab", FlagPrefix = "--")]
public partial class GLabDescriptor;
// Full IntelliSense for every glab command
var client = GLab.Create(binding);
// Create a merge request
var cmd = client.MrCreate(mr => mr
.WithTitle("feat: add quality gate to CI pipeline")
.WithDescription("Adds QualityGate step after test stage")
.WithSourceBranch("feature/quality-gate")
.WithTargetBranch("main")
.WithSquashBeforeMerge(true));
// Trigger a pipeline
var pipelineCmd = client.PipelineRun(p => p
.WithBranch("main")
.WithVariables(["DEPLOY_TARGET=staging"]));
// List project releases
var releasesCmd = client.ReleaseList(r => r
.WithPerPage(10));The same three-phase architecture applies: scrape GLab's --help across versions, generate typed commands and builders via Roslyn, execute with structured output parsing. GLab joins the wrapper family alongside Podman (58 versions, 180+ commands), Docker Compose (57 versions), Vagrant, and Packer.
This means C# can drive the entire GitLab workflow — creating projects, triggering pipelines, managing merge requests, publishing releases — with full type safety and version guards, not string concatenation against a REST API.
57 Projects Need CI/CD
The 57-project figure in this heading reflects an earlier snapshot of the monorepo. The enduring need is to coordinate builds, tests, quality checks and package delivery as the repository grows. Typed C# orchestration against a self-hosted GitLab is the Lab integration target; the source-to-package path below is the intended workflow.
The Pipeline Problem
| Concern | Need | Target integration |
|---|---|---|
| Build | Coordinate projects and shared package versions | Central Package Management, incremental builds |
| Test | Discover, execute and report relevant checks | GitLab Runner, parallel test execution |
| Quality | Evaluate complexity, coverage and mutation results | QualityGate tool with per-project quality-gate.yml |
| Package | Deliver versioned libraries | GitLab Package Registry, versioned artifacts |
| Orchestration | Pipeline definitions, triggers, dependencies | GLab wrapper + C# pipeline configuration |
From Source to Package
Quality Gates Close the Loop
The planned pipeline would evaluate per-project thresholds in quality-gate.yml, for example:
gates:
complexity:
cyclomatic_max: 15
cognitive_max: 20
coverage:
line_min: 80
branch_min: 70
mutation:
score_min: 60gates:
complexity:
cyclomatic_max: 15
cognitive_max: 20
coverage:
line_min: 80
branch_min: 70
mutation:
score_min: 60The QualityGate tooling is intended to contribute analysis and report-based decisions to this path. Lab integration must connect the relevant analysis, coverage and mutation runs to the configured policy, preserve their results, and stop delivery when a required check fails. That complete pipeline behavior remains to be integrated and verified.
Package Delivery
In the target workflow, libraries accepted by the configured checks would be packed and pushed to GitLab's NuGet Package Registry. Consumers would retrieve versioned packages through standard NuGet feeds. The intended benefits are:
- No external dependency on nuget.org for internal packages
- Version control tied to the same GitLab instance that runs CI
- Access control through GitLab's existing permission model
Infrastructure Stack
The infrastructure composition work draws on these tools and typed components. The table maps their intended roles in a HomeLab instance; it does not establish a deployed stack. The CLI wrappers use BinaryWrapper.
| Layer | Tool | C# Wrapper |
|---|---|---|
| VM provisioning | Vagrant | FrenchExDev.Net.Vagrant (7 versions, custom parser, typed events) |
| Image building | Packer | FrenchExDev.Net.Packer (multi-version, HCL workflows) |
| Containers | Podman | FrenchExDev.Net.Podman (58 versions, 180+ commands, 18 groups) |
| Orchestration | Docker Compose | FrenchExDev.Net.DockerCompose (57 versions, 37 commands) + DockerCompose.Bundle (typed config from 32 schema versions) |
| Forge | GitLab CE | FrenchExDev.Net.GLab (scraped via BinaryWrapper) |
| Reverse proxy | Traefik v3 | Traefik.Bundle (typed static + dynamic config from JSON schemas) + PowerShell module (PoSh) |
Typed Configuration: No More YAML by Hand
The CLI wrappers handle executing Docker Compose and Traefik — but what about the configuration files themselves? Writing docker-compose.yml or traefik.yml by hand means no type checking, no IntelliSense, and no compile-time validation. The Bundle projects solve this.
Both DockerCompose.Bundle and Traefik.Bundle follow the same Roslyn source generator pattern:
- Embed the official JSON schemas as
AdditionalTexts - Parse and merge them into a unified model (handling cross-version differences)
- Generate typed C# models + Builder classes for every configuration object
DockerCompose.Bundle ingests 32 versions of the official Compose spec (v1.0.9 to v2.10.1) and generates typed models for ComposeFile, ComposeService, networks, volumes, and every nested configuration object:
var compose = await new ComposeFileBuilder()
.WithServices(services => services
.Add("gitlab", s => s
.WithImage("gitlab/gitlab-ce:latest")
.WithPorts(["8080:80", "2222:22"])
.WithVolumes(["gitlab-config:/etc/gitlab", "gitlab-data:/var/opt/gitlab"])
.WithRestart("unless-stopped"))
.Add("runner", s => s
.WithImage("gitlab/gitlab-runner:latest")
.WithVolumes(["/var/run/docker.sock:/var/run/docker.sock"])))
.WithVolumes(v => v
.Add("gitlab-config", vol => vol.WithDriver("local"))
.Add("gitlab-data", vol => vol.WithDriver("local")))
.BuildAsync(ct);var compose = await new ComposeFileBuilder()
.WithServices(services => services
.Add("gitlab", s => s
.WithImage("gitlab/gitlab-ce:latest")
.WithPorts(["8080:80", "2222:22"])
.WithVolumes(["gitlab-config:/etc/gitlab", "gitlab-data:/var/opt/gitlab"])
.WithRestart("unless-stopped"))
.Add("runner", s => s
.WithImage("gitlab/gitlab-runner:latest")
.WithVolumes(["/var/run/docker.sock:/var/run/docker.sock"])))
.WithVolumes(v => v
.Add("gitlab-config", vol => vol.WithDriver("local"))
.Add("gitlab-data", vol => vol.WithDriver("local")))
.BuildAsync(ct);Traefik.Bundle reads the Traefik v3 JSON schemas (static configuration + file provider/dynamic configuration) and generates TraefikStaticConfig and TraefikDynamicConfig models with full builder support:
var dynamicConfig = await new TraefikDynamicConfigBuilder()
.WithHttp(http => http
.WithRouters(r => r
.Add("gitlab", router => router
.WithRule("Host(`gitlab.homelab.local`)")
.WithService("gitlab-svc")
.WithEntryPoints(["websecure"])
.WithTls(tls => tls.WithCertResolver("letsencrypt"))))
.WithServices(s => s
.Add("gitlab-svc", svc => svc
.WithLoadBalancer(lb => lb
.WithServers([new() { Url = "http://gitlab:80" }])))))
.BuildAsync(ct);var dynamicConfig = await new TraefikDynamicConfigBuilder()
.WithHttp(http => http
.WithRouters(r => r
.Add("gitlab", router => router
.WithRule("Host(`gitlab.homelab.local`)")
.WithService("gitlab-svc")
.WithEntryPoints(["websecure"])
.WithTls(tls => tls.WithCertResolver("letsencrypt"))))
.WithServices(s => s
.Add("gitlab-svc", svc => svc
.WithLoadBalancer(lb => lb
.WithServers([new() { Url = "http://gitlab:80" }])))))
.BuildAsync(ct);The result is infrastructure configuration that can be discovered and composed through typed models and IDE completion. Compiler feedback checks the relationships expressed in those types. Configuration validation and execution checks are still needed for values, cross-service references and runtime behavior.
PowerShell Glue
While C# handles the heavy lifting, PowerShell modules provide the developer experience layer:
- DevPoSh — Developer shell boilerplate: UTF-8, logging, module auto-loading, VS Code integration
- InfraDev — Infrastructure orchestration cmdlets tying Vagrant, Packer, Docker Compose, and Traefik together
- Claude.PoSh — Claude Code VM lifecycle management
Tech Stack
C# .NET 10 Roslyn Source Generators PowerShell 7 Podman Docker Compose Vagrant Packer GitLab CE GLab Traefik v3 Alpine Linux NuGet
The Point
The HomeLab is the integration target for Anything-as-Code applied to a development environment: a C# description, typed models, generated configuration and coordinated operations leading to reproducible infrastructure.
Result and Builder, BinaryWrapper, configuration Bundles and VM components provide existing building blocks. Diem brings the application and environment work toward a shared product experience. The next task is to compose those blocks and verify the complete lifecycle of an instance.
That integration will provide a concrete place to evaluate the ecosystem: provisioning, configuration, builds, tests and delivery exercised together. Follow the Diem presentation for the current direction and the HomeLab roadmap for the dependency-based sequence.
Existing C# and PowerShell components; an integrated, reproducible HomeLab in development.