Skip to content

Portable WSLC backend for net10.0, Testcontainers-parity connection strings, repo renamed to containers - #3

Merged
kieronlanning merged 11 commits into
mainfrom
refactor-into-multitarget
Oct 1, 2026
Merged

kieronlanning merged 11 commits into
mainfrom
refactor-into-multitarget

Conversation

@kieronlanning

Copy link
Copy Markdown
Contributor

Summary

Portable WSLC backend (headline)

Purview.Containers.Wsl is now multi-target:

  • net10.0 - a portable facade that loads the Windows implementation from a wslc/ payload at run
    time and reports wsl unavailable everywhere else, so auto selection falls through to Docker.
  • net10.0-windows10.0.19041.0 - the WSLC implementation (compiled against the
    Microsoft.WSL.Containers projection, which is a net8.0-windows asset).

A plain net10.0 test project can therefore reference the WSL backend and get WSLC on a Windows
developer machine and Docker on a Linux CI runner with no target-framework or configuration change.
This corrects the docs, which claimed WSLC required a .NET 11 Windows project.

  • Packaging: the implementation, WSLC projection, its Windows SDK dependencies and the native SDK are
    packed under payload/win-{x64,arm64}; the buildTransitive targets copy the matching folder for a
    platform-neutral consumer on Windows.
  • Guards: PCC0001 accepts any .NET 10+ target (Windows 10.0.19041.0+ or platform-neutral); PCC0002
    still rejects a 32-bit Windows consumer.
  • Selection: a new optional IContainerBackendPreference.AutoPriority makes auto prefer WSLC (0) over
    Docker (100) deterministically, instead of relying on the MSBuild props-import order.

Testcontainers connection-string parity

