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.
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));// 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);// 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));[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
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: 60gates:
complexity:
cyclomatic_max: 15
cognitive_max: 20
coverage:
line_min: 80
branch_min: 70
mutation:
score_min: 60The 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) |
| Forge | GitLab CE | FrenchExDev.Net.GLab (scraped via BinaryWrapper) |
| Reverse proxy | Traefik | PowerShell module (PoSh) |
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 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.