This repository contains Purview.Containers, a Testcontainers-style library for .NET that runs throwaway
Linux containers for integration testing on Microsoft WSL Containers (WSLC) — the Windows runtime with
no Docker installation — or on Docker through Testcontainers, plus the service modules (PostgreSql,
Redis, MsSql, RabbitMq, Azurite, Nats, MySql, AzureKeyVaultEmulator).
docs/wiki/Backends.md is the consumer guide to choosing and configuring a backend; keep it up to date
whenever a target framework, a backend package or the buildTransitive assets change.
- This file is the repository-wide source of truth for AI agents. A more-specific
AGENTS.mdin a subtree, if one is ever added, takes precedence for that subtree. .github/copilot-instructions.mddefers to this file.- Follow explicit user instructions first, then the nearest applicable repository instructions, then established code patterns.
- Operate only in this repository unless the user explicitly expands the scope.
- Never read, copy, log, or commit secrets from excluded files, environment variables, user profiles, local configuration, test output, or registry credentials.
| Path | Purpose |
|---|---|
src/Containers.slnx |
Canonical solution for restore, build, test and pack |
src/src/Containers |
Umbrella package (Purview.Containers, net10.0): references Core and both backends; no code of its own |
src/src/Core |
Backend-neutral abstractions (Purview.Containers.Core, net10.0, namespace Purview.Containers): IContainer/ContainerConfiguration, ContainerBuilder, ContainerBase, IContainerBackend/ContainerBackends, wait strategies, images, networking, mounts, diagnostics |
src/src/Wsl |
WSL Containers backend (Purview.Containers.Wsl, multi-target net10.0 facade + net10.0-windows10.0.19041.0 implementation): WslContainerBackend, the shared session runtime, WslContainer, WslContainerSession, and the portable facade (WslPayload, *.Facade.cs) that loads the implementation at run time |
src/src/Docker |
Docker backend (Purview.Containers.Docker, net10.0): DockerContainerBackend + DockerContainer over Testcontainers |
src/src/<Module> |
Service modules (Purview.Containers.<Module>, net10.0); each is a thin layer over the abstractions and carries a bespoke Sdk/README.md |
src/src/<Project>/Sdk |
Package-only assets. Sdk/README.md is packed as the package README (suppressing the repo-root README); Sdk/buildTransitive/** ships MSBuild assets to consumers (the abstractions' backend registration, each backend package's own registration entry, and the WSL backend's consumer defaults and guards) |
src/tests |
TUnit unit (*.UnitTests) and container integration (*.IntegrationTests) projects: the WSLC suites need a WSLC host, Docker.IntegrationTests and Modules.DockerIntegrationTests need a Docker daemon |
docs/wiki |
User-facing documentation wiki, aggregated by the purview-dev website. Backends.md is the consumer guide to choosing and configuring WSLC or Docker |
samples/getting-started |
Runnable consumer-shaped samples (WSLC and Docker) built with the solution; just sample-wsl / just sample-docker |
mkdocs.yml |
Wiki site configuration (docs_dir: docs/wiki) |
src/Directory.Build.props / src/Directory.Build.targets |
Solution-wide SDK import, package metadata and the shared package icon |
purview-build.json |
Shared Purview.Build pipeline configuration: solution, test discovery/filter and the exhaustive pack-validation manifest |
Justfile |
Supported local workflow commands |
skills |
The agent skills this repository authors and publishes |
.agents/skills |
Skills installed from other repositories by the skills CLI (gitignored; skills-lock.json is the tracked record) |
- Read this file, inspect the working tree, and locate the implementation, tests, documentation and existing patterns relevant to the task.
- Confirm behaviour from code and tests rather than relying on memory or documentation alone.
- Make the smallest coherent change. Preserve public behaviour unless the task explicitly changes it.
- Update tests for fixes and behaviour changes. Update documentation when public behaviour changes.
- Run the narrowest meaningful validation first, then broader validation in proportion to risk.
- Review the diff for unrelated edits, generated noise, compatibility risks, and missing docs or tests.
- One shared, process-wide session (
WslContainerRuntime.Instance), lazily started, namedwslc-{pid}-{random8}. Container isolation comes from unique names and unique host ports, not from extra sessions. - Images are shared by default (
StorageMode.Shared→%LOCALAPPDATA%\Purview\WslContainers\images). A session exclusively locks itsstorage.vhdx; the runtime verifies the store once and falls back to an isolated per-process store on a sharing violation (0x80070020). Do not remove that fallback. - All session-mutating operations are serialised through the runtime's gate (concurrent
Startcan fail with0x8000FFFF). Keep new lifecycle paths inside it. - Random host ports are native (
windowsPort=0) and read back from the container's mapped ports — never probe for a free port. - Default networking is
Bridged; only IPv4 loopback is mapped, and UDP is unsupported (ContainerNotSupportedException). - One backend registry per process (
ContainerBackends): backend packages register themselves through the generated module initializer,ResolveAsync()returns the pinned backend, thePURVIEW_CONTAINERS_BACKEND-selected backend, or the first usable registered backend in auto-priority order (IContainerBackendPreference.AutoPriority, lowest first;wslis 0 anddockeris 100, so WSLC is preferred when both are usable), and an empty/unusable registry throwsContainerBackendUnavailableExceptionnaming the fix. A named or pinned backend never silently falls back. A module must never reference a backend package — it resolves through the registry. DisposeAsyncis idempotent and never terminates the shared session; a process-exit hook disposes the session so its name is released.- Secrets use
Secretand must never reach logs, names,ToString()or diagnostics (they are redacted).
- The packages split by framework:
Purview.Containers.Core(abstractions) and the service modules targetnet10.0on any platform.Purview.Containers.Wslis multi-target:net10.0(a portable facade) andnet10.0-windows10.0.19041.0(the WSLC implementation).src/src/Directory.Build.propssets thenet10.0subtree default andWsl.csprojadds the Windows build; the WSLC tests keep the Windows target because they drive WSLC and therefore bind the implementation, while the Docker integration projects override tonet10.0so they run on Linux. The contract, the failures that enforce it and the CI workarounds are documented in Consumer Requirements; update that page whenever a target framework, thebuildTransitivedefaults, the payload layout or thePCC0001/PCC0002guards change. Purview.Containers.WslshipsSdk/buildTransitive/Purview.Containers.Wsl.{props,targets}so consumers inheritWindowsSdkPackageVersion/PlatformTargetdefaults, a clear error for an unsupported target framework (PCC0001) or a 32-bit Windows consumer (PCC0002), and — for a platform-neutral consumer on a Windows build host — a copy of the WSLC implementation payload (wslc/) that thenet10.0facade loads at run time. Those assets are framework-agnostic, so NuGet never raisesNU1202at restore time; the guards are the fail-fast path. Keep them, and keep theRequiredContententries (includingpayload/**) that declare them. They belong to the WSL backend package only.Purview.Containers.CoreshipsSdk/buildTransitive/Purview.Containers.Core.{props,targets}, which turn thePurviewContainersBackendslist (each backend package appends its own entry) into a generated module initializer in the consuming assembly. That is how a package consumer's process discovers its backend without reflection or assembly scanning. A project-reference consumer (this repository's own tests) registers explicitly instead —WslcTest.EnsureBackendRegistered().src/Directory.Build.propssetsEnableWindowsTargeting=truebecause the shared CI agent isubuntu-latest; removing it fails the pipeline withNETSDK1100.- The project is experimental: keep the experimental notice in
README.md,docs/wiki/Home.mdand the package READMEs, and keep versions on a-prerelease.Nsuffix.
- A module supplies only defaults: image, ports, environment, module configuration (
WithXxx), a readiness strategy, and connection-string/endpoint accessors. It must not duplicate runtime infrastructure. - New modules follow the shape in Contributing Modules: a
ContainerConfiguration-derived record, aContainerBuilder<TBuilder, TContainer, TConfiguration>subclass, and a container deriving fromContainerBasethat exposesGetConnectionString()/endpoints built fromGetMappedPublicPort. Take anIContainerBackendin the builder and container constructors and pass it through asnew MyContainer(configuration, Backend)— anullbackend is resolved on start viaContainerBackends.ResolveAsync(); a module package must never referencePurview.Containers.Wsl(that is what keeps it runnable on any backend and restorable on Linux). - Prefer verifying the service for readiness (exec a readiness command or open a host client connection) over a bare TCP check. Where an image reports readiness too early (MySQL), a log match is wrong.
- SQL Server requires an explicit
AcceptLicense()call; never accept licensing terms on the caller's behalf.
- Every project under
src/srcsets<IsPackable>true</IsPackable>literally in the.csproj: the SDK resolvesIsPackableby scanning the project file, and the package metadata/icon conditions insrc/Directory.Build.propsdepend on it. - Each package ships
lib/$(TFM)/<Assembly>.{dll,xml},README.md(fromSdk/README.md) andpurview-logo-light.png. PDBs ship only in the.snupkg. MSBuild assets are per package and must be named after their own package id for NuGet to import them:Purview.Containers.CoreshipsbuildTransitive/Purview.Containers.Core.{props,targets}(backend registration),Purview.Containers.WslshipsbuildTransitive/Purview.Containers.Wsl.{props,targets}(consumer defaults and guards) andPurview.Containers.DockershipsbuildTransitive/Purview.Containers.Docker.props(its registration entry). The umbrellaPurview.Containersships none of its own;Core's and the backends' assets reach its consumers transitively. See Consumer requirements and compatibility. PackValidation.RequireExplicitContentdefaults totrue, sopurview-build.json'sRequiredContentis the exhaustive manifest: a produced package with no rule, or a packed entry matched by no glob, fails validation. Adding or removing packaged content means updating that map.- Validate with
just packandjust pipeline-pack-validate.
- Test assemblies are categorised by the SDK:
*.UnitTests→Unit,*.IntegrationTests→Integration. purview-build.jsonfilters the shared pipeline to[Category=Unit], so it never needs a WSLC host. The Docker integration suites (Docker.IntegrationTests,Modules.DockerIntegrationTests) are the one exception: they targetnet10.0and run in the.github/workflows/pr.ymlintegration-dockerjob onubuntu-latest, which is the standing proof that the library and modules work off Windows/WSL. Keep them portable; they remove the SDK's automaticSharedTestingFrameworkreference because that helper is WSLC/Windows-only. The WSLC suites still never run in the shared pipeline.- WSLC integration tests run serially (
--max-parallel-test-modules 1in theJustfile): a session exclusively locks its image-store VHD, so parallel modules fail with0x80070020. - Use the shared
WslcTesthelper insrc/tests/SharedTestingFrameworkfor WSLC integration skip/availability checks; the Docker suites use their ownDockerTesthelper. Unit tests may use the modules'BuildConfigurationForTesting()internal hook. Wsl.UnitTests/ConsumerRequirementsTests.csguards the published target framework of the Windows build (.NETCoreApp,Version=v10.0plusWindows10.0.19041.0), which the test project binds; keep it in step with Consumer Requirements.just verify-consumerspacks and then builds throwaway consumer projects to assert the documented guard errors and workarounds. It restores from nuget.org, so it stays out of the[Category=Unit]filter and is a local/explicit step.
just build # dotnet build (Debug)
just test '/*/*/*/*[Category=Unit]' # unit tests only
just lint-check / just lint-fix # CSharpier check / format
just pack # build + dotnet pack into ./artifacts
just verify-consumers # build throwaway consumers against the packed packages
just pipeline-pack-validate # restore, build, lint, test, pack, validate
just scrub # reset bin/obj, clean, forced restore, build-server shutdown
Commit messages follow Conventional Commits enforced by the commit-msg lefthook
(.config/lefthook.yml → npx commitlint); allowed types are in commitlint.config.mts.
package.jsonis the authoritative release and package version. Do not diverge project versions by hand..github/workflows/pr.ymlbuilds and tests pull requests through the sharedpurview-dev/build/.github/workflows/purview-build.yml, with pack and validation enabled..github/workflows/release.ymlruns the shared release pipeline withrelease-mode: NuGeton a push tomain..github/workflows/integration-wsl.ymlis a manual (workflow_dispatch) workflow that runs the WSLC integration suites on a self-hosted Windows runner with the customwslclabel (documented in Testing);.github/actionlint.yamldeclares that label. It is never attached topull_requestorpush.- The workflows pin
dotnet-versiontoglobal.json'ssdk.version; keep them in sync, and keeppurview-build.jsonpointing atsrc/Containers.slnx. The shared workflow runs onubuntu-latest, which is whyEnableWindowsTargeting=truemust stay insrc/Directory.Build.props.
Before handing work back:
- Confirm the requested behaviour and scope are satisfied, and only intended files changed.
- Review public API, package-content and dependency-direction implications.
- Update the affected package
Sdk/README.md, the rootREADME.md,docs/wiki(plus_Sidebar.mdandmkdocs.ymlfor new pages),AGENTS.mdandpurview-build.jsonwhen the change affects them.docs/wiki/Consumer-Requirements.mdis the contract every consumer-facing change has to keep accurate, andjust verify-consumersis the check that proves it. - Watch for the packaging traps: a new packable project missing
<IsPackable>true</IsPackable>or aRequiredContententry, and a newSdk/README.mdthat does not describe its own package. - Run the appropriate build, test, formatting and pack checks in proportion to risk.
- State exactly what validation ran. If a check was skipped or blocked, give the concrete reason and the remaining risk.
Authored skills live in the repository-root skills/ folder. Nothing packs them into a NuGet
package - Purview.BuildSdk 1.0.6 removed skill packaging and the build-time .agents/ mirror
outright. The skills CLI clones the git repository and only discovers a root skills/ folder, so
that is the one delivery route.
.agents/skills/ is the CLI's install location for skills from other repositories, and is
gitignored. This repository's own skills are never installed into it - that would leave two copies to
drift apart. Read them at their authored path, skills/<name>/SKILL.md.
npx skills add purview-dev/containers| Skill | Answers |
|---|---|
skills/purview-containers-quickstart/ |
which package, how a backend is chosen, a first container, a typed module, connection strings |
skills/purview-containers-lifecycle/ |
start/stop/dispose semantics, cleanup guarantees, wait strategies, WSLC networking |
skills/purview-containers-testing/ |
the test tiers, why modules run serially, pinning a backend, the CI shape |
skills/purview-containers-consumer-requirements/ |
the target-framework contract, PCC0001/PCC0002 and the other consumer build errors |
Every skill carries the purview-containers prefix. Skills install into one flat directory per
agent, shared with every skill a developer has installed from anywhere, so an unprefixed name
collides and the loser is whichever installed second.
src/tests/Repository.UnitTests/SkillTests.cs validates them on every test run through
Purview.Skills.Testing. The [InheritsTests] attribute there is load-bearing - TUnit does not walk
base types during discovery, so without it the run is green and nothing is checked.
A relative link from one SKILL.md into another skill's folder is a rule violation: only a skill's
own folder is installed with it. Name the other skill in prose instead.