A Testcontainers-style library for .NET that runs throwaway Linux containers for integration testing — on Microsoft WSL Containers (WSLC) with no Docker installation, or on Docker through Testcontainers.
Two backends, one API — one reference. Containers are created through the backend-neutral
Purview.Containers.Coreabstractions, so the same test suite runs on WSL Containers (Purview.Containers.Wsl) or Docker (Purview.Containers.Docker, driven by Testcontainers). Add the umbrella packagePurview.Containersto a plainnet10.0project — no Windows target framework needed — and it brings the abstractions plus both backends, gets WSLC on a Windows developer machine and Docker on a Linux CI runner without changing a line of test code or configuration, and can be pinned withPURVIEW_CONTAINERS_BACKEND. ThePurview.Containers.Wslpackage is multi-target: itsnet10.0facade loads the WSLC implementation at run time on Windows and reportswslas unavailable everywhere else.Using it in your tests (auto) — the copy-paste zero-config shape.
Backends: WSLC or Docker — comparison, side-by-side project setup, CI example and troubleshooting.
The WSLC backend is built directly against the Microsoft.WSL.Containers NuGet package (the WSLC managed C# API) — no wslc.exe/wsl.exe/docker CLI. The Docker backend is built on Testcontainers for .NET.
Experimental. This project is an experiment in running throwaway containers through Microsoft WSL Containers. The public API, defaults and packaging rules can change between prereleases, and there is no production support guarantee. Pin the exact package version you build against, and read Consumer Requirements before adopting it.
Status: preview. Backend-neutral abstractions, two backends (WSL Containers and Docker), wait strategies, the
Image/Tagparser, registry auth, observability, hardening, and seven service modules (PostgreSQL, Redis, SQL Server, RabbitMQ, Azurite, NATS, MySQL). On WSLC, images are shared by default (StorageMode.Shared): sessions reuse a stable image store (%LOCALAPPDATA%\Purview\WslContainers\images) so images are pulled once, not per session;StorageMode.PerSessionprovides isolation. Runnable samples live insamples/getting-started.
Pick a backend — see Backends: WSLC or Docker for the comparison.
WSL Containers backend
- Windows 10/11 with WSL Containers, installed via
wsl --install --no-distribution(verified against WSL 3.0.1.0). - A consuming project that is .NET 10 or later. A platform-neutral
net10.0project binds the portable facade and gets automatic WSLC-or-Docker selection; a Windows target framework (net10.0-windows10.0.19041.0, x64 or arm64) binds the implementation directly. ThePurview.Containers.Wslpackage ships MSBuild defaults for the supporting settings.
Docker backend
- Any reachable Docker daemon, and a
net10.0(or later) project on any platform.
.NET SDK 11 is required to build this repository (it pins 11.0.100-rc.1); SDK 10 is enough for a Docker
consumer.
Verify:
wsl --version # WSL Containers installed (verified against 3.0.1.0)
wslc version # e.g. 3.0.1.0docker info # a Docker daemon the tests can reachThe library reports missing prerequisites via WslContainerRuntime.GetInfoAsync() (WSLC) or
DockerContainerBackend.GetInfoAsync() (Docker); it never installs a runtime on its own.
The full consumer contract, the exact errors raised when it is not met, and the EnableWindowsTargeting
workaround for non-Windows CI agents are documented in
Consumer Requirements.
The generic container API below is implemented and working — and the identical code runs on either backend (see Backends: WSLC or Docker):
await using var container = new ContainerBuilder()
.WithImage("docker.io/library/redis:latest")
.WithPortBinding(6379, assignRandomHostPort: true)
.WithWaitStrategy(Wait.ForTcpPort(6379))
.Build();
await container.StartAsync();
int port = container.GetMappedPublicPort(6379);PostgreSQL:
await using var postgres = new PostgreSqlBuilder()
.WithDatabase("integration")
.WithUsername("postgres")
.WithPassword("postgres")
.Build();
await postgres.StartAsync();
await using var connection = new NpgsqlConnection(postgres.GetConnectionString());
await connection.OpenAsync();SQL Server / Redis / RabbitMQ:
await using var sqlServer = new MsSqlBuilder().WithPassword("SomeStrong!Password1").AcceptLicense().Build();
await sqlServer.StartAsync();
string sqlConn = sqlServer.GetConnectionString();
await using var redis = new RedisBuilder().Build();
await redis.StartAsync();
string redisConn = redis.GetConnectionString();
await using var rabbitMq = new RabbitMqBuilder().WithUsername("guest").WithPassword("guest").Build();
await rabbitMq.StartAsync();
string amqp = rabbitMq.GetConnectionString();These are the target APIs. The generic container is implemented; the module builders arrive in Phases 3–6. See the docs for the designed surface.
| Decision | Evidence |
|---|---|
| One shared process-wide session, shared storage path | image store is keyed by storage path; session start ~20 ms (S2); concurrent sessions can't share the VHD (S18) → auto-fallback to isolated store |
Unique session names wslc-{pid}-{rand} |
session names are machine-reserved (S1, S13) |
Default NetworkingMode = Bridged |
port mappings require Bridged; default is none (S4) |
Random host ports via native windowsPort=0 |
race-free random assignment (S4) |
| Serialize session/container lifecycle ops | concurrent Start can race (0x8000FFFF) (S8) |
| No Ryuk-style reaper needed | Session.Dispose() frees the session; orphans block only their own name (S10, S14) |
| No container-name DNS | containers reach each other by IP only (S9) |
Image/WithTag fail fast |
invalid images/tags rejected at configuration time, never at pull time |
src/
Wsl/ core runtime + WSL Containers backend (package Purview.Containers.Wsl)
PostgreSql/ PostgreSQL module (builder, container, Npgsql connection string)
Redis/ Redis module (builder, container, StackExchange.Redis connection string)
MsSql/ SQL Server module (builder, container, SqlClient connection string)
RabbitMq/ RabbitMQ module (builder, container, AMQP + management endpoints)
Azurite/ Azurite module (builder, container, blob/queue/table endpoints)
Nats/ NATS module (builder, container, client + monitoring endpoints)
MySql/ MySQL module (builder, container, MySqlConnector connection string)
tests/
SharedTestingFramework/ shared WSLC skip/helper for integration tests
Wsl.UnitTests/
Wsl.IntegrationTests/
PostgreSql.UnitTests/
PostgreSql.IntegrationTests/
Redis.UnitTests/
Redis.IntegrationTests/
MsSql.UnitTests/
MsSql.IntegrationTests/
RabbitMq.UnitTests/
RabbitMq.IntegrationTests/
Azurite.UnitTests/
Azurite.IntegrationTests/
spikes/
WslcSpikes/ Phase 0 investigation harness (run: see below)
docs/
wiki/ project wiki (mkdocs.yml -> docs_dir: docs/wiki)
index.md Home.md _Sidebar.md Getting-Started.md Testing.md
Architecture.md Lifecycle.md Networking.md Wait-Strategies.md Modules.md
Packaging.md Release-Flow.md Contributing.md Contributing-Modules.md
Wslc-Api-Investigation.md Wslc-Capability-Matrix.md
The project documentation lives in docs/wiki and is published as a MkDocs site
(mkdocs.yml, docs_dir: docs/wiki, aggregated by the purview-dev website):
- Getting Started — prerequisites, first container, first module.
- Backends: WSLC or Docker — how to choose, side-by-side project setup, CI example, troubleshooting.
- Consumer Requirements — the target-framework contract, the
PCC0001/PCC0002guards, and the CI workarounds. - Architecture — the shared session model, concurrency and cleanup decisions.
- Lifecycle, Networking, Wait Strategies.
- Modules — the module contract and every shipped module.
- Testing — unit vs integration categories and the serial WSLC test rule.
- Packaging and Release Flow — what ships and how it is released.
- Contributing and Contributing Modules.
Every package also ships its own README.md (from src/src/<Project>/Sdk/README.md), so
dotnet add package Purview.Containers.<Module> brings documentation specific to that package.
The PostgreSQL, Redis, SQL Server and RabbitMQ modules work today:
await using var postgres = new PostgreSqlBuilder()
.WithDatabase("tests")
.WithUsername("postgres")
.WithPassword("postgres")
.Build();
await postgres.StartAsync(); // waits for pg_isready
await using var connection = new NpgsqlConnection(postgres.GetConnectionString());
await connection.OpenAsync();await using var redis = new RedisBuilder().Build();
await redis.StartAsync(); // waits for redis-cli ping
using var redisConnection = await ConnectionMultiplexer.ConnectAsync(redis.GetConnectionString());
await redisConnection.GetDatabase().ExecuteAsync("PING");await using var sqlServer = new MsSqlBuilder()
.WithPassword("SomeStrong!Password1")
.AcceptLicense() // explicit EULA acceptance required
.Build();
await sqlServer.StartAsync(); // waits for a real SQL Server connection
await using var sqlConnection = new SqlConnection(sqlServer.GetConnectionString());
await sqlConnection.OpenAsync();await using var rabbitMq = new RabbitMqBuilder()
.WithUsername("guest")
.WithPassword("guest")
.Build();
await rabbitMq.StartAsync(); // waits for 'Server startup complete'
var factory = new ConnectionFactory { Uri = rabbitMq.GetAmqpEndpoint() };
using var amqp = await factory.CreateConnectionAsync();await using var azurite = new AzuriteBuilder().Build();
await azurite.StartAsync(); // waits for the blob/queue/table listeners
// Supply the Azurite devstoreaccount1 key in AzuriteAccount.Key for authenticated operations.
string connectionString = azurite.GetConnectionString();
Uri blob = azurite.GetBlobEndpoint();Every test process owns a single shared WSLC session and, by default, uses the shared image store
(%LOCALAPPDATA%\Purview\WslContainers\images). A WSLC session exclusively locks its
storage.vhdx, and the lock is taken lazily on the first store access — so running test assemblies
in parallel makes the losers fail with 0x80070020:
The process cannot access the file because it is being used by another process.
The runtime now verifies the store on first use and transparently falls back to an isolated per-process store (removed when that process's session terminates), but running test modules serially keeps the warm shared image cache (no per-process re-pull) and is the fastest option:
just test # dotnet test, one test module at a time
# Visual Studio: Test > Options > untick "Run Tests in Parallel" (or set
# "Maximum Parallel Test Projects" to 1) before running the WSLC integration tests.To watch a container start end to end, run a sample — the same code, on either backend:
just sample-wsl # needs WSL Containers
just sample-docker # needs a Docker daemonThe
Wsl.IntegrationTestsmodule runs 27 real containers in one session and takes ~4 minutes because WSLC serialises container operations; expect slow-test warnings while it runs.
just verify-consumers # pack, then build 16 throwaway consumer projects against ./artifacts
just verify-consumers -Keep # same, keeping the generated projects for inspectionjust verify-consumers asserts every claim in Consumer Requirements:
the happy path, the shipped buildTransitive defaults, the PCC0001/PCC0002 guards, and the
non-.NET-11 escape hatch that deliberately does not work. It needs network access and is therefore a
local step rather than part of the [Category=Unit] pipeline filter.
dotnet build spikes/WslcSpikes/WslcSpikes.csproj
dotnet run --project spikes/WslcSpikes -- sfull
# or s1..s14 for individual behaviour probesMIT. This project is not affiliated with, or endorsed by, the Testcontainers project or Microsoft.