SQL Server GetConnectionString() now sets Database=master by default (configurable with the new
WithDatabase(...)), matching Testcontainers.MsSql. The other modules already matched (Redis
host:port, PostgreSQL/MySQL client strings, RabbitMQ amqp://, Azurite, NATS nats://), and
docs/wiki/Modules.md now documents the per-module connection-string shape.

Metadata and repository rename

  • Repository renamed purview-dev/wsl-containers -> purview-dev/containers; every reference
    updated (package.json, mkdocs.yml, README, docs, package READMEs, PackageProjectUrl).
  • Module packages described as running on WSL Containers or Docker (they are backend-neutral).
  • Package descriptions/tags refreshed; npm package renamed to purview-containers.

Validation

  • dotnet build src/WSLTestContainers.slnx - 0 warnings, 0 errors
  • Unit tests - 100/100
  • WSLC integration tests - 27/27 (real containers)
  • just verify-consumers - 19/19
  • just pipeline-pack-validate - 20/20 packages valid
  • dotnet csharpier check . - clean

Adds Phase 0/acceptance spikes under spikes/DynamicLoadSpike and spikes/PortableConsumerSpike.

Purview.Containers.Wsl is now multi-target: net10.0 ships a portable facade and
net10.0-windows10.0.19041.0 ships the implementation (compiled against the
Microsoft.WSL.Containers projection, which is a net8.0-windows asset). A
platform-neutral net10.0 project can therefore reference the WSL backend and get
WSLC on a Windows host and Docker elsewhere through auto - no target-framework or
configuration change. This also corrects the docs, which claimed WSLC required a
.NET 11 Windows project.

- facade (WslPayload) loads the Windows build from a wslc/ payload into a
dedicated AssemblyLoadContext and delegates through IContainerBackend; it reports
wsl unavailable on non-Windows or when the payload is absent, so selection falls
through to Docker. Windows-targeting projects bind the implementation directly
and are unchanged.
- packaging: the implementation, the WSLC projection, its Windows SDK
dependencies and the native SDK are packed under payload/win-{x64,arm64}; the
buildTransitive targets copy the matching folder for a platform-neutral consumer
on Windows.
- guards: PCC0001 now accepts any .NET 10+ target (Windows 10.0.19041.0+ or
platform-neutral); PCC0002 still rejects a 32-bit Windows consumer.
- selection: a new optional IContainerBackendPreference.AutoPriority makes auto
prefer WSLC (0) over Docker (100) deterministically, instead of relying on the
MSBuild props-import order.
- spikes: DynamicLoadSpike (phase 0 feasibility) and PortableConsumerSpike
(acceptance) under spikes/.
- tests/docs: consumer contract, packaging, backends and module READMEs updated;
verify-consumers gains a portable case and updates the changed expectations.

Validated: build 0 warnings; unit 98/98; Wsl integration 27/27; verify-consumers
19/19; pipeline-pack-validate 20/20 packages valid; csharpier clean.
Match the Testcontainers SQL Server module: the generated connection string now
sets Database=master by default, configurable with WithDatabase(...). Previously
it omitted the catalog entirely, so consumers that expect Database=master (as
Testcontainers produces) had to add it themselves.
- rename every repository reference from purview-dev/wsl-containers to
  purview-dev/containers (package.json, mkdocs.yml, README, docs, the package
  READMEs and PackageProjectUrl)
- describe the service modules as running on WSL Containers or Docker (they are
  backend-neutral) and refresh the WSL, Docker and abstractions descriptions
- broaden the package tags (docker for the modules, wslc/testcontainers for the
  WSL backend, docker/wsl for the abstractions)
- rename the npm package to purview-containers and point homepage/bugs there
@kieronlanning

Copy link
Copy Markdown
Contributor Author

One-time integration run - proves the Docker path works without WSLC

The shared PR pipeline filters to [Category=Unit], so no integration suite runs there. Verified the
Docker-backed suites manually against a real daemon (Docker Desktop, server 29.8.1, linux/x86_64):

Suite Backend Result
Wsl.IntegrationTests WSL Containers 27/27
Docker.IntegrationTests Docker daemon 7/7
Modules.DockerIntegrationTests Docker daemon (PostgreSQL, Redis, SQL Server, MySQL, RabbitMQ, Azurite, NATS) 7/7

Each suite skips itself when its runtime is unreachable (SkipIfUnavailableAsync), so these are real
executions, not skips (0 skipped). The module suite asserts each service is genuinely ready via the
module'''s own strategy (pg_isready, redis-cli ping, host SqlClient/MySqlConnector connections, and the
RabbitMQ/Azurite/NATS startup log lines), and checks the generated connection strings.

This is a deliberate one-off; the pipeline filter is unchanged.

The umbrella README linked the version badge and its link to Purview.Containers.Wsl
(the Windows-only WSLC backend). Point both at Purview.Containers, the entry point
for the library.
The shared pipeline filters to [Category=Unit] because the WSLC suites need a
Windows host, and the Docker integration suite projects inherited the Windows test
TFM, so they never ran anywhere in CI.

Make Docker.IntegrationTests and Modules.DockerIntegrationTests target net10.0
(they need only a reachable daemon) and add an integration-docker job to pr.yml
that runs them on ubuntu-latest. That job is the standing proof the library and
every service module work off Windows/WSL.

The two projects also remove the SDK's automatic SharedTestingFramework reference,
which is WSLC/Windows-only and nothing in them uses.
@kieronlanning

Copy link
Copy Markdown
Contributor Author

Docker integration now runs in the PR build

Added an integration-docker job to .github/workflows/pr.yml so this is a standing CI check, not a
one-off. Docker.IntegrationTests and Modules.DockerIntegrationTests now target net10.0 (they need
only a docker daemon), so they run on the shared Linux runner; each removes the SDK auto-added
SharedTestingFramework reference, which is WSLC/Windows-only.

Latest run (36923087661) - all green:

Job / suite Runner Result
Build and test (unit) ubuntu-latest passed (PackValidation 20/20)
integration-docker - Docker.IntegrationTests ubuntu-latest, net10.0 7/7
integration-docker - Modules.DockerIntegrationTests ubuntu-latest, net10.0 7/7

The WSLC suites still run only on a WSLC host; the shared pipeline filter stays [Category=Unit].

The WSLC integration suites need a Windows host with WSL Containers, which no
hosted runner provides, so they cannot run on pull_request. Add integration-wsl.yml,
a workflow_dispatch-only workflow that runs Wsl.IntegrationTests and the seven
service-module suites one project at a time on a self-hosted Windows runner
carrying the custom wslc label, after checking 'wsl --version' and 'wslc version'
so a mis-provisioned runner fails fast.

Declare the custom label in .github/actionlint.yaml so actionlint validates the
workflow, and document the runner prerequisites in docs/wiki/Testing.md.
GenericBuilder_WithTheDockerBackendSelected_StartsAContainer ran /bin/echo selected and
then asserted ContainerState.Running. On a fast runner the echo container exits
before the state is read, so the assertion flaked (observed on a CI run, 6/7). Use a
long-lived command so the container is reliably running.
@kieronlanning

Copy link
Copy Markdown
Contributor Author

Manual WSLC workflow (self-hosted) + a flake fix

.github/workflows/integration-wsl.yml - a workflow_dispatch-only workflow (never on pull_request/push) that runs the WSLC integration suites on a self-hosted Windows runner with the custom wslc label: a host check (wsl --version, wslc version), then Wsl.IntegrationTests and the seven service-module suites one project at a time (the shared WSLC image store must not be contended).

Trigger: Actions -> Integration (WSL Containers) -> Run workflow, or gh workflow run "Integration (WSL Containers)". Inputs: optional ref and TUnit filter. Runner prerequisites are documented in docs/wiki/Testing.md; the custom label is declared in .github/actionlint.yaml so actionlint validates the workflow. (A workflow_dispatch workflow only appears in the Actions list once it is on the default branch, so it becomes runnable after this merges.)

Flake fix. The previous run failed Integration (Docker): GenericBuilder_WithTheDockerBackendSelected_StartsAContainer ran /bin/echo selected (exits instantly) then asserted State == Running - a race that passed locally and on the first CI run but flaked on a faster runner. Now uses a long-lived command. Re-run 36927528406 is green: Docker 7/7, modules 7/7, Build and test success.

The same test project should run on WSLC on a Windows machine and Docker everywhere
else without a backend choice in code or environment. That was only described in
pieces across Getting-Started, Backends and Modules. Add docs/wiki/Using-in-Your-Tests.md
with the copy-paste project file (net10.0, both backends, Redis and PostgreSQL), the
test, and a per-host table; link it from the sidebar, index and Getting Started.

Add samples/getting-started/AutoSample: a net10.0 project referencing both backends
and two modules that lets automatic selection choose (it assembles the wslc/ payload
by hand, as a package consumer gets it from Purview.Containers.Wsl buildTransitive).
Wire it into the solution, add `just sample-auto`, and update the samples README.

Guard the documented shape with verify-consumers case 20 (net10.0 + two modules +
both backends); the RequireText check now accepts multiple patterns so it asserts
both registrations are generated. Update the Consumer-Requirements count to twenty.
@kieronlanning

Copy link
Copy Markdown
Contributor Author

Zero-config "auto" story: docs, sample and a guarded case

  • docs/wiki/Using-in-Your-Tests.md - the copy-paste shape: a net10.0 project referencing Redis + PostgreSQL and both backends, the test with no backend code, and a per-host table (Windows -> WSLC, everywhere else -> Docker). Linked from the sidebar, index and Getting Started.
  • samples/getting-started/AutoSample - a runnable net10.0 project that references both backends and two modules and lets auto choose (it assembles the wslc/ payload by hand, since it uses project references). Wired into the solution and just sample-auto.
  • verify-consumers case 20 - net10.0 + Redis + PostgreSQL + both backends builds and generates both registrations (the RequireText check now accepts multiple patterns). 20/20.

Verified end to end on this Windows host: AutoSample reported wsl usable=True/docker usable=True, selected wsl, and both services came up (redis-cli ping -> PONG, pg_isready -> accepting connections). On the CI runner the same solution builds (AutoSample included) and the Docker suites pass.

Latest run 36930173576: success.

…umbrella

BREAKING CHANGE: the abstractions package is renamed to Purview.Containers.Core. A
package reference to Purview.Containers now means the new umbrella bundle. Consumer
source is unchanged (the namespace stays Purview.Containers).

Purview.Containers is the dependency root (IContainerBackend lives there) so it cannot
reference the backends; a TUnit-style split keeps that direction and puts the bundle at
the name consumers reach for:

- src/src/Containers -> src/src/Core (Core.csproj; SDK derives PackageId and AssemblyName
  Purview.Containers.Core and RootNamespace Purview.Containers, so no consumer using changes)
- buildTransitive assets move to Purview.Containers.Core.{props,targets} so NuGet still
  auto-imports them
- new src/src/Containers/Containers.csproj: umbrella referencing Core, Wsl and Docker;
  no code and no buildTransitive of its own (the dependencies flow transitively)

One reference now gives the whole auto setup: a net10.0 project with Purview.Containers
and the modules runs on WSLC on Windows and Docker elsewhere, no registration and no
environment variable.

Docs (Architecture, Consumer-Requirements, Home, Packaging, Modules, Getting-Started,
Using-in-Your-Tests, README, AGENTS, package READMEs) describe the split and present the
umbrella as the zero-config default, with Core plus a single backend as the lean route for
Linux-only CI. purview-build.json gains purview.containers.core and trims
purview.containers to the umbrella. verify-consumers case 13 targets Core and a new case
21 references only the umbrella. AutoSample now references the umbrella project.
@kieronlanning

Copy link
Copy Markdown
Contributor Author

Purview.Containers is now the umbrella; abstractions moved to Purview.Containers.Core

TUnit-style split, per the design decision:

Package Role
Purview.Containers.Core the backend-neutral abstractions (was Purview.Containers)
Purview.Containers new umbrella: depends on Core + Wsl + Docker; no code of its own
Purview.Containers.Wsl / .Docker / the modules unchanged ids

A reference to Purview.Containers used to mean the abstractions; it now means the bundle. Consumer source is unchanged — the namespace stays Purview.Containers (the SDK strips the .Core segment), so only the package id moves. Breaking for prerelease consumers, flagged as BREAKING CHANGE.

The registry stays the dependency root (IContainerBackend lives in Core), so Core is the thing everything depends on and the umbrella sits on top — no cycle.

Why it is nice: one reference gives the whole auto setup. Purview.Containers + a module on a net10.0 project runs WSLC on Windows and Docker elsewhere, no registration and no env var (the backends buildTransitive assets flow transitively through the umbrella).

Validation:

  • build 0 warnings; unit 100/100
  • just verify-consumers 21/21 (new case 21 references only the umbrella and asserts both registrations are generated; case 13 retargeted to Purview.Containers.Core)
  • just pipeline-pack-validate ✓ 22/22 packages valid
  • AutoSample now references the umbrella project and still picks WSLC (wsl usable / docker usable -> selected wsl, PONG + accepting connections)
  • CI run 36933403528: success (Build and test + Integration (Docker))

@kieronlanning
kieronlanning merged commit a363419 into main Oct 1, 2026
2 checks passed
@kieronlanning
kieronlanning deleted the refactor-into-multitarget branch October 1, 2026 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant