braid 0.2.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package braid --version 0.2.0
                    
NuGet\Install-Package braid -Version 0.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="braid" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="braid" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="braid" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add braid --version 0.2.0
                    
#r "nuget: braid, 0.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package braid@0.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=braid&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=braid&version=0.2.0
                    
Install as a Cake Tool

braid

braid is a deterministic concurrency testing library for .NET using explicit async probe points.

braid helps small async concurrency tests make interleavings reproducible. Tests fork logical workers, workers stop at named probes, and braid controls which worker is released next.

Install from NuGet

braid targets .NET 10 and is published on NuGet as 0.2.0.

dotnet add package braid --version 0.2.0

For a one-off consumer check, see Consumer smoke test in the release process doc.

What braid is

  • Explicit-probe-based concurrency testing for ordinary .NET code.
  • Deterministic seed mode for replaying random scheduling choices.
  • Typed replay schedules with BraidSchedule and BraidStep.
  • Failure reports with seed, iteration, schedule, and trace details.

What braid is not

  • Not a TaskScheduler replacement.
  • Not binary rewriting or automatic await interception.
  • Not a Coyote replacement.
  • Not distributed-system testing.
  • Not exhaustive model checking yet.

Quick start (xUnit)

After adding the package, a minimal test can fork one worker, hit a probe under a typed replay schedule, join, and assert the worker finished:

using Braid;
using Xunit;

public sealed class BraidQuickStartTests
{
    [Fact]
    public async Task Fork_probe_join_completes_under_replay()
    {
        var workerCompleted = false;
        var options = new BraidOptions
        {
            Iterations = 1,
            Schedule = BraidSchedule.Replay(new BraidStep("worker-1", "ready")),
        };

        await Braid.RunAsync(
            async context =>
            {
                context.Fork(async () =>
                {
                    await BraidProbe.HitAsync("ready");
                    workerCompleted = true;
                });

                await context.JoinAsync();
            },
            options);

        Assert.True(workerCompleted);
    }
}

For a larger pattern (unsafe vs fixed limiter, failure assertions), see the user operation limiter example and example walkthrough.

Minimal example

using Braid;

await Braid.RunAsync(async context =>
{
    context.Fork(async () =>
    {
        await BraidProbe.HitAsync("ready", cancellationToken);
        // Exercise concurrent code here.
    });

    context.Fork(async () =>
    {
        await BraidProbe.HitAsync("ready", cancellationToken);
        // Exercise concurrent code here.
    });

    await context.JoinAsync(cancellationToken);
}, cancellationToken: cancellationToken);

Outside a braid run, BraidProbe.HitAsync completes immediately. Inside a run, it is an explicit scheduling point.

Run lifecycle

  • Braid.RunAsync always awaits JoinAsync after your callback's task completes, so an explicit JoinAsync at the end of the callback is optional.
  • BraidContext is only valid during the active Braid.RunAsync callback/run; using it after the run completes fails clearly.
  • A canceled CancellationToken passed to Braid.RunAsync is honored before the callback runs (and before options validation).
  • An empty callback with no forks completes when no replay schedule is configured; if a non-empty schedule is provided, every step must be consumed or the run fails with unused-step reporting.
  • An explicitly empty replay schedule (BraidSchedule.Replay() with no steps) is allowed for empty or probe-free runs.
  • Replay matching is ordinal and case-sensitive for both worker ids and probe names.
  • Nested Braid.RunAsync calls are not supported in v0; starting a second run while a scheduler scope is active throws InvalidOperationException.
  • Only one logical probe wait may be in flight per forked worker. Concurrent HitAsync calls on the same worker fail with a clear BraidRunException when the scheduler detects them; serialized child-task probes after the parent probe completes are allowed when the current runtime accepts that pattern. Overlapping parent/child probes are rejected.
  • Fork delegates must return a non-null Task; null is treated as an invalid callback result. Probe names may contain diagnostic punctuation, but cannot be null, empty, or whitespace.
  • Callback faults and scheduler-detected failures are surfaced as BraidRunException (or cancellation) as described below; mutating caller arrays after a failure does not change captured report data.
  • Reusing one BraidOptions instance (including replay schedules) across independent runs is supported.
  • Failure reports are scoped to the current run and iteration only.

Reproducing failures

