Readable intent
Start from the subject: user.Email.Must().Contain("@"). The same pattern works across strings,
numbers, collections, objects, exceptions, files, dates, GUIDs, URIs, spans, and nullable values.
Fluent assertions for modern .NET
OmniAssert is a fluent assertion library for .NET 10 and C# 14. Express test intent in plain language, collect related failures, compare complex objects, and use optional compile-time diagnostics for boolean expressions.
01. Overview
OmniAssert makes tests easier to read at the point of writing and easier to diagnose at the point of failure. Assertions start from the value under test, failure messages capture the expression and formatted actual values, and optional compile-time tooling enriches boolean diagnostics.
Start from the subject: user.Email.Must().Contain("@"). The same pattern works across strings,
numbers, collections, objects, exceptions, files, dates, GUIDs, URIs, spans, and nullable values.
Failures include caller expressions and formatted values. Collections are expanded, object equivalence shows where graphs diverge, and ANSI colour improves terminal output.
The bundled Roslyn generator can rewrite Ensure.Expression(...) call sites at compile time so
failed boolean expressions report operand values, not just “condition was false”.
BeEquivalentTo performs structural comparison of object graphs, handles collections as unordered
multisets, and navigates circular references safely.
Wrap checks in AssertionScope to collect every failure in a test and report them together when
the scope is disposed.
Use Ensure.Succeed() to mark a test or branch as intentionally passing, and
Ensure.Fail() to force a failure with an optional message — similar to NUnit’s
Assert.Succeed / Assert.Fail.
EquivalenceOptions for BeEquivalentTo; expanded XML docs on public APIs; documentation
site API reference updated for v2.3.0.
OmniAssert.Extensions companion package for web, financial,
security, and regional validators; expanded span assertion coverage; improved automated test coverage across Core,
Extensions, Analyzers, and Generator.
Ensure.Expression(...) replaces deprecated
VerifyExpression(); new analyzers OA005–OA007 for subject-first comparisons, BeEmpty()
over HaveCount(0), and redundant NotBeNull() after new; collection
BeNull() / NotBeNull() / NotBe(...); string NotContain(...).
Verify() / To* syntax still compiles in v2
but is obsolete. See the migration guide and bundled Roslyn analyzers OA001–OA007 for
migration and style guidance.
02. Prerequisites
Basic assertions need only the NuGet package and a compatible target framework. Compile-time boolean diagnostics require C# 14 interceptors.
net10.0 or a compatible TFM.PATH.OmniAssert package.InterceptorsNamespaces configured if you pin language settings manually.OmniAssertDisableVerifyInterceptors not set to true.03. Installation
Add the package to your test project, import OmniAssert, then write assertions with
.Must(), .VerifyNullable(), Ensure.Expression(...), and the exception
and file helpers.
dotnet add package OmniAssert --version 2.3.0
<ItemGroup>
<PackageReference Include="OmniAssert" Version="2.3.0" />
</ItemGroup>
using OmniAssert;
Interceptors are bundled and enabled by default. If you pin language settings, ensure C# 14 and the generated namespace are configured:
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14</LangVersion>
<InterceptorsNamespaces>$(InterceptorsNamespaces);OmniAssert.Generated</InterceptorsNamespaces>
</PropertyGroup>
To opt out of compile-time boolean diagnostics:
<PropertyGroup>
<OmniAssertDisableVerifyInterceptors>true</OmniAssertDisableVerifyInterceptors>
</PropertyGroup>
OmniAssert.Extensions adds web, financial, security, and regional assertions. It requires the core OmniAssert package.
dotnet add package OmniAssert.Extensions --version 2.3.0
<ItemGroup>
<PackageReference Include="OmniAssert.Extensions" Version="2.3.0" />
</ItemGroup>
04. Quick start
This xUnit example validates a user object. The same style works with any .NET test framework because OmniAssert throws assertion exceptions directly.
using OmniAssert;
public sealed record User(int Id, string Email, bool IsActive);
public class UserTests
{
[Fact]
public void Should_validate_user()
{
var user = new User(42, "ada@example.com", true);
user.Id.Must().BeGreaterThan(0);
user.Email.Must().Contain("@");
user.Email.Must().EndWith(".com");
user.IsActive.Must().BeTrue();
}
}
Must(), then choose the
assertion that describes the expected behaviour. For nullable subjects use VerifyNullable(). For
simple booleans use flag.Must().BeTrue(). For compound conditions use
Ensure.Expression(x > 0 && count < limit).
Combine value checks with exception assertions on the same test. Failure messages include the expression under test and the actual value.
using OmniAssert;
public class OrderServiceTests
{
[Fact]
public void CreateOrder_with_invalid_email_fails()
{
var email = "not-an-email";
email.Must().NotBeNullOrEmpty();
email.Must().Contain("@");
var ex = (() => OrderService.Create(email)).Throws<ArgumentException>();
ex.WithMessageContaining("invalid email");
}
}
05. API reference
Fluent assertions begin with .Must() on any value. Nullable subjects use
.VerifyNullable(). Exception testing uses extension methods on Action,
Func<Task>, or the static Ensure.Throws<T>() helper. For explicit
pass/fail control, use Ensure.Succeed() and Ensure.Fail().
Assertions for standard numeric types including BigInteger (via INumber<T>).
answer.Must().Be(42);
count.Must().BeGreaterThan(0);
order.Total.Must().BePositive();
price.Must().BeApproximately(9.99m, 0.01m);
rate.Must().HaveDecimalPlaces(2);
score.Must().BeOneOf(10, 20, 30);
Be(expected) / NotBe(unexpected)BeGreaterThan(threshold) / BeGreaterThanOrEqualTo(threshold)BeLessThan(threshold) / BeLessThanOrEqualTo(threshold)BeInRange(min, max)BeApproximately(expected, precision)BeOneOf(values...)BePositive() / BeNegative() — strictly greater/less than zeroBeEven() / BeOdd() — integral values onlyBeMultipleOf(factor) — exact integer multiple; factor must be non-zeroBeFinite() / BeInfinite() — floating-point semantics; integral types always pass BeFiniteHaveDecimalPlaces(n) — counts fractional digits after converting to decimal (trailing zeros ignored)name.Must().StartWith("John");
sku.Must().BeAlphanumeric();
createdAt.Must().BeIso8601();
token.Must().BeBase64();
configPath.Must().BeAbsolutePath();
Contain(substring) / NotContain(substring) — ordinal by default; overloads with StringComparisonStartWith(prefix) / EndWith(suffix) / NotEndWith(suffix)Match(regex)BeIgnoringCase(expected) / BeOneOf(values...)BeNull() / NotBeNull()BeEmpty() / NotBeEmpty()BeNullOrEmpty() / NotBeNullOrEmpty()BeNullOrWhiteSpace() / NotBeNullOrWhiteSpace()BeWhiteSpace() / NotBeWhiteSpace()HaveLength(n) / HaveLengthGreaterThan(n) / HaveLengthLessThan(n) / HaveLengthBetween(min, max)BeTrimmed() — no leading or trailing whitespaceBeAlphanumeric() / BeLowerCase() / BeUpperCase() / BeNumeric()BeBase64() / BeHexString() / BeHexColor() — format validatorsBeIso8601() / BeDateString(format) — parsed with InvariantCultureBeAbsolutePath() / BeRelativePath() — syntactic check via Path.IsPathRooted (OS-specific)users.Must().HaveCountBetween(1, 10);
roles.Must().BeSubsetOf(allRoles);
items.Must().ContainOnly(allowedStatuses);
results.Must().BeEmpty();
settings.Must().ContainKey("Theme");
Be(expected) / NotBe(expected) — reference equality on the collection instanceHaveCount(n), HaveCountGreaterThan(n), HaveCountLessThan(n), HaveCountBetween(min, max)HaveCountMatching(n, predicate)BeEmpty() / NotBeEmpty()Contain(item) / NotContain(item)Contain(predicate), AllSatisfy(predicate), AnySatisfy(predicate), NoneSatisfy(predicate)ContainOnly(allowed) / ContainOnly(predicate) — every element must be permitted or matchBeSubsetOf(expected) / BeSupersetOf(expected) — set semantics with default equalityBeUnique() / HaveUniqueCount(n)BeInAscendingOrder() / BeInDescendingOrder() — optional key-selector overloadsBeEquivalentTo(other) — multiset equivalence, order ignoredBeEquivalentTo(other, EquivalenceOptions) — relaxed string case and/or nested sequence orderBeSequenceEqual(other) — exact sequence, same orderContainInOrder(items...) — items appear in order, not necessarily consecutivelyBeNull() / NotBeNull()ContainKey, NotContainKey, ContainValue, NotContainValue, HaveValue(key, value)instance.Must().BeOfType<MyClass>();
actualDto.Must().BeEquivalentTo(expectedDto);
actual.Must().BeEquivalentTo(expected, new EquivalenceOptions
{
IgnoreCase = true,
IgnoreCollectionOrder = true
});
Be(expected) — reference equalityBeNull() / NotBeNull()BeOfType<T>() / NotBeOfType<T>()BeAssignableTo<T>() / NotBeAssignableTo<T>()BeEquivalentTo(expected) — recursive property comparison with coloured diffBeEquivalentTo(expected, EquivalenceOptions) — tune case and nested collection orderEquivalenceOptions.IgnoreCase — ordinal case-insensitive strings in the graphEquivalenceOptions.IgnoreCollectionOrder — multiset comparison for nested sequencesAction act = () => service.DoWork();
act.Throws<InvalidOperationException>().WithMessage("Failed");
Func<Task> asyncAct = () => service.DoWorkAsync();
await asyncAct.ThrowsAsync<ArgumentException>();
await TimeSpan.FromSeconds(2).CompleteWithin(() => longRunningTask);
Throws<T>() / ThrowsAsync<T>()NotThrow() / NotThrowAsync()WithMessage(expected), WithMessageContaining(substring), WithMessageMatching(wildcard)WithMessageIgnoringCase(expected)WithInnerException<TInner>()CompleteWithin(action) on TimeSpancreatedAt.Must().BeInPast();
dueDate.Must().BeSameDayAs(expected);
birthday.Must().BeToday();
timeSpan.Must().BePositive();
guid.Must().NotBeEmpty();
DateTime / DateTimeOffset: Be, NotBe, BeBefore, BeAfter, BeWithin, BeInPast, BeInFuture, BeSameDayAsBeInPast / BeInFuture compare against UtcNow — use UTC subjectsDateOnly: Be, BeBefore, BeAfter, HaveYear, HaveMonth, HaveDay, BeToday, BeYesterday, BeTomorrow, BeWeekday, BeWeekend, BeLeapYear, BeWithinDays(n, anchor)DateOnly “today” helpers use the local clock (DateTime.Today)TimeOnly: Be, BeBefore, BeAfterTimeSpan: Be, NotBe, BePositive, BeNegative, range and ordering helpersGuid: Be, BeEmpty, NotBeEmpty, BeOneOfEnum: Be, NotBe, BeOneOfUri: Be, HaveScheme, HaveHost, HavePath, HaveQueryZero-allocation assertions for ReadOnlySpan<T>, Span<T>, ReadOnlyMemory<T>, and Memory<T> via .Must().
ReadOnlySpan<byte> header = buffer.AsSpan(0, 4);
header.Must().StartWith(magicBytes);
header.Must().HaveCountBetween(1, 64);
Equal / NotEqual, BeEmpty / NotBeEmptyHaveLength / HaveCount, count bounds including HaveCountBetweenContain, NotContain, StartWith, EndWithBeUnique, ordering, predicate helpers — same vocabulary as collectionsBeEquivalentTo, BeSequenceEqual, ContainInOrder"config.json".FileExists().HaveContent(expectedJson);
"log.txt".FileExists().NotBeEmpty().HaveExtension(".txt");
"temp".DirectoryExists().BeEmpty();
FileExists() → HaveContent(text), BeEmpty(), NotBeEmpty(), HaveExtension(ext)HaveExtension — leading dot optional, comparison is case-insensitiveDirectoryExists() → BeEmpty()Common assertion names elsewhere and their OmniAssert spelling:
BeDistinct / HaveUniqueItems
BeUnique()
BeSorted / BeInAscendingOrder
BeInAscendingOrder()
MatchRegex / BeRegex
Match(pattern)
BeNumericString
BeNumeric()
BeExistingFile
path.FileExists()
HaveSameElementsAs
BeEquivalentTo() (unordered) / BeSequenceEqual() (ordered)
BeQuasiEquivalentTo
BeEquivalentTo(expected, new EquivalenceOptions { IgnoreCase = true, IgnoreCollectionOrder = true })
Must() vs Ensure.Expression()// Fluent — consistent with other assertions
isValid.Must().BeTrue();
// Subject-first comparison (preferred over wrapping comparisons in BeTrue)
count.Must().BeLessThan(limit);
// Expression — compound boolean logic with rich diagnostics
Ensure.Expression(x > 0 && count < limit);
The legacy extension (condition).VerifyExpression() still works in v2.1 but is deprecated; use
Ensure.Expression(condition) for new code. It will be removed in v3.
Ensure.Succeed() and Ensure.Fail()
Static helpers for tests that need an intentional pass marker or a forced failure without asserting on a
subject value. Useful for placeholder tests, documented no-ops, unreachable branches, and
framework-agnostic equivalents of NUnit’s Assert.Succeed / Assert.Fail.
// Mark a test as intentionally passing (no checks performed)
Ensure.Succeed();
// Force failure with default message: "Verification failed: explicit failure."
Ensure.Fail();
// Force failure with a custom message (prefix added when omitted)
Ensure.Fail("not implemented yet");
// → "Verification failed: not implemented yet"
// Message already prefixed is used as-is
Ensure.Fail("Verification failed: custom wording");
Ensure.Succeed() — completes without throwing; performs no verificationEnsure.Fail(message?) — throws OmniAssertionException immediately, unless an enclosing AssertionScope is collecting failures05b. Extensions
OmniAssert.Extensions extends
the .Must() fluent API with validators for web payloads, financial identifiers, security tokens,
and regional business rules. Import only the namespaces you need.
OmniAssert package at the
same version. Add both PackageReference entries to your test project.
<ItemGroup>
<PackageReference Include="OmniAssert" Version="2.3.0" />
<PackageReference Include="OmniAssert.Extensions" Version="2.3.0" />
</ItemGroup>
OmniAssert.Extensions.Web
Email, URL, JSON/XML/HTML, IP, hostname, MAC, HTTP status, content-type, network reachability
OmniAssert.Extensions.Financials
GUID, ULID, CUID, ISBN, IMEI, credit card (Luhn), IBAN (mod-97)
OmniAssert.Extensions.Security
Password strength, hash formats, JWT structure, API keys, OAuth tokens
OmniAssert.Extensions.Regional
Nigerian identifiers (phone, NUBAN, BVN, NIN, postal), global phone/postal/SSN, age and business hours
using OmniAssert;
using OmniAssert.Extensions.Web;
using OmniAssert.Extensions.Financials;
using OmniAssert.Extensions.Security;
using OmniAssert.Extensions.Regional;
// Web
"user@acme.com".Must().BeEmailAddress();
"https://api.example.com/v1".Must().BeUrl();
"{\"id\":1}".Must().BeJson();
// Financial
"978-0-306-40615-7".Must().BeIsbn();
"GB29NWBK60161331926819".Must().BeIban();
// Security
"MyP@ssw0rd!".Must().BeStrongPassword(minLength: 12);
token.Must().BeJwt();
// Regional
"+2348012345678".Must().BeNigerianPhoneNumber();
"0022728153".Must().BeNigerianBankAccountNumber("058");
String extensions unless noted. Use core Uri assertions (HaveScheme, HaveHost, …) when you already have a parsed Uri object; use Extensions BeUrl() to validate string format before parsing.
BeEmailAddress()
Non-empty; MailAddress parseable; local-part and domain present; domain contains .; no spaces; max 254 characters (practical RFC 5322 subset).
BeUrl() / BeAbsoluteUrl()
Absolute URI with http or https scheme and non-empty host.
BeRelativeUrl()
Starts with /; not parseable as an absolute URI (no scheme/host).
BeSlug()
Lowercase alphanumeric segments separated by single hyphens; no leading/trailing hyphen (^[a-z0-9]+(?:-[a-z0-9]+)*$).
BeJson()
Non-empty; strict JsonDocument.Parse (malformed or trailing junk fails).
BeXml()
Non-empty; well-formed XML via XDocument.Parse.
BeHtml()
Document heuristic: <html>…</html> / <body>…</body>, or opening tag plus matching closing tag (not a full HTML5 parser).
BeIpAddress() / BeValidIpAddress()
IPAddress.TryParse succeeds (IPv4 or IPv6).
BeIpv4() / BeIpv6()
Parsed address family is InterNetwork or InterNetworkV6 respectively.
BeValidHostname()
Labels 1–63 chars; total ≤ 253; optional trailing dot stripped; each label matches hostname label rules.
BeValidMacAddress() / BeMacAddress()
AA:BB:CC:DD:EE:FF, hyphen-separated, or 12 contiguous hex digits.
BeValidPort()
On NumericAssertions<int>: value in 0–65535.
BeHttpStatusCode(int) / BeSuccessStatusCode()
On NumericAssertions<int>: exact match, or any 2xx code.
BeReachable() / BeReachableAsync()
Live HTTP check: prepends https:// when no scheme; HEAD then GET fallback; 2xx/3xx = reachable; default timeout 5s (optional TimeSpan?). Set OMNIASSERT_SKIP_NETWORK=1 in CI to skip without failing.
Use HttpResponseMessage.Must() from OmniAssert.Extensions.Web:
using var response = await client.GetAsync("/api/users");
response.Must().BeSuccessStatusCode();
response.Must().HaveContentType("application/json");
404.Must().BeHttpStatusCode(404);
BeSuccessStatusCode()
Status code in 200–299.
BeHttpStatusCode(int)
Exact status code match.
HaveContentType(string)
Content-Type media type equals expected (case-insensitive; charset/parameters ignored).
BeGuid(GuidFormat?)
Guid.TryParse; optional GuidFormat enum (N, D, B, P, X) requires string to match that layout exactly.
BeUlid()
26 Crockford base32 characters; rejects I, L, O, U.
BeCuid()
Starts with c; 24–25 lowercase alphanumeric characters (CUID v1 pattern).
BeIsbn()
ISBN-10 (mod 11 check digit, X allowed) or ISBN-13 (978/979 prefix, mod 10 check digit); hyphens/spaces stripped.
BeImei()
Exactly 15 digits; Luhn-valid including check digit.
BeCreditCard() / BeCreditCardNumber()
13–19 digits after stripping separators; Luhn-valid.
BeIban()
Length 15–34; country code + check digits; mod-97 rearrangement validation (IBAN standard).
BeStrongPassword(minLength?)
Default minLength 8; requires uppercase, lowercase, digit, and symbol from a documented punctuation set.
BeSha256Hash()
Exactly 64 hex characters (optional 0x prefix stripped).
BeMd5Hash()
Exactly 32 hex characters.
BeHashedWith(HashType)
HashType: Md5 (32 hex), Sha256 (64 hex), Sha512 (128 hex).
BeJwtToken() / BeJwt()
Three dot-separated Base64Url segments; decoded header JSON contains alg; payload is a JSON object.
BeApiKey()
Length 16–128; [A-Za-z0-9_-]; at least one letter and one digit.
BeOAuthToken()
Optional Bearer prefix stripped; length ≥ 20; RFC 6750-style token character set.
BeNigerianPhoneNumber()
+234 or leading 0; normalized to Nigerian mobile prefixes (e.g. 701–709, 802–809, 811, 813–819, 901–909).
BeNigerianBankAccountNumber()
Format-only: exactly 10 digits.
BeNigerianBankAccountNumber(bankCode)
Full NUBAN check: 10-digit account + 3-digit bank code; weighted mod-10 check digit verification.
BeNigerianBvn() / BeNigerianNin()
Exactly 11 digits; rejects all-identical digits.
BeNigerianPostalCode()
Exactly 6 digits.
BePhoneNumber()
E.164-style: optional +; 8–15 total digits.
BePostalCode(CountryCode)
CountryCode: US, GB, CA, NG, or Generic (default). Each applies country-specific patterns.
BeSocialSecurityNumber()
US SSN: rejects invalid area (000, 666, 9xx), group 00, serial 0000.
BeAdult(age?)
On DateTime / DateOnly; default age 18; computed against UTC calendar date today.
BeBirthday()
Month and day match local calendar date today (DateTime.Today / DateOnly equivalent).
BeBusinessHours(timeZone?)
Weekday (Mon–Fri); time in [09:00, 17:00) local or converted via optional TimeZoneInfo.
Extensions use the same VerificationFlow as core OmniAssert. Failures are prefixed with
Verification failed: and include the captured expression (for example email) and the
actual value. Inside an AssertionScope, failures are collected and reported together.
Match(regex)?
Prefer Extensions when you need a named, documented validator (IBAN mod-97, NUBAN, JWT structure) with
consistent failure wording. Use core Match for one-off project-specific patterns.
BeReachable() pass in CI without network access?
Set OMNIASSERT_SKIP_NETWORK=1. Reachability assertions become no-ops so pipelines without
outbound HTTP stay deterministic.
Yes. OmniAssert.Extensions declares a NuGet dependency on OmniAssert at the
matching version. Mismatched versions can cause missing APIs or assertion struct incompatibilities.
06. Advanced usage
Soft assertions, explicit pass/fail outcomes, deep object comparison, predicate-based collection checks, async exception testing, and compile-time boolean operand capture.
Ensure.Succeed() and Ensure.Fail()
Use Ensure.Succeed() when a test must pass without performing checks — for example a placeholder
while behaviour is still being defined, or a branch that documents intentional no-op behaviour. Use
Ensure.Fail() when a code path should never run, or when you need to abort a test with a clear
message before v3-style guard assertions exist.
[Fact]
public void Placeholder_until_feature_lands()
{
// Documents that the test harness is wired; replace with real checks later.
Ensure.Succeed();
}
[Fact]
public void Should_reject_unsupported_platform()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Ensure.Fail("This integration test runs on Linux CI agents only.");
}
service.RunLinuxOnlyWorkflow();
}
Inside an AssertionScope, Ensure.Fail() is collected like any other failure and
reported when the scope is disposed — it does not throw immediately.
using (new AssertionScope())
{
customer.Id.Must().BeGreaterThan(0);
Ensure.Fail("billing address not yet validated");
// Both failures are reported together when the scope ends
}
AssertionScopeusing (new AssertionScope())
{
user.Name.Must().Be("John");
user.Age.Must().Be(30);
user.Email.Must().Contain("@");
}
BeEquivalentTo compares object graphs recursively and reports the path where values diverge. On
collections it treats sequences as multisets — same elements in any order. Use BeSequenceEqual when
order matters. Pass EquivalenceOptions to relax string case and nested collection order instead of
inventing a separate “quasi-equivalent” API.
var expected = new InvoiceDto
{
Number = "INV-2026-001",
Lines =
[
new InvoiceLineDto("Consulting", 2, 750m),
new InvoiceLineDto("Support", 1, 125m)
]
};
var actual = await client.GetInvoiceAsync("INV-2026-001");
actual.Must().BeEquivalentTo(expected);
// Case-insensitive strings + order-insensitive nested sequences
actual.Must().BeEquivalentTo(expected, new EquivalenceOptions
{
IgnoreCase = true,
IgnoreCollectionOrder = true
});
roles.Must().BeSubsetOf(allRoles);
permissions.Must().BeSupersetOf(requiredPermissions);
tags.Must().ContainOnly(allowedTags);
batch.Must().HaveCountBetween(1, 100);
results.Must().HaveCount(documents.Count);
results.Must().BeUnique();
results.Must().AllSatisfy(r => r.Score > 0);
results.Must().AnySatisfy(r => r.Title.Contains("OmniAssert"));
results.Must().NoneSatisfy(r => r.Status == "Failed");
results.Must().BeInDescendingOrder(r => r.Score);
Action syncAct = () => parser.Parse("");
syncAct.Throws<ArgumentException>()
.WithMessageContaining("input");
Func<Task> asyncAct = () => service.CreateUserAsync(invalidRequest);
await asyncAct.ThrowsAsync<ValidationException>();
await TimeSpan.FromSeconds(2).CompleteWithin(
() => service.RebuildProjectionAsync());
The bundled Roslyn generator rewrites supported Ensure.Expression(...) call sites at compile time.
Legacy VerifyExpression() call sites are still intercepted for compatibility. When active, a bare
identifier such as Ensure.Expression(flag) becomes flag.Must(expression).BeTrue(), and
compound expressions capture operand snapshots on failure.
var count = orders.Count;
var limit = 10;
var hasPriorityOrder = orders.Any(o => o.Priority == Priority.High);
Ensure.Expression(count > 0 && count <= limit && hasPriorityOrder);
"appsettings.test.json".FileExists().HaveContent(expectedJson);
"artifacts".DirectoryExists().BeEmpty();
var callback = new Uri("https://github.com/bolorundurowb/OmniAssert");
callback.Must().HaveScheme("https");
callback.Must().HaveHost("github.com");
07. Assertion style
OmniAssert ships Roslyn analyzers that migrate legacy v1 syntax (OA001–OA004) and suggest clearer, subject-first patterns (OA005–OA007). All rules are bundled in the NuGet package.
Assert.* entry point → use Ensure.*
.Verify() fluent root → use .Must()
To* / NotTo* grammar → use Be* / Not* / Have*
VerifyExpression(...) → use Ensure.Expression(...)
(a < b).Must().BeTrue() / BeFalse() → rewrite as a.Must().BeLessThan(b) (numeric / TimeSpan only)
collection.Must().HaveCount(0) → use collection.Must().BeEmpty()
new Widget().Must().NotBeNull() is redundant (a new expression cannot be null)
// Avoid
(count < limit).Must().BeTrue();
// Prefer
count.Must().BeLessThan(limit);
// Avoid
results.Must().HaveCount(0);
// Prefer
results.Must().BeEmpty();
// Avoid
(path.Contains('/')).Must().BeFalse();
// Prefer
path.Must().NotContain('/');
// When a single comparison assertion does not apply
Ensure.Expression(x > 0 && count < limit && hasPriority);
OA005 suggests Ensure.Expression(...) when the left-hand side is not a numeric or TimeSpan subject.
list.Must().BeNull() or list.Must().NotBeNull() — no (object?) cast needed.08. Migration
OmniAssert v2 modernises the fluent API, avoids naming clashes with NUnit’s Assert, and ships
Roslyn analyzers to automate migration. Legacy v1 syntax still compiles but is [Obsolete] — plan
to migrate before v3 removes it.
Assert, .Verify(), all To* / NotTo* methods, the
Expect wrapper, and extension VerifyExpression(...). Use
Ensure.Expression(...) instead. VerifyNullable, exception extensions, and file helpers
are not scheduled for removal.
Assert static entry point
Ensure
.Verify()
.Must()
.ToBeTrue(), .ToBe(42), .ToContain("x")
.BeTrue(), .Be(42), .Contain("x")
.NotToBe(...), .NotToBeNull()
.NotBe(...), .NotBeNull()
.ToHaveCount(3), .ToBeGreaterThan(0)
.HaveCount(3), .BeGreaterThan(0)
Expect.Throws<T>(...)
Ensure.Throws<T>(...) or (...).Throws<T>()
Assert.Succeed() / Assert.Fail(...)
Ensure.Succeed() / Ensure.Fail(...)
(condition).VerifyExpression()
Ensure.Expression(condition)
Methods that never used a To prefix in v1 are unchanged — for example ContainKey,
HaveHost, AllSatisfy, and file helpers like HaveContent.
Assert → Ensurev1 used a static class named Assert, which conflicts with NUnit, xUnit, and other frameworks.
// v1
OmniAssert.Assert.VerifyExpression(condition);
// v2 (legacy extension — still works with warnings)
Ensure.VerifyExpression(condition);
// v2 (recommended)
Ensure.Expression(condition);
Verify() → Must()// v1
count.Verify().ToBeGreaterThan(0);
user.Email.Verify().ToContain("@");
// v2
count.Must().BeGreaterThan(0);
user.Email.Must().Contain("@");
To prefixGeneral rules:
ToXxx(...) → Xxx(...) — e.g. ToBeFalse() → BeFalse()NotToXxx(...) → NotXxx(...) — e.g. NotToBeNull() → NotBeNull()ToBeTrue() / ToBeFalse()BeTrue() / BeFalse()ToBe(expected)Be(expected)NotToBe(unexpected)NotBe(unexpected)ToBeNull() / NotToBeNull()BeNull() / NotBeNull()ToContain(...) / NotToContain(...)Contain(...) / NotContain(...)ToBeEquivalentTo(...)BeEquivalentTo(...)ToBeOfType<T>()BeOfType<T>()VerifyNullable(...) — nullable entry point (e.g. value.VerifyNullable().BeNull())(...).Throws<T>(), NotThrow(), ThrowsAsync<T>()"path".FileExists().HaveContent(...)AssertionScope — soft assertions behave the sameWithMessage / WithInnerException<T> — exception chainingEnsure.Succeed() / Ensure.Fail(...) — explicit pass and forced failure (new in v2; legacy Assert.Succeed / Assert.Fail delegate to these)Must() vs Ensure.Expression()// Fluent (v2)
isValid.Must().BeTrue();
// Expression style (recommended for compound conditions)
Ensure.Expression(x > 0 && count < limit);
// Legacy extension (deprecated — removed in v3)
(x > 0 && count < limit).VerifyExpression();
When interceptors are enabled, Ensure.Expression(flag) is rewritten at compile time to
flag.Must(expression).BeTrue(). Legacy VerifyExpression() call sites are still
intercepted for compatibility.
The OmniAssert NuGet package includes OmniAssert.Analyzers.dll — no extra package
reference is required.
OmniAssert.Assert → replace with Ensure
.Verify() fluent entry → replace with .Must()
To* / NotTo* methods → replace with Be* / Not* / Have*
VerifyExpression(...) → rewrite to Ensure.Expression(...)
Style analyzers OA005–OA007 are documented in the assertion style section.
// Before
Assert.Throws<InvalidOperationException>(() => act());
value.Verify().ToBeFalse();
items.Verify().NotToBeNull();
(x > y).VerifyExpression();
// After
Ensure.Throws<InvalidOperationException>(() => act());
value.Must().BeFalse();
items.Must().NotBeNull();
Ensure.Expression(x > y);
If you used Expect to avoid clashing with NUnit’s Assert, prefer Ensure in v2. The Expect wrapper remains in v2 but will be removed in v3.
// v1
Expect.Throws<ArgumentException>(() => act());
// v2
Ensure.Throws<ArgumentException>(() => act());
// or
((Action)act).Throws<ArgumentException>();
OmniAssert package to v2.x (latest: 2.3.0)..Verify(), .ToBe, VerifyExpression, or OmniAssert.Assert usages.<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>OA001;OA002;OA003;OA004</WarningsNotAsErrors>
</PropertyGroup>
09. Troubleshooting
Most setup issues come from target framework mismatches, missing namespace imports, or pinned language settings.
.Must() not appear in IntelliSense?
Confirm the package is referenced by the test project and using OmniAssert; is in scope. Restore
packages and reload the project if you use central package management.
Check that the project builds with the .NET 10 SDK and C# 14, and that
OmniAssertDisableVerifyInterceptors is not true. If you pin language settings, add
OmniAssert.Generated to InterceptorsNamespaces.
Must().BeTrue() or Ensure.Expression()?
Use Must().BeTrue() for simple boolean values. Prefer subject-first comparisons such as
count.Must().BeLessThan(limit) over (count < limit).Must().BeTrue() (OA005).
Use Ensure.Expression(...) when a compound condition cannot be expressed as a single comparison
assertion. The legacy VerifyExpression() extension is deprecated.
BeEquivalentTo for collections treats sequences as multisets. Use
BeSequenceEqual or ordering assertions such as BeInAscendingOrder() when order is
part of the behaviour under test.
The assertions were probably inside an AssertionScope. The scope collects failures and throws
an aggregate result when disposed — this is expected.
Yes. OmniAssert is a standalone assertion library. Import the namespace and use it from whichever .NET test
framework your project already uses. v2's Ensure entry point avoids clashing with NUnit's
Assert.
No. Extensions is an optional companion package. The core OmniAssert package includes all
standard assertions. Install OmniAssert.Extensions only if you need domain-specific validators
for web, financial, security, or regional checks.
Set the environment variable OMNIASSERT_SKIP_NETWORK=1 before running tests. When this variable
is set, BeReachable() and BeReachableAsync() pass immediately without making any
network calls. This prevents flaky tests in CI environments without network access.
BeNigerianBankAccountNumber() validate the bank code?
Without a bank code parameter, the assertion checks only that the value is exactly 10 digits (format-only).
Use the overload BeNigerianBankAccountNumber(string bankCode) for full NUBAN validation that
verifies the check digit against the 3-digit bank code.
Legacy Verify(), To*, and VerifyExpression APIs are marked obsolete in
v2. Apply the OA001–OA004 code fixes or follow the migration guide. Legacy syntax
will be removed in v3.
Ensure.Succeed() or Ensure.Fail()?
Use Ensure.Succeed() for intentional no-op or placeholder tests where no subject value is under
verification. Use Ensure.Fail() to abort a test with a message — for example an unsupported
platform branch or a path that should be unreachable. Prefer fluent .Must() assertions when you
have a value to check. If you used NUnit’s Assert.Succeed / Assert.Fail, switch to
the Ensure equivalents to avoid clashing with NUnit’s Assert type.
10. Contributing
OmniAssert is open source. Contributions are most useful when they are focused, tested, and clear about the behaviour they change.
Use GitHub issues for bug reports, documentation gaps, feature requests, or confusing diagnostics. Include the OmniAssert version, target framework, test framework, a minimal reproduction, and failure output.
dotnet build src/OmniAssert.slnx
dotnet test src/OmniAssert.slnx
dotnet test src/OmniAssert.slnx /p:CollectCoverage=true