Async-native fluent assertion library for .NET — real soft assertions, observability hooks, and source-generator-powered typed surfaces.
dotnet add package Catchy
dotnet add package Catchy.XUnit # or .NUnit / .MSTest / .TUnit / .Reqnrollusing static Catchy.Stateless;
await Assert.That(user.Name).IsNotNull();
await Assert.That(order.Total).IsGreaterThan(0m).Because("free orders go through promotions");
await Assert.That(tags).Contains("urgent").And().HasCount(3);Soft assertions — all failures collected, reported together:
using static Catchy.AmbientSoft;
await Verify.That(order.Id).IsGreaterThan(0);
await Verify.That(order.Total).IsGreaterThan(0m);
await Verify.That(order.Status).IsNotEmpty();Soft and hard assertions plus flush
using static Catchy.Ambient;
await Assert.That(order.Id).IsGreaterThan(0);
await Assert.Soft.That(order.Total).IsGreaterThan(0m);
await Verify.That(order.Status).IsNotEmpty();
await Assert.That().SoftState().HasNoErrors(); // Explicit flush (automatic flushing is available via runner-specific packages)- Async-native. Every chain is
awaited — attach logs, traces, or AI/spec-driven output via async hooks with no threading workarounds. - Real soft assertions. Not scoped exception aggregation. Interleave soft and hard assertions; pass failure state across helpers and step definitions via ambient, DI, or explicit instance.
- No IntelliSense pollution.
Assert.That(value)— not.Should()on every type. - Source-generator extensibility. Add
[Assertable]to your type, get a full typed assertion surface generated. No boilerplate. - Trailing modifiers.
.Because("reason").IgnoringCase()after the assertion — reads like a sentence. - Conflict-free aliases. Built-in
Check/Verifyand support for custom ones. - MIT, permanently.
Early-stage, pre-1.0. The API will change. Open a Discussion before writing code so direction is agreed first. Issues and PRs are very welcome.
src/— production packagestests/— automated tests and NuGet smoke validationsamples/— runnable example projectsdocs/— canonical documentation
