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

Home Lab — C# All the Way Down

[TO HIRE] Senior Software Developer — Seeking team — 2026 — Full-Time Remote

During my job search period, I'm building a complete development infrastructure in C# — from foundational patterns to CI/CD pipelines to deployment and monitoring — proving that a single developer with the right abstractions can manage serious complexity. This is the operational heart of the FrenchExDev ecosystem.

The Big Picture

The Home Lab is not a toy project. It's a self-hosted software development platform: GitLab as the forge, typed C# wrappers for every CLI tool in the stack, and a CI/CD pipeline that builds, tests, and packages the 57 projects in the FrenchExDev monorepo. Every layer is code. Every tool has a typed API.

graph TB subgraph "Foundational Patterns" R["Result / Result"] --> BW[BinaryWrapper] B["Builder"] --> BW R --> Apps[Applications] B --> Apps end subgraph "CLI Wrappers (via BinaryWrapper)" BW --> Podman[Podman] BW --> DC[Docker Compose] BW --> Vagrant[Vagrant] BW --> Packer[Packer] BW --> GLab[GLab — GitLab CLI] end subgraph "Typed Config Bundles (via Source Generators)" DCBundle["DockerCompose.Bundle (32 schema versions)"] --> ComposeYml["Typed docker-compose.yml"] TBundle["Traefik.Bundle (v3 static + dynamic)"] --> TraefikYml["Typed traefik.yml"] end subgraph "Infrastructure" Podman --> Infra[Container Stack] DC --> Infra ComposeYml --> Infra Vagrant --> VM[VM Provisioning] Packer --> VM Infra --> GL[GitLab CE] Infra --> Runner[GitLab Runner] Infra --> Registry[Package Registry] TraefikYml --> Traefik[Traefik Reverse Proxy] Infra --> Traefik Infra --> Services[Other Services] end subgraph "CI/CD Pipeline" GLab --> Pipeline[Pipeline Orchestration] Pipeline --> CI[Build & Test] CI --> QG[Quality Gate] QG --> Pkg[NuGet / Package Delivery] end style R fill:#059669,stroke:#047857,color:#fff style B fill:#059669,stroke:#047857,color:#fff style BW fill:#2563eb,stroke:#1d4ed8,color:#fff style DCBundle fill:#d97706,stroke:#b45309,color:#fff style TBundle fill:#d97706,stroke:#b45309,color:#fff style GL fill:#7c3aed,stroke:#6d28d9,color:#fff style Pipeline fill:#7c3aed,stroke:#6d28d9,color:#fff
Everything in the Home Lab composes on two foundational patterns — Result and Builder — through BinaryWrapper into every CLI in the stack.

Foundational Patterns: Result & Builder

Every project in FrenchExDev builds on two foundational libraries. They aren't utilities — they're the grammar the entire ecosystem speaks.

Result — Explicit Error Handling Everywhere

Result<T> and Result<T, TError> replace exceptions as control flow. Every operation that can fail returns a Result. Every consumer must handle both paths. No silent swallowing, no surprises.

// 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 at scale. With 57 projects, error handling conventions must be mechanical, not cultural. If a scraper fails, if a parser hits an edge case, if a version guard rejects a flag — the error propagates through the same type-safe channel. Match, Bind, Map, Recover — the vocabulary is consistent from the lowest library to the highest application.

Builder — Typed Construction for Complex Objects

Builder<T> handles async object construction with validation accumulation, circular reference detection, and SemaphoreSlim-based caching. Every non-trivial object in the ecosystem is built through a Builder.

// 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 two patterns are the bedrock. Everything above — BinaryWrapper, CLI wrappers, pipeline orchestration — is built on this foundation.

GitLab as the Forge

The Home Lab runs GitLab CE as its software forge — source control, CI/CD pipelines, package registry, and issue tracking. GitLab is self-hosted on the infrastructure stack, provisioned through Podman containers managed by Docker Compose.

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, Podman Compose, Docker, Docker Compose, Packer, and Vagrant 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));

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

A monorepo with 57 projects isn't viable without automated CI/CD. Each project needs to be built, tested, quality-gated, and — for libraries — packaged and published. Doing this manually is impossible. Doing it with bash scripts is fragile. Doing it with typed C# orchestration against a self-hosted GitLab is the point.

The Pipeline Problem

Concern Scale Solution
Build 57 .csproj files, shared Directory.Packages.props Central Package Management, incremental builds
Test 400+ tests across 4 test projects (BinaryWrapper alone) GitLab Runner, parallel test execution
Quality Cyclomatic complexity, coverage, mutation scores QualityGate tool with per-project quality-gate.yml
Package 15+ libraries need NuGet delivery GitLab Package Registry, versioned artifacts
Orchestration Pipeline definitions, triggers, dependencies GLab wrapper + C# pipeline configuration

From Source to Package

Diagram
The full source-to-package path: typed C# orchestration drives GitLab CI through build, test, quality gate and packaging for 57 projects.

Quality Gates Close the Loop

Each project defines thresholds in quality-gate.yml:

gates:
  complexity:
    cyclomatic_max: 15
    cognitive_max: 20
  coverage:
    line_min: 80
    branch_min: 70
  mutation:
    score_min: 60

The QualityGate tool — itself part of the monorepo — runs Roslyn semantic analysis, ingests Cobertura coverage and Stryker mutation reports, and returns a pass/fail exit code. The pipeline stops if quality degrades. No exceptions, no overrides.

Package Delivery

Libraries that pass quality gates are packed and pushed to GitLab's built-in NuGet Package Registry. Downstream projects within the monorepo (and external consumers) pull versioned packages through standard NuGet feeds. The self-hosted registry means:

  • 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 physical infrastructure runs on the Home Lab hardware, provisioned through the same typed wrappers (all built on 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:

  1. Embed the official JSON schemas as AdditionalTexts
  2. Parse and merge them into a unified model (handling cross-version differences)
  3. 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);

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);

The result: infrastructure configuration with full IntelliSense, compile-time validation, and version-aware type safety. A typo in a service name or a missing required field is caught by the compiler, not by a failed deployment at 2 AM.

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

This Home Lab exists to answer a question: can a single developer, with the right patterns and tooling, build and maintain a professional-grade development platform?

The answer is yes — if you invest in foundations. Result and Builder give you a grammar for error handling and object construction that scales across 57 projects. BinaryWrapper gives you typed APIs for every CLI tool in your stack. GitLab gives you the forge. And C# ties it all together with compile-time safety.

The Home Lab is not the end goal — it's the proof that the ecosystem works. Every library, every wrapper, every pattern converges here: a self-hosted platform where FrenchExDev builds, tests, gates, and packages itself. The infrastructure-as-code philosophy drives every layer.


Built with .NET 10, Roslyn source generators, PowerShell 7, self-hosted GitLab CE, and the conviction that typed APIs beat string concatenation at any scale.

⬇ Download