When a run fails, braid wraps many failures in BraidRunException and reports:

  • Seed: the seed used by the failing iteration (use the same seed to reproduce random scheduling for that iteration).
  • Iteration: the zero-based failing iteration index.
  • Schedule: the typed replay schedule when one was configured (empty when only random scheduling was used).
  • Trace: the recorded worker/probe/release trace.
  • InnerException: the original fault when braid wraps an underlying exception; ToString() on BraidRunException appends inner details when present.

Use the same seed to reproduce random scheduling behavior. Once a race is understood, prefer a typed replay schedule for stable regression tests instead of relying on random exploration.

var options = new BraidOptions
{
    Seed = 12345,
    Iterations = 1,
    Schedule = BraidSchedule.Replay(
        new BraidStep("worker-1", "after-read"),
        new BraidStep("worker-2", "after-read"),
        new BraidStep("worker-1", "before-write"),
        new BraidStep("worker-2", "before-write")),
};

await Braid.RunAsync(async context =>
{
    context.Fork(async () =>
    {
        await BraidProbe.HitAsync("after-read", cancellationToken);
        await BraidProbe.HitAsync("before-write", cancellationToken);
    });

    context.Fork(async () =>
    {
        await BraidProbe.HitAsync("after-read", cancellationToken);
        await BraidProbe.HitAsync("before-write", cancellationToken);
    });

    await context.JoinAsync(cancellationToken);
}, options, cancellationToken);

Example report:

braid run failed.
Seed: 12345
Iteration: 0
Schedule:
  1. worker-1 @ after-read
  2. worker-2 @ after-read
Trace:
  1. worker-1 forked
  2. worker-2 forked
  3. worker-1 hit after-read
  4. worker-1 released at after-read

True interleaving replay (arrive/hold/release)

BraidStep(worker, probe) (or BraidStep.Hit) keeps the original behavior: release a worker when it is blocked at that probe.

For stricter interleaving assertions, replay also supports a two-phase probe flow:

  • BraidStep.Arrive(worker, probe) waits until the worker reaches the probe and keeps it blocked.
  • BraidStep.Release(worker, probe) releases a worker that was previously held by Arrive.

This lets you express "worker-1 is already blocked at probe A, then worker-2 mutates, then worker-1 resumes" without adding extra probes:

var options = new BraidOptions
{
    Iterations = 1,
    Schedule = BraidSchedule.Replay(
        BraidStep.Arrive("worker-1", "cache-hit"),
        BraidStep.Hit("worker-2", "mutation-done"),
        BraidStep.Release("worker-1", "cache-hit")),
};

Semantic difference:

  • release worker at probe: worker is released as soon as that schedule step is matched.
  • wait until arrived and hold: worker arrival is asserted first, but execution stays blocked.
  • true interleaving test: a competing step runs while the first worker is provably held.

When to use

  • Cache and library concurrency tests.
  • Race reproduction after a flaky failure is understood.
  • CAS, TTL, and state-machine style code.
  • Small deterministic async scenarios with clear probe points.

Real-world example: per-user operation limiter

A per-user operation limiter is supposed to allow at most one active operation for a configured user. The example limiter stores one userId and one limit, but still uses a dictionary internally so the unsafe read/check/write sequence is visible. Two workers can both observe 0 and both enter.

braid can force that interleaving through explicit probes such as after-read and before-write. The tests call TryEnterAsync(cancellationToken), and the failure report includes the seed, schedule, and trace needed to reproduce the race.

This example is generic domain code (not tied to any product beyond the MIT sample); see examples/user-operation-limiter on GitHub.

Current limitations

  • Explicit probes are required.
  • Await interception is not automatic.
  • Exhaustive search is not implemented.
  • String schedule parsing is not implemented yet.

Package status

braid 0.1.0 is published on NuGet. Minor releases may still evolve the public surface, but 0.1.x updates are intended to stay source-compatible when practical.

Manual release steps for maintainers: docs/release-process.md.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.5.0 91 6/3/2026
0.4.0 99 5/22/2026
0.3.1 98 5/10/2026
0.3.0 99 5/9/2026
0.2.1 112 5/9/2026
0.2.0 101 5/8/2026
0.1.0 112 5/8/2026
0.1.0-preview.1 50 5/7/2026