fluentcontracts
Agent skill for FluentContracts — write and apply Must() guard chains in C#, pick the right check, and add reusable rules as specifications.
Open source Open in the app JSON README (API)
About
Agent skill for FluentContracts — write and apply Must() guard chains in C#, pick the right check, and add reusable rules as specifications.
Details
- Kind
- Plugins
- Topic
- No topic detected
- Publisher
- fluentcontracts
- Origin
- gemini
- Category
- ferramentas
- Version
- 1.0.0
- Stars
- 41
- Forks
- 3
- Open pull requests
- 5
- Last push
- 2026-09-06T00:56:13Z
- Repository state
- ativo
- Language
- C#
- License
- MIT
- Added
- 2026-09-05 09:00:35
- Updated
- 2026-09-05 09:00:35
- Origin id
fluentcontracts/fluentcontracts
README
<img alt="Logo" width="100px" src="https://github.com/FluentContracts/FluentContracts/raw/master/assets/icon.png"/>
# FluentContracts
[](https://www.nuget.org/packages/FluentContracts/)
[](https://www.nuget.org/packages/FluentContracts/)
[](LICENSE)
Argument validation that reads like the rule it enforces, and fails with a message that says what
was expected of which argument. Inspired by [FluentAssertions](https://github.com/fluentassertions/fluentassertions).
```
dotnet add package FluentContracts
```
## Why another validation library
[FluentValidation](https://github.com/FluentValidation/FluentValidation) and `Guard` from the
[.NET Community Toolkit](https://github.com/CommunityToolkit/dotnet) are excellent, and if you already
use them, keep doing so.
This one exists because guard clauses tend to read as noise: three lines of `if` and `throw` for one
rule, a `nameof` to keep in sync, an exception type to pick, and a message to write — or, more often,
not write. FluentContracts makes the rule the whole statement, the way FluentAssertions does for a
test, and puts the argument name, the exception type and a readable message in for you.
## A guard clause, before and after
```csharp
public void AddOrder(Order myOrder)
{
if (myOrder == null) throw new ArgumentNullException(nameof(myOrder));
if (myOrder.Quantity < 5) throw new ArgumentOutOfRangeException(nameof(myOrder), "Quantity cannot be less than 5");
}
```
```csharp
public void AddOrder(Order myOrder)
{
myOrder.Must().NotBeNull().Satisfy<Order>(o => o.Quantity >= 5, "Quantity cannot be less than 5");
}
```
`Must()` starts a chain on any value. Every check returns the same chain, so checks follow one
another with no glue; `.And.` between them is optional and purely for reading. The argument's name
is captured at the call site, so exceptions point at the right parameter without `nameof`.
## What every check gives you
**A failure message that names the argument, the expectation and the value.**
```csharp
port.Must().BeBetween(1, 65535);
// ArgumentOutOfRangeException: Expected port to be between 1 and 65535, but found 70000. (Parameter 'port')
email.Must().BeEmailAddress();
// ArgumentException: Expected email to be a valid email address, but found "not-an-email". (Parameter 'email')
pages.Must().BeInAscendingOrder();
// ArgumentException: Expected pages to be in ascending order, but 5 appears before 3. (Parameter 'pages')
```
**Your own message when you want one.** It is always the last parameter, it replaces the default, and
it may use `{argument}` and `{value}`:
```csharp
port.Must().BeBetween(1, 65535, "{argument} must be a usable port, got {value}");
// ArgumentOutOfRangeException: port must be a usable port, got 70000 (Parameter 'port')
environment.Must("This should be prod").NotBe("test").NotBeEmpty();
// one message for every check in the chain; a check's own message still wins
```
**The validated value back**, so the guard and the read are one expression:
```csharp
this.port = config.Port.Must().BeBetween(1, 65535).Value();
```
`Value()` unwraps a nullable and fails a `null` argument exactly as `NotBeNull` would.
**The right exception type**, without choosing it:
| Failure | Throws |
|---|---|
| a `null` argument | `ArgumentNullException` |
| a comparison, range, sign or NaN check | `ArgumentOutOfRangeException` |
| everything else — equality, format, containment, type, your own rules | `ArgumentException` |
Or the exception you name: `myOrder.Must().NotBeNull<OrderNullException>()` and
`Satisfy<Order, OrderQuantityException>(o => o.Quantity >= 5, "...")` throw yours.
**Frames you can read.** The library hides its own frames from the stack trace, so a failure points
at the check you wrote.
## What you can check
Every type gets `BeNull`/`NotBeNull`, `Be`/`NotBe`, `BeAnyOf`/`NotBeAnyOf` and `Satisfy`; then
each adds what makes sense for it. The full list is in
[SupportedContracts.md](docs/SupportedContracts.md).
**Numbers** — `int`, `long`, `short`, `byte`, their unsigned forms, `float`, `double`, `decimal`, and
on .NET 8+ any `INumber<T>` such as `Half`, `Int128` or `BigInteger`:
```csharp
quantity.Must().BePositive().BeLessOrEqualTo(100);
retries.Must().BeBetween(1, 5);
page.Must().BeEven();
ratio.Must().BeFinite(); // neither NaN nor infinity
```
A comparison never passes on `NaN` and never passes on `null`; `BeNaN` and `BeFinite` are how you
ask about NaN on purpose.
**Text and characters** — shape, format and content:
```csharp
name.Must().NotBeNullOrWhiteSpace().HaveLengthLessOrEqualTo(64);
code.Must().BeAlphanumeric().BeUppercase();
input.Must().BeEmailAddress(); // also BeUrl, BeIpAddress, BeGuid, BeBase64, BeHexadecimal, BeCreditCardNumber
path.Must().BeExistingFile();
slug.Must().BeMatching("^[a-z0-9-]+$");
title.Must().Contain("draft", StringComparison.OrdinalIgnoreCase); // Ordinal by default
initial.Must().BeLetter().BeUppercase();
```
**Dates and times** — `DateTime`, `DateTimeOffset`, `TimeSpan`, and on .NET 8+ `DateOnly` and
`TimeOnly`:
```csharp
start.Must().BeInTheFuture().BeWeekday();
deadline.Must().BeBetween(start, end);
timeout.Must().BeLongerThan(TimeSpan.FromSeconds(1));
stamp.Must().BeUtc();
booking.Must().BeInCurrentYear().NotBeInDecember();
```
Checks that need the current time take a clock, so tests can pin it:
`start.Must(dateTimeProvider: clock).BeInTheFuture()`.
**Collections and dictionaries** — arrays, `IList<T>`, `IDictionary<TKey, TValue>`:
```csharp
items.Must().NotBeEmpty().HaveCountLessOrEqualTo(100).HaveUniqueItems();
tags.Must().Contain("public").NotContainNull();
scores.Must().BeInDescendingOrder().AllSatisfy(s => s >= 0);
settings.Must().ContainKey("region").NotContainKey("legacy");
```
`BeAnyOf`, `Contain` and `ContainAnyOf` take one value or one bracketed set:
`state.Must().BeAnyOf(["draft", "published"], "Not a known state")`.
**Enums, GUIDs, booleans**:
```csharp
role.Must().BeDefined().NotBe(Role.Guest);
flags.Must().HaveFlag(Permissions.Read);
id.Must().NotBeEmpty();
enabled.Must().BeTrue();
```
**Files, directories, streams and URIs**:
```csharp
file.Must().Exist().NotBeEmpty().HaveExtension(".json").HaveSizeLessThan(1_000_000);
folder.Must().Exist().NotBeReadOnly();
stream.Must().BeReadable().BeSeekable();
endpoint.Must().BeAbsolute().BeHttps().HaveHost("api.example.com");
```
**Any object** — null, type, and a rule of your own:
```csharp
payload.Must().NotBeNull().BeOfType<OrderPlaced>();
handler.Must().BeAssignableTo<IHandler>();
myOrder.Must().Satisfy<Order>(o => o.Quantity >= 5);
```
## Your own rules
A rule you check in more than one place is a specification: a predicate and the phrase that
completes *"Expected `{argument}` to …"*. Its failure reads exactly like a built-in check:
```csharp
static readonly ISpecification<string> ValidIban =
Spec.From<string>(s => Iban.IsValid(s), "be a valid IBAN");
iban.Must().Satisfy(ValidIban);
// ArgumentException: Expected iban to be a valid IBAN, but found "XX00". (Parameter 'iban')
```
Rules compose — `ValidIban.And(SepaCountry)` expects *"be a valid IBAN and be in a SEPA country"*,
`ValidIban.Not()` expects *"not be a valid IBAN"* — and a rule that needs more room than a lambda
derives from `Specification<T>` and overrides `IsSatisfiedBy`.
A type of your own can get a contract of its own: derive from `ObjectContract<T, TContract>`, add
checks that return the contract, and add a `Must()` extension for the type. Every check above then
chains with yours.
## The package
- Targets `netstandard2.0` and `net8.0`: .NET Framework 4.6.1+, .NET Core, .NET 5 and later, Mono, Unity.
- No runtime dependencies.
- Trimming and Native AOT compatible on `net8.0`, verified on a trimmed and an AOT-published app.
- A chain is one small object, and on .NET 10 the JIT keeps it off the heap entirely; see
[Benchmarks.md](docs/Benchmarks.md).
- Ships a Roslyn analyzer for the misuses that compile but check the wrong thing (none open at the
moment; see [the analyzer project](src/FluentContracts.Analyzers/README.md)).
- Every public member has XML documentation, so all of the above is in IntelliSense.
## The agent skill
The rules that make a chain correct — which check to reach for, where the message goes, which
exception a check throws — are documented, but documentation does not reach a coding agent working in
*your* project. So they ship as an **agent skill** too: `fluentcontracts`, packaged as a plugin for
Claude Code, Codex and Gemini CLI.
With it installed, an agent asked to add argument validation confirms a check exists instead of
guessing one, chains from `Must()` and ends with `Value()`, keeps the message last, respects the
single bracketed-set rule for `BeAnyOf` and its family, lets the check decide the exception, and
reaches for `Satisfy` or an `ISpecification<T>` rather than dropping a raw `throw` into a chain.
It is not in the NuGet package — it is served from this repository, so you install it once per
machine and it applies to every project you use FluentContracts in.
### Installing it
**Claude Code** — add this repository as a plugin marketplace, then install the plugin:
```
/plugin marketplace add FluentContracts/FluentContracts
/plugin install fluentcontracts@fluentcontracts
```
Pin it to a version by adding the marketplace at a tag instead —
`FluentContracts/FluentContracts@plugin-v1.0.0`. Every merge that moves the plugin version tags it,
whether or not a package was released alongside.
**Codex** — the same repository is a Codex plugin marketplace:
```
codex plugin marketplace add https://github.com/FluentContracts/FluentContracts
codex plugin install fluentcontracts
```
**Gemini CLI** — the repository is a Gemini extension, and the skills are discovered from it:
```
gemini extensions install https://github.com/FluentContracts/FluentContracts
```
**Any other agent** — the skill is a plain folder. Copy
[`skills/fluentcontracts`](skills/fluentcontracts) into wherever your harness looks for skills.
### Using it
You do not invoke it. It carries a description of when it applies, and the agent loads it on its own
once the work is about argument validation — "add guards to this constructor", "replace these
`if`/`throw` blocks", "validate the options before we use them". Naming FluentContracts in the ask
makes it certain.
Two files, both worth reading yourself:
[`SKILL.md`](skills/fluentcontracts/SKILL.md) is the guidance, and
[`references/cheatsheet.md`](skills/fluentcontracts/references/cheatsheet.md) is the catalogue of what
exists plus the rules that decide overloads, messages and exception types.
## Help needed 🙏
The goal is for this to be exhaustive, safe and stable enough for production use on large projects,
and help is very welcome: a check that is missing, a message that could read better, a platform that
is not covered. Open an issue first, then a pull request — [CONTRIBUTING.md](CONTRIBUTING.md) and
[AGENTS.md](AGENTS.md) describe the conventions, and the latter is written for coding agents as well
as people.
## Repository 🚧
### Builds
| Type | Status |
|:-------------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Release | [](https://github.com/FluentContracts/FluentContracts/actions/workflows/release.yml) |
| Code Coverage | [](https://coveralls.io/github/FluentContracts/FluentContracts) |
Pull requests are built and tested on Linux, Windows and macOS by the
[`pr`](https://github.com/FluentContracts/FluentContracts/actions/workflows/pr.yml) workflow. Every
merge into `master` is a release; [CHANGELOG.md](CHANGELOG.md) is the curated account of each one.
### Status

### How to build locally
The .NET SDK version is pinned in `global.json`. Then:
```bash
./build.sh Test # compile and run the tests (build.cmd on Windows)
./build.sh Test Pack # ...and produce the package in output/packages
```
## Where to find me 🕵️
[](https://todorov.bg)
[](https://twitter.com/totollygeek)
[](https://www.linkedin.com/in/totollygeek)
[](https://infosec.exchange/@totollygeek)
[](https://www.threads.net/@totollygeek)
[](https://bsky.app/profile/totollygeek.com)
[](https://linktr.ee/totollygeek)
[](mailto://fluentcontracts@pm.me)
## Special thanks 🙇♂️
#### [Matthias Koch](https://twitter.com/matkoch87)
> The creator of [NUKE](https://nuke.build), because I cannot build any .NET project without it and because he helped me tremendously in setting up the repository and everything around this project. (_I have also copy-pasted, like his entire build and some markdown files_ 🤫)
#### [Dennis Doomen](https://twitter.com/ddoomen)
> The "[FluentAssertions](https://fluentassertions.com/)" guy. This whole project was inspired by how that library works and I might have copy-pasted also parts of his repo too 😏
## Technology Sponsors 💻
<img alt="JetBrains Logo" width="300px" src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png"/>
> Special thanks to [JetBrains](https://www.jetbrains.com/) for supplying a free license for [Rider](https://www.jetbrains.com/rider/), which is my primary IDE of choice for this project!
Icon made by [IconMonk](https://www.flaticon.com/authors/icon-monk) from [Flaticon](https://www.flaticon.com)