# Welcome

Keep exceptions exceptional

Waystone.Monads gives C# two types that say what your code actually means.

* `Option<T>` — the value might not be there.
* `Result<T, E>` — the work might fail.

Both are ordinary C# types. There is no framework to adopt and nothing to wire up. You add a package, you change a return type, and the compiler starts telling you about the cases you used to find at runtime.

## Who this is for

You are writing C# and you are tired of two things: `null` reaching places it should not, and exceptions being used for outcomes that are not exceptional.

The library replaces both with values you can return, pass around, and compose. Absence and failure stop being surprises hidden inside a method body, and start being part of the signature you already read.

If you have used `Option` and `Result` in Rust or F#, you already know the shape. If you have not, [Why monads](/start-here/why-monads) walks through it with no prior knowledge assumed.

## Where to go next

| If you want to                                     | Go to                                    |
| -------------------------------------------------- | ---------------------------------------- |
| Install the package and see both types work        | [Quickstart](/start-here/quickstart)     |
| Understand why this beats `null` and `try`/`catch` | [Why monads](/start-here/why-monads)     |
| Teach your coding agent to write it properly       | [Agent skills](/start-here/agent-skills) |

## What comes in the box

Installing `Waystone.Monads` gets you the two types, a Roslyn analyzer, and a source generator. You configure none of it. The analyzer flags the mistakes people make with these types, and the generator turns an enum into a set of error codes. See [Analyzer rules](/reference/analyzers) and [Generated error codes](/reference/source-generation).

Optional packages sit beside the library — Shouldly assertions, LINQ query syntax, JSON converters, and more. None is required, and none changes how `Waystone.Monads` behaves. [Add-ons](/reference/packages) extend the library itself. [Integrations](/reference/integrations) connect it to a library you already use.

## Links

* [Source on GitHub](https://github.com/draekien-industries/waystone-dotnet) — the `Waystone.Monads` package lives in `src/Waystone.Monads`
* [Waystone.Monads on NuGet](https://www.nuget.org/packages/Waystone.Monads)
* [Report an issue](https://github.com/draekien-industries/waystone-dotnet/issues) — for the library or for these docs


# Quickstart

Install the package and get both types working, without leaving this page.

Ready to stop writing `null` checks and stop catching exceptions you expected? Here is the whole setup.

## Install

```sh
dotnet add package Waystone.Monads
```

That is the only package you need. The analyzer and the source generator come with it, already switched on.

## Add the usings

**`using Waystone.Monads;` on its own gets you nothing.** The root namespace holds no types, so that line compiles and then every type name fails with `CS0234`. Pick the namespaces you need instead:

| Namespace                            | What it holds                                                                                                                         |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `Waystone.Monads.Options`            | `Option<T>` and the static `Option` factories                                                                                         |
| `Waystone.Monads.Options.Extensions` | Extension methods on `Option<T>`: `Flatten`, `Transpose`, `Unzip`, `UnwrapOrNull`, the collection helpers, and every `Async` overload |
| `Waystone.Monads.Results`            | `Result<TOk, TErr>` and the static `Result` factories                                                                                 |
| `Waystone.Monads.Results.Extensions` | Extension methods on `Result<TOk, TErr>`: `Flatten`, `Transpose`, `UnwrapOrNull`, the collection helpers, and every `Async` overload  |
| `Waystone.Monads.Results.Errors`     | `Error`, `ErrorCode` and `[ErrorCodeCatalog]`                                                                                         |
| `Waystone.Monads.Exceptions`         | `UnwrapException` and `UnmetExpectationException`                                                                                     |
| `Waystone.Monads.Configs`            | `MonadOptions` and `ErrorCodeFactory`                                                                                                 |

{% hint style="warning" %}
**The two `Extensions` namespaces are not just async.** They hold synchronous methods too. Import only `Waystone.Monads.Options` and a call to `UnwrapOrNull` fails with `CS1061`, even though the method exists.
{% endhint %}

On C# 10 or later, put the ones you use everywhere in a single `GlobalUsings.cs` and stop repeating them per file:

```csharp
global using Waystone.Monads.Options;
global using Waystone.Monads.Options.Extensions;
global using Waystone.Monads.Results;
global using Waystone.Monads.Results.Extensions;
global using Waystone.Monads.Results.Errors;
```

{% hint style="warning" %}
**Generated catalog members need a using for your own namespace.** When you mark an enum with `[ErrorCodeCatalog]`, the generated `{EnumName}Catalog` class and the `ToError`, `ToErrorCode` and `ToErrorCodeName` extensions are emitted into the enum's own namespace, not into a Waystone one. Fully qualifying the enum at the call site is not enough — the extensions still need `using` for the namespace the enum lives in. See [Source generation](/reference/source-generation).
{% endhint %}

## Your first Option\<T>

`Option<T>` says a value might not be there. Instead of returning `null` and hoping the caller checks, you return a type that cannot be read without handling both cases.

```csharp
Option<string> patron = Option.Some("The Raven Queen");
Option<string> noPatron = Option.None<string>();

string vow = patron.Match(
    some => $"You are sworn to {some}.",
    () => "You are sworn to no one.");
// "You are sworn to The Raven Queen."

string silence = noPatron.Match(
    some => $"You are sworn to {some}.",
    () => "You are sworn to no one.");
// "You are sworn to no one."
```

`Match` is the way out. It takes one function for each case and returns a plain value, so there is no state left to forget about.

## Your first Result\<T, E>

`Result<T, E>` says the work might fail. The failure is a value you return, not an exception you throw, so the caller sees it in the signature.

```csharp
Result<int, string> RollDie(string sides)
{
    return int.TryParse(sides, out int faces) && faces > 0
        ? Result.Ok<int, string>(Random.Shared.Next(1, faces + 1))
        : Result.Err<int, string>($"'{sides}' is not a number of sides.");
}

Result<int, string> roll = RollDie("20");

int value = roll.Match(
    ok => ok,
    err => 0);
// somewhere between 1 and 20
```

Same shape as before. One function for success, one for failure, a plain value out the other end.

{% hint style="success" %}
No exceptions. No `try`/`catch`. Both outcomes are visible in the return type.
{% endhint %}

## Where to go next

* [Why monads](/start-here/why-monads) — the case for doing it this way at all.
* [Option\<T>](/guides/option) and [Result\<T, E>](/guides/result) — working with each type properly.
* [Agent skills](/start-here/agent-skills) — teach your coding agent the same habits.


# Why monads

Why return an Option or a Result instead of using null and exceptions.

You already have ways to say "no value" and "it failed". C# gives you `null` and it gives you exceptions. So why add a library?

Because neither one shows up where you read it: the method signature.

## What goes wrong today

Nothing here is exotic. It is the ordinary shape of C# code.

* A method returns `null` when a business rule says there is nothing to return.
* A method throws when a business rule says the work cannot continue.
* Callers wrap the call in `try`/`catch` just to log and re-throw.
* Guard clauses pile up at the top of every method.
* Branching logic spreads across `if`, `else` and `switch` until the happy path is hard to find.

The cost is that a signature tells you almost nothing. `Ritual PrepareRitual(decimal)` looks total. It might return `null`. It might throw. You find out in production.

## What a monad actually is

The word sounds academic. In practice it is small.

A monad is a type that wraps a value and gives you one consistent way to keep working with it — including when there is no value, or when something failed.

{% hint style="success" %}
A monad has to do three things:

1. Let you wrap a value — `Some`, `Ok`.
2. Let you chain work onto it — `Map`, `AndThen`.
3. Carry the context along — whether it is missing, or failed.
   {% endhint %}

This library ships two of them:

* `Option<T>` — there might be a value.
* `Result<T, E>` — this might have failed.

That is the whole idea. The rest is what you can do with it.

## The same code, both ways

Here is a spell being cast. Components go in, an effect comes out, and several things can go wrong along the way.

Written the usual way:

```csharp
Ritual? PrepareRitual(decimal components); // can return null or throw
SpellEffect Cast(Ritual ritual);           // can still return null or throw

SpellEffect CastSpell(decimal? components)
{
    if (components is null)
    {
        return Cantrip();
    }

    try
    {
        Ritual? ritual = PrepareRitual(components.Value);

        return ritual is not null
            ? Cast(ritual)
            : Cantrip();
    }
    catch (FailedToPrepareRitualException ex) // an exception for a valid outcome
    {
        _logger.LogWarning(ex, "Failed to prepare the ritual");
        return Cantrip();
    }
    catch (Exception ex)
    {
        _logger.LogWarning(ex, "Failed to cast the spell");
        throw;
    }
}

SpellEffect effect = CastSpell(10); // an effect, or a thrown exception

string message = effect?.Message ?? "Something went wrong"; // the error is gone
```

Count what is actually happening. Two of those branches are business rules wearing an exception costume. The caller still cannot tell success from failure, and the reason for the failure was thrown away on the last line.

Now the same thing with monads:

```csharp
Option<Ritual> PrepareRitual(decimal components);       // never throws, Some or None
Result<SpellEffect, Error> Cast(Ritual ritual);         // Ok or Err

Result<SpellEffect, Error> CastSpell(Option<decimal> components) =>
    components.AndThen(PrepareRitual) // if components are Some, prepare the ritual
              .Map(Cast)              // if the ritual is Some, cast it
              .Transpose()            // turn the Option<Result> into a Result<Option>
              .InspectErr(error => _logger.LogWarning(error))  // if Err, log it
              .Map(effect => effect.UnwrapOrElse(Cantrip));    // if Ok, take the effect

string message = CastSpell(Option.Some(10.0m)).Match(
    onOk: effect => effect.Message,
    onErr: error => error.Message); // the first error the pipeline hit
```

{% hint style="info" %}
`_logger` is whatever logger you already use. It is scenery in both samples — nothing in this library requires one.
{% endhint %}

No conditionals. No defensive `null` checks. No local variables scattered between the steps. Every failure that can happen is named in a signature, and the reason survives all the way to the caller.

## The picture: two railway tracks

Think of your program as a train.

Each step of your logic is a station. The train loads data, transforms it, runs a check, and moves on. Things go wrong: a record is missing, a validation fails, a file is not there.

**Without monads, a problem derails the train.** An exception unwinds the stack past every station you cared about. A `null` slips through and derails you three stations later, somewhere unrelated. Some methods return `null`, some throw, some just work — so you cannot chain them without guessing.

**With monads, there are two tracks.** A success track, where the train keeps moving. A failure track, where it is quietly diverted to a siding.

* :train2: **Success track** — the next step runs.
* :construction: **Failure or none track** — the next step is skipped, and the train arrives carrying the reason.

The train never jumps between tracks by accident. It stays on one or the other, and every station is built to handle both.

```csharp
Option<string> patron = FindCharacter(name)
    .Map(character => character.Spellbook)
    .AndThen(spellbook => spellbook.Patron);
```

If the character does not exist, or the spellbook has no patron, nothing crashes. The train stops safely at `None`, and you decide what that means later:

```csharp
string display = patron.Match(
    some => some,
    () => "[No patron]");
```

## Where to go next

* [Quickstart](/start-here/quickstart) — install the package and run both types yourself.
* [Option\<T>](/guides/option) — absence, in depth.
* [Result\<T, E>](/guides/result) — failure, in depth.


# Agent skills

Teach your coding agent to write Waystone.Monads the way it is meant to be written.

Coding agents reach for `null` checks and `try`/`catch` by default, because that is what most of the C# they learned from does. Give an agent this library and it will often write `if (option.IsSome) { option.Unwrap(); }` — code that compiles, passes review at a glance, and throws away everything you adopted the library for.

The `waystone-monads` **skill** fixes that. It is a document the agent loads when it notices you are writing `Option<T>` or `Result<TOk, TErr>`, and it teaches the composition style, the traps, and the analyzer rules that catch them.

You do not need it to use the NuGet package. Install it if you want an agent to write idiomatic code without being told how every time.

## Install it

Pick whichever suits the agent you use.

### As a Claude Code plugin

Run both commands inside Claude Code:

```
/plugin marketplace add draekien-industries/waystone-dotnet
/plugin install waystone-dotnet@waystone-dotnet
```

The plugin name and the marketplace name are both `waystone-dotnet`, which is why it appears twice. Installing the plugin gets you the skill plus anything else the plugin gains later.

### As a standalone skill

Use [`npx skills`](https://agentskills.io) if you use a different agent, or if you want the skill without the plugin:

```sh
npx skills add draekien-industries/waystone-dotnet --skill waystone-monads
```

That installs into the current project. Add `-g` to install it for every project instead. Run the same command with `--list` and no `--skill` to see everything the repository offers.

## What it teaches

The skill is built from this library's own analyzer rules and sample project rather than from general advice about monads. It covers:

| Area            | What the agent learns                                                                  |
| --------------- | -------------------------------------------------------------------------------------- |
| Composition     | Chain `Map`, `AndThen`, `Filter` and `OrElse`; collapse once at the end                |
| Traps           | Nested `Match`, `IsSome` guarding an `Unwrap`, `null` on a monad, a discarded `Result` |
| Choosing a type | When absence is an `Option`, when failure is a `Result`, and when to throw instead     |
| Nesting         | What `Result<Option<T>, E>` means, and how `Transpose` moves between the two shapes    |
| Async           | Keeping a chain intact across an `await` rather than breaking it into locals           |
| Error codes     | Building failures through the generated `{EnumName}Catalog.Errors` factories           |
| Rust habits     | Which reflexes carry over, and which ones hurt in shipped C#                           |

You do not invoke it. The agent loads it when the work matches, so it applies to code you ask for in passing as much as to a task you set up deliberately.

## It does not replace the analyzer

The skill and the analyzer solve two halves of one problem, and you want both.

* The **skill** shapes what an agent writes. It has no way to check the result.
* The **analyzer** checks what anyone wrote, whether an agent or a person. It cannot write anything.

An agent following the skill still produces code the analyzer reports, and a clean build is the bar. See [Analyzers](/reference/analyzers) for what each rule means.

{% hint style="info" %}
**Update it the way you update a package.** The skill describes the library at a point in time, so a stale copy will teach an API that has moved on. Run `npx skills update` for a standalone install, or `/plugin update waystone-dotnet` inside Claude Code — that one names the plugin and needs a restart to take effect.
{% endhint %}


# Option\<T>

Model a value that might not be there, and work with it without null checks.

`Option<T>` says a value might not be there. It has exactly two shapes:

* `Some<T>` — there is a value.
* `None<T>` — there is not.

That is the whole type. What makes it worth using is that you cannot read the value without acknowledging the second case, so the compiler catches what a `null` check would have let through.

{% hint style="info" %}
Other languages call this `Maybe<T>`. Same idea.
{% endhint %}

## Why not just use null?

`null` tells you nothing. It does not say why the value is missing, or whether it was ever meant to be there. It spreads guard clauses through your code, and the compiler will still happily let you dereference it.

`Option<T>` says the absence out loud, in the signature, where you already look.

There is a second difference that matters more than it sounds. `None` is not an error. When you write:

```csharp
Option<Character> FindCharacter(string name);
```

you are not saying "this might blow up". You are saying "this might not find anything, and that is a normal outcome". If you need to know *why* it failed, reach for [`Result<T, E>`](/guides/result) instead.

## Create one

```csharp
Option<string> some = Option.Some("Keyleth");
Option<string> none = Option.None<string>();
Option<string> fromNullable = Option.FromNullable(sigil);
Option<string> fromTry = Option.Try(() => sigil!.Split('@')[1]);
```

* `Option.Some` and `Option.None<T>` are the two you will write most.
* `Option.FromNullable` takes something that might already be `null` — usually at the edge of your code, where you cannot control the shape.
* `Option.Try` runs a function that might throw and gives you `None` if it does.

## Transform it

You rarely want to look inside an `Option`. You want to keep working, and let the `None` case take care of itself.

### Map

`Map` changes the value if there is one, and does nothing if there is not.

```csharp
Option<int> nameLength = FindCharacter(name)
    .Map(character => character.Name.Length);
```

### AndThen

Use `AndThen` when the next step *also* returns an `Option`. `Map` would give you an `Option<Option<T>>`; `AndThen` keeps it flat.

```csharp
Option<string> domain = FindPatron(name).AndThen(TryExtractDomain);
```

{% hint style="info" %}
`AndThen` short-circuits. If anything in the chain is `None`, the later functions never run.
{% endhint %}

{% hint style="warning" %}
This was called `FlatMap` before 5.4.0. It was `[Obsolete]` through 5.x and 6.0.0 removed it, so a call to it is `CS0117` rather than a warning. `WM2014`, the rule that reported each call site, retired with it — delete any `.editorconfig` entry for that id. See [Deprecations](/upgrading/deprecations).
{% endhint %}

### Filter

`Filter` keeps a value only if it passes your predicate. If it does not, you get `None`.

```csharp
Option<string> maybeName = Option.Some("Thordak");

Option<string> nonEmpty = maybeName.Filter(name => name.Length > 0); // Some("Thordak")
Option<string> blank = maybeName.Filter(name => name.Length == 0);   // None
```

This is the replacement for an `if`-guard in the middle of a pipeline.

### Chain them

Put those three together and the whole thing reads top to bottom, with no branching at all:

```csharp
Option<string> patron = FindCharacter(name)
    .Filter(character => character.Name.Length > 0)
    .AndThen(character => character.Patron)
    .Map(patron => patron.ToUpperInvariant());
```

Compare that with the version you would otherwise write:

```csharp
string? DoWork(string name)
{
    Character? character = FindCharacter(name);

    if (character is { Name.Length: > 0, Patron: not null })
    {
        return character.Patron.ToUpperInvariant();
    }

    return null;
}
```

Same behaviour. One of them tells you what it is doing.

## Get the value back out

Every chain ends somewhere. These are the ways out.

### Match

`Match` is the honest one. You supply both branches and get a plain value.

```csharp
string patron = maybePatron.Match(
    patron => patron,
    () => "[No patron]");
```

### Unwrap with a fallback

```csharp
string orFallback = maybePatron.UnwrapOr("[No patron]");
string orComputed = maybePatron.UnwrapOrElse(() => LoadHousePatron());
```

Use `UnwrapOr` when the fallback is already sitting there. Use `UnwrapOrElse` when producing it costs something — the function only runs on a `None`.

Wrapping a value you already hold in a lambda gets you the worst of both. The fallback is built either way, and the call allocates a delegate to defer work that has already happened. If you can write it as an argument, pass it as one.

{% hint style="danger" %}
There is also a bare `Unwrap()`. It throws on a `None`. It exists for the cases where absence really is a bug, and the analyzer will tell you when you have reached for it out of habit.
{% endhint %}

### Pattern matching

Since 7.0.0, `Option<T>` deconstructs, so C# pattern matching works on it directly:

```csharp
if (maybePatron is Some<string>(var patron))
{
    logger.LogInformation("Sworn to {Patron}", patron);
}
```

And exhaustively, in a `switch`:

```csharp
string display = maybePatron switch
{
    Some<string>(var patron) => patron,
    None<string> => "[No patron]",
    _ => throw new UnreachableException(),
};
```

{% hint style="info" %}
The discard arm is there because the compiler cannot see that `Some` and `None` are the only two cases. It never runs.
{% endhint %}

## Check without unwrapping

Sometimes you only want a `bool`.

```csharp
Option<string> maybePatron = Option.Some("The Raven Queen");

maybePatron.IsSomeAnd(patron => patron.Length > 0);                 // true
maybePatron.IsNoneOr(patron => patron.Length > 0);                  // true
maybePatron.IsNoneOr(patron => string.IsNullOrWhiteSpace(patron));  // false
```

* `IsSomeAnd` — there is a value **and** it passes the predicate.
* `IsNoneOr` — there is no value, **or** the one there passes.

## Combine two options

### Zip, ZipWith and Unzip

`Zip` pairs two options into one, and gives `None` if either side is missing.

```csharp
Option<string> vex = Option.Some("Vex'ahlia");
Option<string> vax = Option.Some("Vax'ildan");
Option<string> missing = Option.None<string>();

Option<(string, string)> twins = vex.Zip(vax);     // Some(("Vex'ahlia", "Vax'ildan"))
Option<(string, string)> alone = vex.Zip(missing); // None
```

`ZipWith` does the same but combines the two values yourself instead of making a tuple:

```csharp
Option<int> fireball = Option.Some(24);
Option<int> sneakAttack = Option.Some(18);

Option<int> total = fireball.ZipWith(sneakAttack, (a, b) => a + b);
//         ^? Some(42)
```

`Unzip` reverses a `Zip`:

```csharp
(Option<string>, Option<string>) unzipped = twins.Unzip();
//                              ^? (Some("Vex'ahlia"), Some("Vax'ildan"))
```

A component that happens to equal its type's default is an ordinary value here, so `Option.Some((0, "x")).Unzip()` gives `(Some(0), Some("x"))`. This threw before 6.0.0.

### Fallback chains

`Or` takes the first `Some` it finds. `OrElse` is the same, but the fallback is only built if it is needed.

```csharp
Option<string> result = chosen.Or(absent).Or(fallback);
//             ^? Some("Keyleth")

Option<string> lazy = first
    .OrElse(() => RollForAnother())
    .OrElse(() => SendInTheHireling());
//     ^? Some("The understudy")
```

### The rest

Three more exist, and each is occasionally exactly what you want.

| Method   | What it does                                                                                                                           |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `And`    | Returns the second option, but only if the first was `Some`. Answers "did both arrive?"                                                |
| `Reduce` | Merges two options of the same type. Your function runs only when both are `Some`; otherwise the one that exists comes back untouched. |
| `Xor`    | Returns the value only if exactly one of the two is `Some`.                                                                            |

```csharp
Option<int> both = maybeName.And(maybeLevel);                    // Some(19)
Option<int> merged = firstRoll.Reduce(secondRoll, (a, b) => a + b); // Some(7)
Option<string> exclusive = bardsong.Xor(silence);                // Some("Scanlan")
```

## Work with a collection of them

A `List<Option<T>>` has its own set of helpers — `Collect`, `Flatten`, `Map`, `Filter`, `FirstOrNone` and friends. They live in `Waystone.Monads.Options.Extensions`, and are covered on the [Option\<T> collections reference](/reference/option/collections).

To step out of a single `Option` and into `System.Linq`, use `AsEnumerable`. It gives you a sequence of nothing or one:

```csharp
Option<string> maybeName = Option.Some("Pike");

IEnumerable<string> sequence = maybeName.AsEnumerable();
//                  ^? ["Pike"], and [] for a None
```

For real LINQ query syntax over an `Option` — `from`, `where`, `select`, staying inside the monad the whole way — see [Waystone.Monads.Linq](/reference/packages/linq).

## Printing and logging

**`ToString()` never shows the wrapped value.** You get the state and nothing else:

```csharp
Option.Some("Vex'ahlia").ToString() // "Some { IsSome = True, IsNone = False }"
Option.None<string>().ToString()    // "None { IsSome = False, IsNone = True }"
```

`Option<T>` is a record and `Some<T>` keeps its value in a private property, so the compiler-generated `ToString()` has nothing to print. Interpolating an option into a log message tells you whether a value was there, never what it was.

To log the value when it exists, use `Inspect`. It runs your action only on a `Some`, and hands the option back so the chain continues:

```csharp
Option<Character> character = FindCharacter(name)
    .Inspect(c => logger.LogInformation("Found character {Name}", c.Name));
```

Nothing runs on a `None`. To log both branches, use `Match`.

## When to reach for it

Use `Option<T>` when:

* The value is intentionally optional, not missing by accident.
* You want a chain that bails out early on absence.
* You do not care *why* it is absent.
* You want the caller to have to deal with the empty case.

Reach for something else when:

* The default of a value type already means absence — `0` for a count, say.
* You care about the reason. That is [`Result<T, E>`](/guides/result).

## Where to go next

* [Result\<T, E>](/guides/result) — the same idea, for failure.
* [Async](/guides/async) — keeping a chain intact across an `await`.
* [Option\<T> API](/reference/option) — every overload, when you need one this page did not show.


# Result\<T, E>

Return failure as a value instead of throwing it, and chain fallible work.

`Result<T, E>` says the work might fail. It has exactly two shapes:

* `Ok<T, E>` — it worked, and here is the value.
* `Err<T, E>` — it failed, and here is why.

Where [`Option<T>`](/guides/option) models absence, `Result<T, E>` models failure. The difference is the `E`. An `Option` tells you nothing arrived. A `Result` tells you what went wrong.

{% hint style="info" %}
Some languages call this `Either<E, T>`. This library puts the success case first, because that is the one you read most.
{% endhint %}

## Why not just throw?

Exceptions are control flow with a bomb strapped to it.

* They are invisible in a signature. `Reward ClaimReward(Quest)` looks total.
* They are easy to forget. Nothing makes you handle one.
* They do not compose. You cannot chain a method that throws.
* They are catastrophic in a chain. One throw unwinds everything above it.

You cannot tell which methods throw without reading their source. A `Result` puts the failure in the return type, where you were already looking.

There is a second point, and it matters more. **`Err` is not an emergency.** When you write:

```csharp
Result<Character, Error> FindCharacter(string name);
```

you are saying "this can fail, and here is what failure looks like". That is a statement about your domain, not a panic button. Keep exceptions for the things that really are exceptional.

{% hint style="info" %}
If you are writing `try`/`catch` just to return a fallback or log something, you wanted a `Result`.
{% endhint %}

## Create one

```csharp
Result<int, string> ok = Result.Ok<int, string>(1);
Result<int, string> err = Result.Err<int, string>("Something went wrong");
```

Leave `TErr` off and it defaults to the library's own `Error` type:

```csharp
Result<int, Error> okWithDefaultError = Result.Ok<int>(1);
Result<int, Error> errWithDefaultError =
    Result.Err<int>(new Error("quest.failed", "Something went wrong"));
```

See [Errors](/guides/errors) for what `Error` holds and how to build one from an enum.

### Neither side can hold null

```csharp
Result.Ok<string, Error>(null!); // throws ArgumentNullException
```

`TOk` and `TErr` are both constrained `notnull`, and the constructors enforce it. The guard arrived in 5.5.0 — before it, an `Ok` could hold `null`, and the `null` surfaced later as a `NullReferenceException` somewhere in your own code.

A **default** value is fine, though. It is a real value:

```csharp
Result<int, string> zero = Result.Ok<int, string>(0);
Result<Guid, string> empty = Result.Ok<Guid, string>(Guid.Empty);
```

{% hint style="info" %}
`Result.Try` is the exception. It returns an `Err` for a `null` rather than throwing — that is the point of it.
{% endhint %}

## Chain fallible steps

`AndThen` is the workhorse. Each step returns a `Result`, and the first `Err` short-circuits the rest.

```csharp
Result<Reward, Error> reward = FindCharacter(name)
    .AndThen(GetQuest)
    .AndThen(ClaimReward);
```

If `FindCharacter` fails, `GetQuest` and `ClaimReward` never run, and the original error arrives at the caller untouched. No `try`/`catch`. No special cases.

{% hint style="info" %}
This is what people mean by railway-oriented programming. Your computation runs on one of two tracks, and it never jumps between them by accident.
{% endhint %}

`Map` is the same idea when the next step *cannot* fail — it changes the `Ok` value and leaves an `Err` alone.

## Work on the error side

This is what separates a `Result` from an `Option`, so it is worth knowing well.

### MapErr

`MapErr` transforms the error, leaving the success value alone. Reach for it when two pieces of code disagree about the error type and you need them to chain.

```csharp
Result<int, Error> lengthResult = RollForName()            // Result<string, string>
    .MapErr(message => new Error("name.failed", message))  // Result<string, Error>
    .Map(name => CountRunes(name))                         // Result<Result<int, Error>, Error>
    .Flatten();                                            // Result<int, Error>
```

The last two lines are a `Map` that nests, then a `Flatten` that undoes it. That is exactly what `AndThen` does in one step:

```csharp
Result<int, Error> lengthResult = RollForName()            // Result<string, string>
    .MapErr(message => new Error("name.failed", message))  // Result<string, Error>
    .AndThen(name => CountRunes(name));                    // Result<int, Error>
```

Prefer the second. The first is shown because you will meet it in code that grew one method at a time.

### InspectErr

`InspectErr` runs a side effect on the error and hands the result back unchanged, so the chain continues. Logging is what it is for.

```csharp
Result<string, string> username = FindCharacter("Percy")
    .InspectErr(err => Console.WriteLine($"Find character failed: {err.Message}"))
    .Map(character => character.Username)
    .MapErr(err => err.Message);
```

`Inspect` is the same thing on the `Ok` branch. Use both together and each log line runs only on its own track:

```csharp
Result<Quest, Error> quest = LoadQuest(id)
    .Inspect(q => logger.LogInformation("Loaded quest {Id}", q.Id))
    .InspectErr(e => logger.LogWarning("Load failed: {Code} {Message}", e.Code, e.Message));
```

### UnwrapErr and ExpectErr

These pull the error out and throw if the result was actually `Ok`.

```csharp
Result<int, string> ok = Result.Ok<int, string>(10);
ok.UnwrapErr(); // throws UnwrapException

Result<int, string> err = Result.Err<int, string>("Error");
err.UnwrapErr(); // returns "Error"
```

`ExpectErr` does the same, but you supply the message:

```csharp
Result.Ok<int, string>(10).ExpectErr("Must be error");
// throws UnmetExpectationException with message "Must be error"
```

{% hint style="danger" %}
Both are intentional points of failure, like `First` on an empty sequence. Use them when you have already established the result is an `Err` — in a test, say. Everywhere else, use `Match`.
{% endhint %}

## Get the value back out

### Match

```csharp
string message = result.Match(
    reward => reward.Item,
    error => error.Message);
```

Both branches, one plain value out. This is the default answer.

### Unwrap with a fallback

```csharp
Reward orFallback = result.UnwrapOr(new Reward("A handful of copper"));
Reward orComputed = result.UnwrapOrElse(error => new Reward(error.Code));
```

`UnwrapOrElse` gets the error, so you can decide the fallback based on what went wrong. It only runs on an `Err`.

### Pattern matching

Since 7.0.0, `Result<T, E>` deconstructs:

```csharp
if (result is Err<Reward, Error>(var error))
{
    logger.LogWarning("No reward: {Message}", error.Message);
}
```

And exhaustively:

```csharp
string message = result switch
{
    Ok<Reward, Error>(var reward) => reward.Item,
    Err<Reward, Error>(var error) => error.Message,
    _ => throw new UnreachableException(),
};
```

{% hint style="info" %}
The discard arm is there because the compiler cannot see that `Ok` and `Err` are the only two cases. It never runs.
{% endhint %}

## Check without unwrapping

```csharp
Result<DateTime, Error> parsed = SafeParse("2025-01-01");

parsed.IsOkAnd(dateTime => dateTime > new DateTime(2024, 1, 1)); // true

Result<DateTime, Error> failed = SafeParse("2025");
failed.IsErrAnd(error => error.Code == ErrorCodes.MalformedDateTime); // true
```

* `IsOkAnd` — it succeeded **and** the value passes the predicate.
* `IsErrAnd` — it failed **and** the error passes the predicate.

## Combine two results

`And` and `Or` take a result you already have. `AndThen` and `OrElse` take a function, so the second result is only built when it is needed.

{% hint style="warning" %}
`And` and `Or` evaluate their argument eagerly. If producing it costs anything, use `AndThen` or `OrElse` instead.
{% endhint %}

`And` gives you the first `Err`, or the last `Ok`:

| Left   | Right  | Output |
| ------ | ------ | ------ |
| `Ok1`  | `Ok2`  | `Ok2`  |
| `Ok`   | `Err`  | `Err`  |
| `Err`  | `Ok`   | `Err`  |
| `Err1` | `Err2` | `Err1` |

`Or` gives you the first `Ok`, or the last `Err`:

| Left   | Right  | Output |
| ------ | ------ | ------ |
| `Ok1`  | `Ok2`  | `Ok1`  |
| `Ok`   | `Err`  | `Ok`   |
| `Err`  | `Ok`   | `Ok`   |
| `Err1` | `Err2` | `Err2` |

`OrElse` is how you recover. Its function runs on the **error**, not the value:

```csharp
Result<int, string> Recover(string error)
    => error == "NaN"
        ? Result.Ok<int, string>(0)
        : Result.Err<int, string>(error);

Result.Ok<int, string>(2).OrElse(Recover);            // Ok(2), untouched
Result.Err<int, string>("NaN").OrElse(Recover);       // Ok(0), recovered
Result.Err<int, string>("overflow").OrElse(Recover);  // Err("overflow"), still failed
```

## Work with a collection of them

A sequence of `Result` has its own helpers — `Collect`, `Partition`, `Flatten`, `FlattenErr` and `AsEnumerable`. They live in `Waystone.Monads.Results.Extensions`, and are covered on the [Result\<T, E> collections reference](/reference/result/collections).

The short version:

* `Collect` — all or nothing. One `Err` fails the whole batch.
* `Partition` — keeps both sides, so you learn which ones failed.

For LINQ query syntax over a `Result`, see [Waystone.Monads.Linq](/reference/packages/linq).

## Printing and logging

**`ToString()` never shows the wrapped value.** You get the state and nothing else:

```csharp
Result.Ok<int, Error>(1).ToString()  // "Ok { IsOk = True, IsErr = False }"
Result.Err<int, Error>(e).ToString() // "Err { IsOk = False, IsErr = True }"
```

`Result<TOk, TErr>` is a record and both sides keep their value in a private property, so the compiler-generated `ToString()` has nothing to print. Interpolating a result into a log message tells you which branch you are on, never what it holds. Use the `Inspect` / `InspectErr` pair above instead.

## When to reach for it

Use `Result<T, E>` when:

* A function can fail and you want that visible in the signature.
* You care *why* it failed.
* You want the caller to handle the failure explicitly.
* You are parsing, validating, or transforming input you do not control.
* You want exceptions to mean something has genuinely gone wrong.

Reach for [`Option<T>`](/guides/option) instead when you do not care about the reason.

## Where to go next

* [Option\<T>](/guides/option) — the same idea, for absence.
* [Errors](/guides/errors) — the `Error` type, error codes, and building one from an exception.
* [Exceptions](/guides/exceptions) — what the library throws, and when.
* [Async](/guides/async) — keeping a chain intact across an `await`.
* [Result\<T, E> API](/reference/result) — every overload, when you need one this page did not show.


# Errors

Build the failure value you put inside an Err.

You have decided a method returns `Result<T, E>`. Now you need something to put in the `E`.

You can use any type you like — a `string`, your own record, an enum. This page is about the two types the library ships for when you would rather not write your own.

{% hint style="info" %}
Looking for what the library **throws** at you? That is [Exceptions](/guides/exceptions).
{% endhint %}

## The two types

```csharp
public record ErrorCode
{
    public ErrorCode(string value);

    public string Value { get; }
}

public record Error
{
    public Error(ErrorCode code, string message);

    public ErrorCode Code { get; }
    public string Message { get; }
}
```

An `ErrorCode` is a short, stable identifier for a *kind* of failure. An `Error` is one *occurrence* of it, with a message a human can read.

Two details that trip people up:

* `Error` names its code property `Code`, not `ErrorCode`. Read it as `error.Code.Value`.
* Both types declare their constructor explicitly rather than positionally. So you cannot deconstruct them, and you cannot use `with` to change a property. Build a new instance instead.

## Define your error codes

An error code should not change between one occurrence of a failure and the next. That means defining them up front, not building them at the call site.

### As static fields (recommended)

The simplest thing that works. A static class holding every code your application can produce — in your domain layer, if you are following domain-driven design.

```csharp
public static class SpellErrorCodes
{
    public static readonly ErrorCode ComponentMissing = new("component.missing");
    public static readonly ErrorCode SigilMalformed = new("sigil.malformed");
    public static readonly ErrorCode LevelOutOfRange = new("level.out_of_range");
}
```

Start here. If you outgrow it, the next section is where to go.

### From an enum

If you would rather group codes as an enum, mark it `[ErrorCodeCatalog]` and the source generator writes the codes for you:

```csharp
[ErrorCodeCatalog]
public enum SpellErrors
{
    ComponentMissing = 1,
    SigilMalformed = 2,
    LevelOutOfRange = 3,
}
```

```csharp
ErrorCode code = SpellErrorsCatalog.Codes.SigilMalformed;
//        ^? "SpellErrors.SigilMalformed"
```

This is a generated API with more to it than one line — the naming format, the `ToErrorCode()` extension for when you only know the member at run time, the `using` it needs. See [Generated error codes](/reference/source-generation).

{% hint style="danger" %}
**`ErrorCode.FromEnum` was obsolete from 6.2.0 and 7.0.0 removed it.** So is overriding `ErrorCodeFactory.FromEnum` to shape what it returns. Mark the enum with `[ErrorCodeCatalog]` and use the generated members instead. See [Deprecations](/upgrading/deprecations) for the migration. `FromException` is unaffected.
{% endhint %}

## Build an error

```csharp
Error malformedSigil = new(
    SpellErrorCodes.SigilMalformed,
    "Expected an absolute sigil but received a relative one");

Error unparseable = new(
    SpellErrorCodes.SigilMalformed,
    "Failed to parse the sigil as a rune sequence");
```

Two occurrences, one code. That is the split working as intended: the code is what you branch on and log against, the message is what you show a person.

{% hint style="info" %}
You need an `ErrorCode` before you can build an `Error`. There is no message-only constructor.
{% endhint %}

### From a catalog enum

If your codes come from a `[ErrorCodeCatalog]` enum, the generated `Errors` factory builds the whole `Error` in one call:

```csharp
Error error = SpellErrorsCatalog.Errors.SigilMalformed(
    "Failed to parse the sigil as a rune sequence");
//    ^? Code: "SpellErrors.SigilMalformed", Message: "Failed to parse the sigil…"
```

The message is required here, unlike the code-only form. An `Error` always carries one.

To turn that straight into a `Result`, pass it to `Result.Err`:

```csharp
Result<int, Error> result = Result.Err<int>(
    SpellErrorsCatalog.Errors.SigilMalformed(
        "Failed to parse the sigil as a rune sequence"));
```

{% hint style="danger" %}
**`Error.FromEnum` was obsolete from 6.3.0 and 7.0.0 removed it**, along with the `Result.Err<TOk>(enum, message)` overload that used to collapse the two calls into one. Use the generated factory as above, or `value.ToError(message)` where you only know the member at run time. See [Generated error codes](/reference/source-generation).
{% endhint %}

## From an exception you caught

Both types convert from an exception you already hold, for when you are at a boundary and want a `Result` rather than a rethrow. That lives on the other page, next to the exceptions themselves — see [From an exception you caught](/guides/exceptions#turning-an-exception-into-an-error).

## Where to go next

* [Exceptions](/guides/exceptions) — what the library throws, and when.
* [Generated error codes](/reference/source-generation) — the `[ErrorCodeCatalog]` surface in full.
* [Result\<T, E>](/guides/result) — putting the error to work.


# Exceptions

What the library throws, when it throws it, and what to write instead.

The point of this library is that failure is a value. So it throws rarely, and when it does, it is because you asked for a value that was not there.

There are two exception types, and both mean the same thing: **you skipped the check.**

{% hint style="info" %}
Looking for how to build the failure you put *inside* an `Err`? That is [Errors](/guides/errors).
{% endhint %}

## UnwrapException

Thrown when you take the value out of a monad that does not have one.

```csharp
Option<string> none = Option.None<string>();
none.Unwrap(); // throws UnwrapException

Result<int, string> err = Result.Err<int, string>("the ritual fizzled");
err.Unwrap(); // throws UnwrapException
```

And in the other direction, when you take the error out of something that succeeded:

```csharp
Result<int, string> ok = Result.Ok<int, string>(20);
ok.UnwrapErr(); // throws UnwrapException
```

So: `Unwrap` on a `None` or an `Err`, and `UnwrapErr` on an `Ok`.

The async versions — `UnwrapAsync` and `UnwrapErrAsync` — throw for exactly the same reasons.

### What to write instead

Nine times out of ten you wanted one of these:

| Instead of        | Write                          | Because                                        |
| ----------------- | ------------------------------ | ---------------------------------------------- |
| `option.Unwrap()` | `option.Match(…, …)`           | Handles both cases, returns a plain value.     |
| `option.Unwrap()` | `option.UnwrapOr(fallback)`    | You already have something to fall back to.    |
| `option.Unwrap()` | `option.UnwrapOrElse(() => …)` | The fallback costs something to build.         |
| `option.Unwrap()` | `option.Map(…)`                | You were going to keep working with it anyway. |

`Unwrap` is not forbidden. It is the right call when absence really would be a bug and you want to fail loudly and immediately — the same reasoning as `First` on a sequence you know is not empty. The analyzer will tell you when you have reached for it out of habit.

## UnmetExpectationException

Thrown for the same reasons, by `Expect` and `ExpectErr` rather than `Unwrap` and `UnwrapErr`. The difference is that you supply the message.

```csharp
Option<string> none = Option.None<string>();
none.Expect("the familiar must be summoned");
// throws UnmetExpectationException with message "the familiar must be summoned"

Result<int, string> err = Result.Err<int, string>("the ritual fizzled");
err.Expect("the ritual must succeed"); // throws UnmetExpectationException

Result<int, string> ok = Result.Ok<int, string>(20);
ok.ExpectErr("the ritual must fail"); // throws UnmetExpectationException
```

`ExpectAsync` and `ExpectErrAsync` behave the same way.

### When to prefer Expect over Unwrap

Use `Expect` when the throw is deliberate and you can say something useful about why. The message goes straight into the exception, so whoever reads the log gets your reasoning rather than a stack trace and a shrug.

That makes `Expect` the better choice in a test, in application startup, and anywhere else the invariant is worth stating out loud. `Unwrap` is the terser option when there is nothing to add.

## Exceptions the library catches

Separately from the two above, some operations *swallow* exceptions on purpose.

`Option.Try`, `Result.Try` and their async counterparts run a factory that might throw, and turn a throw into a `None` or an `Err`. That is the whole point of them.

```csharp
Option<int> parsed = Option.Try(() => int.Parse(text));
// None if the parse threw
```

Two things to know about that:

* **The exception is not lost.** It goes to your configured exception logger. See [Configuration](/guides/configuration) and [Observability](/guides/observability).
* **`OperationCanceledException` is let through.** Cancellation is not a failure of your work, so it is not turned into an `Err`. See [Configuration](/guides/configuration#cancellation).

## Turning an exception into an Error

The other direction. You caught something at a boundary, and you want it as a `Result` rather than a rethrow. Both [error types](/guides/errors) convert.

```csharp
try
{
    // do work
}
catch (ScryingFailedException e)
{
    Error error = Error.FromException(e);
    //    ^? Code: "ScryingFailed", Message: e.Message
}
```

The code is the exception's type name with a trailing `Exception` removed. There is no prefix, the suffix match ignores case, and the exception's message is never read — so nothing from its text reaches the code.

| Exception type              | Resulting code     |
| --------------------------- | ------------------ |
| `SqlException`              | `Sql`              |
| `InvalidOperationException` | `InvalidOperation` |
| `ScryingFailedException`    | `ScryingFailed`    |
| `TimeoutException`          | `Timeout`          |
| `Exception`                 | `Exception`        |

`Exception` itself is the one special case. It keeps its whole name rather than reducing to an empty code.

`ErrorCode.FromException` does the same job when you want only the code:

```csharp
ErrorCode code = ErrorCode.FromException(e); // "ScryingFailed"
```

{% hint style="warning" %}
**This is a fallback, not a strategy.** Codes derived from exception types drift out of step with the codes you define by hand, and they do not help at all for failures that were never exceptions. Reach for it at a boundary you do not control, not throughout your domain.
{% endhint %}

{% hint style="info" %}
To change what these produce, supply your own `ErrorCodeFactory` to the global `MonadOptions` and override `FromException`. See [Configuration](/guides/configuration).
{% endhint %}

## Exceptions from the constructors

One more, and it is not from this library's own hierarchy.

Neither side of a `Result` can hold `null`, and nor can a `Some`. The constructors enforce it:

```csharp
Result.Ok<string, Error>(null!); // throws ArgumentNullException
```

A *default* value is fine — `Result.Ok<int, string>(0)` is an `Ok` holding zero. It is `null` specifically that is rejected. See [Result\<T, E>](/guides/result#neither-side-can-hold-null).

## Where to go next

* [Errors](/guides/errors) — building the failure value you return.
* [Option\<T>](/guides/option) and [Result\<T, E>](/guides/result) — the safe ways out of a monad.
* [Analyzer rules](/reference/analyzers) — what flags an `Unwrap` you should not have written.


# Schemas

Stop validating an object you already built. Parse the input instead, and let the type system carry the proof.

There is a bug that almost every codebase has a version of. It looks like this.

```csharp
public static Registration? Register(
    RegistrationDto dto,
    List<string> problems)
{
    if (string.IsNullOrWhiteSpace(dto.Email))
    {
        problems.Add("Email is required.");
    }

    if (dto.DisplayName is null)
    {
        problems.Add("Display name is required.");
    }

    if (dto.AcceptedTerms != true)
    {
        problems.Add("You have to accept the terms.");
    }

    if (problems.Count > 0)
    {
        return null;
    }

    return new Registration(
        dto.Email!,
        dto.DisplayName!,
        dto.Age is null ? Option.None<int>() : Option.Some(dto.Age.Value));
}
```

Nothing in there is wrong. It is how most of us write it.

But look at the null-forgiving operators in that final `return`. They are the tell. The compiler has no idea those checks ran, so nothing stops that line moving above them, and nothing stops it being written against a field nobody checked. The checks and the construction are two separate things that happen to be next to each other.

## Parse, don't validate

A schema closes that gap by doing both at once.

You do not hand it an object and ask whether it is valid. You hand it the raw input, and it hands you back the object — or it hands you back every reason it could not build one.

So holding the object *is* the proof. There is no separate step to forget.

## Three steps

### 1. Make the type unbuildable

Give your domain type a constructor nobody outside can call.

```csharp
public sealed class Registration
{
    internal Registration(string email, string displayName, Option<int> age)
    {
        Email = email;
        DisplayName = displayName;
        Age = age;
    }

    public string Email { get; }

    public string DisplayName { get; }

    public Option<int> Age { get; }
}
```

This is the step that does the work. Once the constructor is out of reach, the only way to hold a `Registration` is to have gone through the schema — and now the compiler enforces that, not your code review.

Notice `Option<int>` rather than `int?`. A value that may be absent says so in its type, so there is no null to forget about downstream.

### 2. Write the checks once

A schema is a value. Declare it, name it, and reuse it everywhere that shape of input turns up.

```csharp
public static class Registrations
{
    public static readonly Schema<string, string> Email =
        Schema.Text.Trim().Email();

    public static readonly Schema<string, string> DisplayName =
        Schema.Text.Trim().LengthBetween(2, 40);

    public static readonly Schema<int, int> Age =
        Schema.Number.Int32.Between(13, 130);

    // A rule over the whole subject rather than over one field.
    public static readonly Schema<RegistrationDto, RegistrationDto> Terms =
        Schema.For<RegistrationDto>()
              .Check(
                   subject => subject.AcceptedTerms == true,
                   ViolationCode.NotAllowed,
                   "You have to accept the terms.");
}
```

`Registrations.Email` is now the *only* definition of what an email address is in your application. When it changes, it changes in one place.

### 3. Describe the object

Derive from `SchemaConfig<TIn, TOut>`, mark the class `partial`, and list the fields.

```csharp
public partial class RegistrationSchema
    : SchemaConfig<RegistrationDto, Registration>
{
    protected override Result<Registration, SchemaViolation> Configure(
        RegistrationDto subject) =>
        Schema.Fields(
                   Schema.Required(subject.Email, Registrations.Email),
                   Schema.Required(subject.DisplayName, Registrations.DisplayName),
                   Schema.Optional(subject.Age, Registrations.Age))
              .Refine(Schema.Extend(subject, Registrations.Terms))
              .Into(
                   (email, name, age) => new Registration(email, name, age));
}
```

Three things are happening there.

* **`Schema.Fields` is written for you**, at exactly the number of fields you passed. So the `Into` lambda is checked when you compile, not when someone posts a registration.
* **`Schema.Optional` gives you `Option<int>`.** An absent age never reaches a rule and never reaches the constructor.
* **`Refine` is for rules that gate without contributing.** Accepting the terms has to be true, but there is nowhere on `Registration` to put it, so it goes here instead of into the lambda.

## Use it

```csharp
return RegistrationSchema.Instance
                         .Parse(body)
                         .Match<IResponse>(
                              registration => new Created(registration),
                              violation => new BadRequest(
                                  violation.ToDictionary()));
```

One call, two outcomes. Either you are holding a `Registration`, or you are holding every reason you are not.

## What you get back

A failure is a `SchemaViolation`. It carries a list of individual violations, and each one knows the path it was found at and has a message written for a human.

Two properties of that are worth knowing up front.

**Every failure comes back at once.** A schema does not stop at the first problem. A payload with a bad email, a short display name and no terms gives you three violations, so whoever sent it fixes their request once instead of three times.

**Paths nest.** A violation inside a list reads `roles[1]`. One inside a nested schema reads `address.postcode`. `ToDictionary()` turns the whole set into the path-to-messages shape most APIs already return.

## Where this belongs

**At the edge.** A request body, a message off a queue, a row from a file — anywhere input arrives from somewhere you do not control and has to become a domain type.

**Not inside your domain.** Past the edge, the types already say what is true. That is the whole point of getting them right at the boundary.

## Compared to a validator

If you already write FluentValidation validators and you only want a `Result` back from them, [FluentValidation](/reference/integrations/fluent-validation) is a much smaller change. It checks the object you built.

Reach for a schema when *building the object* is where your bugs come from. That is a different problem, and it is the one this solves.

## Read on

[Schemas](/reference/packages/schemas) is the reference for the package — every primitive, every rule, lists and dictionaries, and the parts that only come up once you are past the first parse.


# Async

Chain asynchronous work through Option and Result without awaiting every step.

Most operations have an async counterpart with an `Async` suffix. Use one when your transform, predicate, or side effect returns a `Task`. The full list is at the [bottom of this page](#the-full-surface).

## The receiver is the task, not the monad

This is the part worth understanding, and it is not obvious.

Most async methods are extension methods on the **task that wraps the monad**, not on the monad itself. They extend `Task<Option<T>>`, `ValueTask<Option<T>>`, `Task<Result<T, E>>` and `ValueTask<Result<T, E>>`.

That is what lets you chain without awaiting each step:

```csharp
// no intermediate awaits, one await at the end
Character character = await SummonCharacterAsync(id)
    .MapAsync(c => EnrichAsync(c))
    .UnwrapOrAsync(Commoner);
```

Without them, you would await into a local at every step:

```csharp
Option<Character> fetched = await SummonCharacterAsync(id);
Option<Character> enriched = await fetched.MapAsync(c => EnrichAsync(c));

Character character = enriched.UnwrapOr(Commoner);
```

Two things about that first sample, because both catch people out:

* **`EnrichAsync` must return `Task<Character>`.** `MapAsync` takes `Func<T, Task<TOut>>` or a plain `Func<T, TOut>`. It does not take a `ValueTask` factory.
* **`Commoner` is a value, not a function.** `UnwrapOrAsync` takes `T`, the same as `UnwrapOr`. Pass a method group and you get `CS0411`.

{% hint style="info" %}
These overloads are generated, by `Waystone.SourceGenerators`, from the synchronous methods. That is why the surface is so uniform, and why you will not find them written out in the library source.
{% endhint %}

{% hint style="info" %}
They live in `Waystone.Monads.Options.Extensions` and `Waystone.Monads.Results.Extensions`. Add the `using` for the monad you are working with.
{% endhint %}

`MatchAsync` is the exception — it also extends the monad directly, so you can pass an async branch to an `Option<T>` or `Result<T, E>` you already hold. See [Matching with an async branch](#matching-with-an-async-branch).

## Every async member returns ValueTask

From 7.0.0 the rule has no exceptions: **every** async member on `Option` and `Result` returns `ValueTask` or `ValueTask<T>`. That includes the static factories. `Option.TryAsync`, `Result.TryAsync` and `CollectAsync` returned `Task` up to 6.7.0 — see [Loud change: TryAsync and CollectAsync return ValueTask](/upgrading/v7/from-v6#loud-change-tryasync-and-collectasync-return-valuetask).

```csharp
ValueTask<string> output = result.MatchAsync(
    async x => await RenderAsync(x),
    async e => await DescribeAsync(e));
```

You await a `ValueTask` the same way you await a `Task`, so this rarely changes your code. Two things to know:

* **Await it once, and only once.** This matters if you store it before awaiting.
* **Call `.AsTask()` when you need a `Task`** — most often for `Task.WhenAll`.

```csharp
await Task.WhenAll(
    a.MapAsync(FetchAsync).AsTask(),
    b.MapAsync(FetchAsync).AsTask());
```

Both `Task` and `ValueTask` work as receivers, so a chain that mixes them still composes.

{% hint style="info" %}
`ValueTask` is cheaper when the work finishes synchronously and slightly more expensive when it does not. A three-link chain saves 144 bytes on a synchronous `Option` receiver and costs 84 bytes when the head is genuinely pending. See [v5.x to v6.x](/upgrading/older/v5-to-v6#the-measured-trade-off) for the numbers.
{% endhint %}

## Create a monad from async work

`TryAsync` captures a factory that returns a `Task` and may throw.

{% tabs %}
{% tab title="Option" %}

```csharp
Option<Character> maybeCharacter = await Option.TryAsync(
    () => SummonCharacterOrThrowAsync(id));
```

If the factory throws, the exception is caught and sent to your [configured exception logger](/guides/configuration), and you get back a `None<Character>`.

You also get a `None<Character>` if the task completes with `null`, because a `Some` cannot hold one. Nothing is logged in that case, because nothing threw.
{% endtab %}

{% tab title="Result" %}

```csharp
// supply your own error type
Result<Character, string> result = await Result.TryAsync(
    asyncFactory: () => SummonCharacterOrThrowAsync(id),
    onError: ex => ex.Message);

// or let the error type default to Error
Result<Character, Error> builtIn = await Result.TryAsync<Character>(
    () => SummonCharacterOrThrowAsync(id));
```

The single type parameter overload converts the exception with `Error.FromException`, so you do not pass an `onError` delegate.

`TryAsync` also calls `onError` when the task completes with `null`, passing you an `ArgumentNullException` that names the `asyncFactory` argument.
{% endtab %}
{% endtabs %}

{% hint style="danger" %}
**Never pass an async factory to `Try`.** The overloads that accepted one were removed in 6.0.0, and the call still compiles — it binds to the synchronous overload, gives you an `Option<Task<T>>`, and catches nothing. [`WM1011`](/reference/analyzers/runtime-bugs#wm1011) reports every occurrence. See [Silent change 1](/upgrading/older/v5-to-v6#silent-change-1-try-with-an-async-factory).
{% endhint %}

{% hint style="warning" %}
`TryAsync` lets an `OperationCanceledException` through rather than turning it into a `None` or an `Err`. See [Configuration](/guides/configuration#cancellation).
{% endhint %}

## Matching with an async branch

`MatchAsync` works on a monad you already hold, not only on a task that wraps one. Use it when one or both branches do async work.

```csharp
// both branches async
string text = await option.MatchAsync(
    async x => await RenderNumberAsync(x),
    async () => await LoadDefaultAsync());

// only the Some branch is async
string fromSome = await option.MatchAsync(
    async x => await RenderNumberAsync(x),
    () => "none");

// only the None branch is async
string fromNone = await option.MatchAsync(
    x => x.ToString(),
    async () => await LoadDefaultAsync());
```

Pick the overload that matches your branches. A branch you write as a plain value stays a plain value instead of being wrapped in a completed task, and the branch that does not match never runs.

The same three shapes work on `Task<Option<T>>` and `ValueTask<Option<T>>` receivers, so a chain reaches them too.

{% hint style="info" %}
These three arrived on the plain `Option<T>` receiver in 7.0.0. Before that they existed only on the `Task` and `ValueTask` receivers, so matching an `Option<T>` you already held meant wrapping it in `Task.FromResult` first.
{% endhint %}

### Option and Result cover different combinations

This catches people out, so check the table before you write the call:

| Branches                      | `Option<T>` | `Result<T, E>` |
| ----------------------------- | ----------- | -------------- |
| Both async, returning a value | Yes         | Yes            |
| One async, returning a value  | Yes         | **No**         |
| Async, returning nothing      | **No**      | Yes            |

The middle row is the one that bites. This does not compile:

```csharp
// does not compile
ValueTask<string> output = result.MatchAsync(
    async x => await RenderAsync(x),
    e => e.ToString());
```

There is no `Result` overload taking one async branch and one plain branch that returns a value, so the call binds to the overload returning nothing. You get `CS0029` on the assignment, which points at the line but not at the cause. Make both branches async, or match on an `Option<T>`.

## End a chain

The consuming operations have async counterparts too, so you can finish a chain without awaiting the monad first.

{% tabs %}
{% tab title="Option" %}

```csharp
Character character = await SummonCharacterAsync(id).UnwrapAsync();
Character orCommoner = await SummonCharacterAsync(id).UnwrapOrAsync(Commoner);
Character? orDefault = await SummonCharacterAsync(id).UnwrapOrDefaultAsync();
Character expected = await SummonCharacterAsync(id).ExpectAsync("the character must exist");
```

{% endtab %}

{% tab title="Result" %}

```csharp
Character character = await LoadCharacterAsync(id).UnwrapAsync();
Character orCommoner = await LoadCharacterAsync(id).UnwrapOrAsync(Commoner);
Character? orDefault = await LoadCharacterAsync(id).UnwrapOrDefaultAsync();
Character expected = await LoadCharacterAsync(id).ExpectAsync("the character must exist");

Error error = await LoadCharacterAsync(id).UnwrapErrAsync();
Error expectedErr = await LoadCharacterAsync(id).ExpectErrAsync("the load must fail");
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
**`UnwrapOrDefaultAsync` returns `T?`, not `T`.** Assign it to a nullable local. With nullable reference types on, `Character orDefault = …` is `CS8600`.
{% endhint %}

{% hint style="info" %}
`UnwrapAsync`, `UnwrapErrAsync`, `ExpectAsync` and `ExpectErrAsync` throw for the same reasons their synchronous versions do. See [Exceptions](/guides/exceptions).
{% endhint %}

## The full surface

Every method below behaves exactly like the synchronous version documented in [Option\<T>](/guides/option) and [Result\<T, E>](/guides/result). The only difference is that it accepts an async delegate, a task receiver, or both.

{% hint style="info" %}
Some of these take state, so the delegate does not have to capture. Not all of them do yet — see [On the async surface](/reference/state-overloads#on-the-async-surface).
{% endhint %}

### Option\<T>

| Category           | Methods                                                                                                  |
| ------------------ | -------------------------------------------------------------------------------------------------------- |
| Transform          | `MapAsync`, `MapOrAsync`, `MapOrDefaultAsync`, `MapOrElseAsync`, `AndThenAsync`                          |
| State checks       | `IsSomeAndAsync`, `IsNoneOrAsync`                                                                        |
| Consume            | `MatchAsync`, `UnwrapAsync`, `UnwrapOrAsync`, `UnwrapOrElseAsync`, `UnwrapOrDefaultAsync`, `ExpectAsync` |
| Side effect        | `InspectAsync`                                                                                           |
| Filter and combine | `FilterAsync`, `ZipWithAsync`, `OrElseAsync`                                                             |
| Nesting            | `FlattenAsync`                                                                                           |
| Conversion         | `OkOrAsync`, `OkOrElseAsync`                                                                             |

### Result\<T, E>

| Category          | Methods                                                                                                                                      |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Transform         | `MapAsync`, `MapErrAsync`, `MapOrAsync`, `MapOrDefaultAsync`, `MapOrElseAsync`                                                               |
| State checks      | `IsOkAndAsync`, `IsErrAndAsync`                                                                                                              |
| Consume           | `MatchAsync`, `UnwrapAsync`, `UnwrapErrAsync`, `UnwrapOrAsync`, `UnwrapOrElseAsync`, `UnwrapOrDefaultAsync`, `ExpectAsync`, `ExpectErrAsync` |
| Side effect       | `InspectAsync`, `InspectErrAsync`                                                                                                            |
| Logical operators | `AndThenAsync`, `OrElseAsync`                                                                                                                |
| Nesting           | `FlattenAsync`                                                                                                                               |


# Configuration

This library has a few configurable behaviours. You set them through `MonadOptions.Configure`, once, at start-up. Configure each option below *once* in your application's lifetime.

**If your application has a dependency injection container, configure the library there instead.** Install [Waystone.Monads.Extensions.Hosting](/reference/integrations/hosting) on a host, or [Waystone.Monads.Extensions.DependencyInjection](/reference/integrations/dependency-injection) without one, and the container writes these settings for you. You get a delegate that can resolve services out of the container, optional binding from `IConfiguration`, and a diagnostic event when configuration was registered but never installed.

Reach for `MonadOptions.Configure` when there is no container — a library, a test, or a small console application. Reach for a [scope](#scoped-configuration) when one region of code needs different settings. Every `Use…` method below is the same one on all three routes.

```csharp
MonadOptions.Configure(options => options
    .UseFallbackErrorCode("Unknown")
    .UseFallbackErrorMessage("Something went wrong."));
```

## How configuration works

You do not hold options. You describe them, and the library publishes the result.

`Configure` hands your callback a **`MonadOptionsBuilder`**. Every `Use…` method is on the builder, and each returns the builder so you can chain. When your callback returns, the library builds an immutable `MonadOptions` from it and swaps that in as the options the whole process reads.

Three consequences, all of which matter in practice.

**There is nothing to read.** `MonadOptions` exposes no public property, accessor or instance member — only the two statics `Configure` and `BeginScope`. Nothing in your code can inspect the current configuration, and nothing needs to.

**A reader never sees a half-configured state.** The swap is atomic. Code running on another thread reads either the options from before your `Configure` call or the ones from after it, never a mixture. Before 7.0.0, configuration mutated a shared object in place, so a concurrent reader could see one setting applied and the next not.

{% hint style="danger" %}
**Do not keep the builder.** It is authoring state, not the published options. Calls you make on it after your callback has returned are discarded — no exception, no warning, nothing takes effect.

```csharp
// Wrong. The second call does nothing.
MonadOptionsBuilder? stashed = null;
MonadOptions.Configure(options => stashed = options.UseFallbackErrorCode("A"));
stashed!.UseFallbackErrorMessage("B");
```

Put every `Use…` call inside the callback.
{% endhint %}

### If you are upgrading from 6.x

The common call shape is unchanged, because the lambda parameter's type is inferred:

```csharp
MonadOptions.Configure(options => options.UseFallbackErrorCode("Unknown"));
```

That compiled against 6.x and it compiles against 7.0.0. You do not have to touch call sites that already build.

Three things do change:

* An **explicitly typed** lambda parameter breaks. `(MonadOptions options) =>` becomes `(MonadOptionsBuilder options) =>`, or drop the annotation and let it infer.
* A **field, parameter, local or property typed `MonadOptions`** has no replacement. Configuration is reachable only inside a `Configure` or `BeginScope` callback now, so move the `Use…` calls into one rather than passing options around.
* In the satellite packages, **`MonadOptionsExtensions` is now `MonadOptionsBuilderExtensions`**. A `using static` or a qualified static call naming the old class needs updating. A normal extension call on the callback's parameter, the usual shape, needs nothing.

The [6.x to 7.0.0 upgrade page](/upgrading/v7/from-v6) covers this with the compiler diagnostics you will see.

## Logging

This library catches exceptions in several places and turns them into non-throwing types. To see those exceptions, install `Waystone.Monads.Extensions.Logging` and hand the library your logger once:

```csharp
MonadOptions.Configure(options => options.UseLoggerFactoryFrom(app.Services));
```

Use `UseLoggerFactory(factory)` if you have no service provider, or `UseLogger(logger)` if you already hold a logger.

Each entry carries the exception plus the call site that caught it — the member name, the source text of the delegate you passed, and the line number.

The three methods above ship in that package, and [Logging](/reference/packages/logging) covers them properly — the levels, the category, and what lands in each entry.

To count these exceptions instead, install nothing at all. Read [Observability](/guides/observability) for the signals that need no package.

{% hint style="warning" %}
**`UseExceptionLogger` is gone.** It was obsolete from 6.7.0 and 7.0.0 removes it. It took a delegate you wrote yourself and held only one, so a second integration silently replaced the first. Install the package and call one of the three methods above instead. See [Deprecations](/upgrading/deprecations#seeing-handled-exceptions-through-a-hand-written-delegate).
{% endhint %}

{% hint style="info" %}
**The library also writes to the console whenever a debugger is attached**, whether or not you configure a logger. It prints the exception, the call site and the argument expression, then reports it through the signals above as well. This is a debugging aid, so it costs nothing in a normal run — but do not read a console message as proof that your logger ran.
{% endhint %}

## Cancellation

`Option.Try`, `Option.TryAsync`, `Result.Try` and `Result.TryAsync` catch the exceptions your factory throws and turn them into a `None` or an `Err`. From 6.0.0 they make one exception to that: an `OperationCanceledException` propagates to your caller instead.

Cancelling is you telling the work to stop. It is not the work failing, and turning it into a `None` leaves the caller unable to tell "cancelled" from "genuinely absent".

`TaskCanceledException` inherits from `OperationCanceledException`, so it propagates too.

If you need the pre-6.0.0 behaviour, opt back in:

```csharp
MonadOptions.Configure(options => options.UseCancellationAsFailure());
```

A cancellation is then caught, counted and logged like any other handled exception, and becomes a `None` or an `Err` as it did before.

`UseCancellationAsFailure` takes an optional `bool`, which defaults to `true`, so the call above turns the behaviour on. Pass `false` to put it back:

```csharp
MonadOptions.Configure(options => options.UseCancellationAsFailure(false));
```

You need that only when something earlier already turned it on and you want to undo it — a container registration from a library, or a `Configure` call elsewhere in start-up. A builder inherits the settings in effect when it was handed to you, so passing `false` is the only way to reverse the decision.

{% hint style="info" %}
We recommend leaving this off. It exists so that upgrading to 6.0.0 does not force you to rewrite every call site at once. Scope it with [`MonadOptions.BeginScope`](#scoped-configuration) if only part of your code needs it.
{% endhint %}

## Error Code Generation

There are a few factory methods included in the library for generating `ErrorCode` instances from `Enum` and from `Exception` instances. To customise how these error codes are generated, create a class inheriting from `ErrorCodeFactory` and override the methods you wish to customise. Then create an instance and pass it into the `MonadOptions` instance via the `UseErrorCodeFactory`.

```csharp
internal sealed class ShoutingErrorCodeFactory : ErrorCodeFactory
{
    public override ErrorCode FromException(Exception exception) =>
        new(exception.GetType().Name.ToUpperInvariant());
}
```

Register your instance once, at start-up:

```csharp
MonadOptions.Configure(
    options => options.UseErrorCodeFactory(new ShoutingErrorCodeFactory()));
```

{% hint style="warning" %}
**`FromEnum` is no longer one of the methods to override.** It is obsolete from 6.2.0 and removed in 7.0.0, because a factory runs too late for the compiler, the analyzers or the error code registry to see what it returns. Shape enum codes with `[ErrorCodeCatalog(Format = "…")]` instead — see [Code format language](/reference/source-generation/code-format) — and keep the factory for `FromException`, which is unaffected.
{% endhint %}

## Error Code and Message Fallbacks

There may be exception circumstances which cause the `string` used to create the `ErrorCode` or the message of the `Error` classes to be null or white-space. In these situations, a set of fallbacks are used. These fallbacks can be configured.

```csharp
MonadOptions.Configure(options => options
    .UseFallbackErrorCode("unknown")                     // default: Unspecified
    .UseFallbackErrorMessage("Something went wrong!"));  // default: An unexpected error occurred.
```

**The substitution is silent.** `new ErrorCode(code)` and `new Error(code, message)` trim what you pass, and swap in the fallback when the result is empty. Neither throws, and nothing is logged. So an `Error` whose message reads `An unexpected error occurred.` is telling you a call site passed a blank message, not that the library hit an unexpected error. Pass a real message at every call site — a fallback says nothing about what actually failed.

Both configuration methods reject a blank argument themselves. `UseFallbackErrorCode` and `UseFallbackErrorMessage` throw an `ArgumentException` when you pass null, empty or whitespace, because a fallback that is itself unusable would leave nothing to fall back to.

## Scoped Configuration

Use `MonadOptions.BeginScope` when you want different options for one region of code — a single request, a test, or a block you are debugging. The scope applies until you dispose it, and your global configuration is untouched.

```csharp
using (MonadOptions.BeginScope(options => options.UseFallbackErrorCode("Debug")))
{
    Result<int, Error> result = Result.Try<int>(() => int.Parse(input));

    _ = result;
}

// out here, your global configuration applies again
```

A scope accepts the same configuration methods as `Configure`, so it can override any option:

```csharp
using (MonadOptions.BeginScope(options => options
           .UseErrorCodeFactory(new ShoutingErrorCodeFactory())
           .UseFallbackErrorMessage("Something went wrong while debugging.")))
{
    // ...
}
```

### What a scope does

* **Inherits what you do not set.** Options you leave alone keep the values they had when the scope opened.
* **Takes a snapshot.** Calling `Configure` while a scope is open does not change that scope. The new global value applies once the scope ends. This is not new in 7.0.0 — a scope has always held its own copy.
* **Nests.** Disposing the innermost scope restores the scope around it.
* **Restores only from the inside out.** A scope that is no longer the innermost one declines to restore anything when you dispose it, and reports itself instead. See below.
* **Isolates concurrent work.** A scope applies to the current asynchronous flow, so parallel work each sees its own options. This makes scopes safe to use in tests that run in parallel.

{% hint style="info" %}
A scope affects work you start inside it. It does not affect work that was already running when you opened the scope.
{% endhint %}

{% hint style="warning" %}
**Dispose scopes in the reverse of the order you opened them.** A `using` block does this for you. Nothing else guarantees it.
{% endhint %}

#### What happens when you dispose out of order

Since 7.0.0, a scope restores only when it is the innermost one still open. Disposing it at any other time changes nothing and reports the mistake.

`Dispose` looks at the options in effect on the current flow and picks one of three paths:

| What it finds                                                        | What it does                                                     |
| -------------------------------------------------------------------- | ---------------------------------------------------------------- |
| The options this scope installed, so this scope is the innermost one | Restores what came before it                                     |
| The options this scope restored to, so it has already been disposed  | Nothing, silently                                                |
| Anything else                                                        | Nothing, and writes a `ScopeDisposedOutOfOrder` diagnostic event |

It never throws, on any path.

**The third path leaves the early-disposed scope's options in effect.** They stay live until the inner scope that is still open is disposed, which then restores them as *its own* predecessor — so those options outlive the scope that installed them. That is the bug the event exists to tell you about.

Two more cases take the third path, both worth knowing:

* Disposing a scope from a different asynchronous flow than the one that opened it. A scope lives in the flow, so another flow's `Dispose` never sees it.
* Disposing a `default(MonadOptionsScope)`. Before 7.0.0 this dropped the flow back to your global configuration; now it reports like any other out-of-order disposal.

**Repeated disposal on the third path reports every time.** A scope that has already declined cannot remember that it did, because it is a readonly struct, so each further `Dispose` writes the event again. Deduplicate in your subscriber if that matters. The "harmless twice" promise covers the restoring path only.

To see these events, see [Watching for a scope disposed out of order](/guides/observability#watching-for-a-scope-disposed-out-of-order).

{% hint style="info" %}
[`Waystone.Monads.FluentValidation`](/reference/integrations/fluent-validation) options are covered by the same scope, so you only ever open one.
{% endhint %}


# Observability

See the exceptions the library swallows, with nothing extra installed.

`Option.Try` and `Result.Try` catch the exception your factory throws and hand you back a `None` or an `Err`. That is the point of them. But it means the exception never reaches you, and in the `Option` case it is gone for good.

This page shows you how to see those exceptions anyway.

**Everything on this page works with no extra package.** From 6.7.0 the library reports on sources named after itself, and your pipeline finds them by name.

| Signal            | What you get                                                            |
| ----------------- | ----------------------------------------------------------------------- |
| Metrics           | A count of handled exceptions, tagged by exception type                 |
| Diagnostic events | Three events you can subscribe to, including two configuration mistakes |

Logs are the exception, and they are a separate page. See [Logging](/reference/packages/logging).

{% hint style="info" %}
**Why metrics need no package and logs do.** Your metrics pipeline discovers meters by name, so we publish one and you name it. `Microsoft.Extensions.Logging` has no equivalent — there is no ambient logger to find. You have to hand us one.
{% endhint %}

## Count handled exceptions

Add `Waystone.Monads` to the meters your pipeline already collects. That is the whole setup.

```csharp
builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics.AddMeter("Waystone.Monads"));
```

Prometheus, Datadog and anything else that reads .NET meters work the same way — none of them needs a Waystone package.

You now get one instrument:

| Instrument                           | Type            | Unit          |
| ------------------------------------ | --------------- | ------------- |
| `waystone.monads.exceptions_handled` | `Counter<long>` | `{exception}` |

It carries two tags:

| Tag                     | Values                                                           | What it tells you                                                      |
| ----------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `error.type`            | The exception's full type name, such as `System.FormatException` | Which exception. This is the OpenTelemetry attribute of the same name. |
| `waystone.monads.monad` | `option` or `result`                                             | Whether the exception is gone or survived                              |

That second tag matters more than it looks. An exception counted as `option` was discarded — this counter is the only record that it happened. One counted as `result` also went into the `Err`, so your error handling still has it.

{% hint style="info" %}
**Exception types are safe to tag with.** Any one application throws a handful of types, so `error.type` will not blow up your metrics backend's cardinality budget.
{% endhint %}

## Subscribe to an event

Skip this unless you are building your own integration. The logging package already does it for you.

The library writes three events to a `DiagnosticListener` named `Waystone.Monads`. `MonadDiagnostics` gives you a token for each one. A token pairs the event's name with the type of payload it carries:

| Token                                           | Payload                   |
| ----------------------------------------------- | ------------------------- |
| `MonadDiagnostics.ExceptionHandledEvent`        | `ExceptionHandled`        |
| `MonadDiagnostics.ScopeDisposedOutOfOrderEvent` | `ScopeDisposedOutOfOrder` |
| `MonadDiagnostics.ConfigurationNotAppliedEvent` | `ConfigurationNotApplied` |

Call `Subscribe` on the token. Your callback gets the payload directly:

```csharp
using System;
using Waystone.Monads.Diagnostics;

IDisposable watching = MonadDiagnostics.ExceptionHandledEvent.Subscribe(
    handled =>
    {
        // handled.Exception, handled.Caller, handled.Monad
    });
```

The `ExceptionHandled` payload is:

```csharp
public sealed record ExceptionHandled(
    Exception Exception,
    CallerInfo Caller,
    MonadKind Monad);
```

Subscribe once, at start-up. It does not matter whether you subscribe before or after the first `Try` runs.

Use the token rather than the name constants. Typing a name by hand gives you three ways to get it wrong: the listener name, the event name, and the payload type. Every one of them fails silently. You get no exception, no warning, and an empty dashboard. The token cannot point at the wrong event.

### Disposing the subscription

Dispose the return value to detach. Disposing it twice is safe.

A subscriber that runs for the life of your application can be left alone. Anything shorter-lived must be disposed, or it leaks an observer for the rest of the process.

{% hint style="danger" %}
**Your subscriber runs on the thread that wrote the event, synchronously.** For `ExceptionHandledEvent` that is the throwing thread, inside the `catch`. Two consequences you have to plan for:

* Slow work in the subscriber delays the caller waiting for its `None` or `Err`.
* An exception thrown from your subscriber escapes the `Try` that was supposed to swallow the original one. We do not catch it for you. That is deliberate — it is how you make one of these events fatal in a test suite.

Queue the work and return.
{% endhint %}

### Without the token

You never need a Waystone package to observe the library, and that has not changed. The token is a shortcut over the standard `DiagnosticListener` API, not a replacement for it. Here is the same subscription written by hand:

```csharp
using System.Diagnostics;
using Waystone.Monads.Diagnostics;

public sealed class MonadWatcher : IObserver<DiagnosticListener>
{
    public void OnNext(DiagnosticListener listener)
    {
        if (listener.Name != MonadDiagnostics.ListenerName)
        {
            return;
        }

        listener.Subscribe(
            new HandledExceptions(),
            name => name == MonadDiagnostics.ExceptionHandledEventName);
    }

    public void OnCompleted() { }
    public void OnError(Exception error) { }
}

public sealed class HandledExceptions : IObserver<KeyValuePair<string, object?>>
{
    public void OnNext(KeyValuePair<string, object?> written)
    {
        if (written.Key == MonadDiagnostics.ExceptionHandledEventName
         && written.Value is ExceptionHandled handled)
        {
            // handled.Exception, handled.Caller, handled.Monad
        }
    }

    public void OnCompleted() { }
    public void OnError(Exception error) { }
}
```

Hook it up once, at start-up:

```csharp
DiagnosticListener.AllListeners.Subscribe(new MonadWatcher());
```

`AllListeners` replays listeners that already exist, so it does not matter whether you subscribe before or after the first `Try` runs.

Two traps to handle yourself if you go this way:

* **`DiagnosticListener.Write` ignores your predicate.** The predicate only decides what `IsEnabled` reports. Your observer receives every event written to that listener, so check `written.Key` yourself — as the sample above does.
* **The payload arrives as `object?`.** Test its type rather than casting it.

## Watching for a scope disposed out of order

The library writes a second event, `Waystone.Monads.ScopeDisposedOutOfOrder`, to the same listener. It fires when a `MonadOptionsScope` is disposed at a point where it is not the innermost open scope, which means nothing was restored — see [What happens when you dispose out of order](/guides/configuration#what-happens-when-you-dispose-out-of-order).

The payload is:

```csharp
public sealed record ScopeDisposedOutOfOrder(
    MonadOptions? Scope,
    MonadOptions? Live);
```

* `Scope` is what the disposed scope had installed. It is `null` exactly when a `default(MonadOptionsScope)` was disposed, because that scope was never begun.
* `Live` is what is in effect instead. It is `null` when no scope remains open.

Subscribe the same way you subscribe to the exception event:

```csharp
IDisposable watching = MonadDiagnostics.ScopeDisposedOutOfOrderEvent.Subscribe(
    disposed =>
    {
        // disposed.Scope is gone; disposed.Live is what is in effect instead.
    });
```

**There is no caller information in the payload.** `IDisposable.Dispose()` takes none, so the library has nothing to pass. Your subscriber does run synchronously inside `Dispose`, on the disposing thread, so capturing a stack trace there names the offending call site — which is the reason to write one of these at all. This is a bug in your own code that the library cannot fix for you, and the event is how you find it.

**Expect duplicates.** A scope that has already declined to restore reports again on every further `Dispose`, because a readonly struct cannot record that it reported. If you alert on this, deduplicate.

## Watching for configuration that was never installed

The library writes a third event, `Waystone.Monads.ConfigurationNotApplied`, to the same listener. It fires when something reads `MonadOptions` after configuration has been registered but before it has been installed — in practice, when an application called `AddWaystoneMonads` and never called `UseWaystoneMonads`. See [Dependency injection and hosting](/reference/integrations/dependency-injection#forgetting-the-install).

The payload carries no data:

```csharp
public sealed record ConfigurationNotApplied;
```

There is nothing useful to put in it. The event's whole meaning is that it fired at all, and the read that triggered it was answered from the defaults.

**The signal is held, not spent, while nobody is listening.** If no subscriber is attached when the first early read happens, the library keeps the flag set, so a subscriber attached later still receives it.

**Configuration arriving by any route disarms it** — `UseWaystoneMonads`, the host install, or a plain `MonadOptions.Configure` call — whether or not the event was ever written.

**Expect it once per process, but do not rely on it.** Writing the event disarms the flag, so later reads write nothing. Two threads reading at the same moment can each write before either disarms it. The payloads are identical and carry no data, so deduplicate in your subscriber if that matters.

**It reports reads, not registrations.** `AddWaystoneMonads` on its own writes nothing. The event needs something to actually read the options in the window between registration and install. An application that registers, never reads early, and never installs gets no event — and no wrong behaviour either, because nothing consulted the options.

In a test suite, subscribe and throw to make the omission fatal. We do not catch what your callback throws:

```csharp
using IDisposable watching =
    MonadDiagnostics.ConfigurationNotAppliedEvent.Subscribe(
        _ => throw new InvalidOperationException(
            "AddWaystoneMonads was called but UseWaystoneMonads was not."));
```

{% hint style="info" %}
**You will not see this event unless you use the dependency injection package.** Nothing else in the library marks configuration as pending, so an application that calls `MonadOptions.Configure` directly never triggers it.
{% endhint %}

## What the library does not report

**Exceptions it lets through.** The metric and the `ExceptionHandled` event fire only when `Try` or `TryAsync` catches something. An exception that propagates to you is yours to log. `ScopeDisposedOutOfOrder` is unrelated to `Try` and has no counter — it reports a misuse of configuration, not a failure in your work.

**Cancellations, by default.** From 6.0.0 an `OperationCanceledException` is not caught, so nothing counts or logs it. Call [`UseCancellationAsFailure`](/guides/configuration#cancellation) and it becomes an ordinary caught exception, counted and logged like any other.

**Traces.** The library publishes no `ActivitySource`. It creates no spans of its own, and OpenTelemetry's conventions no longer recommend recording an exception that gets handled and never escapes a span — which is exactly what these exceptions are. If you want an `Err` marked on a span you own, do it at the call site you chose:

```csharp
result.InspectErr(
    error => Activity.Current?.SetStatus(
        ActivityStatusCode.Error,
        error.Message));
```

## You pay nothing when nobody is listening

Every signal the library publishes checks whether anything is subscribed before it does any work. With no listener attached, a `Try` that throws allocates exactly what it allocated before 6.7.0 — measured, not assumed. Attach both and you pay 40 bytes per handled exception, which is the event payload; the counter allocates nothing at all.

`ScopeDisposedOutOfOrder` is gated the same way, so an out-of-order disposal in a process with no listener allocates no payload either. It also costs nothing on the normal path — the check runs only once `Dispose` has already decided it cannot restore.

## These names are a contract

Dashboards and alert rules bind to strings, and no compiler warns you when a string changes. So treat every name the library publishes the way you treat a public type:

| Thing                | Name                                      |
| -------------------- | ----------------------------------------- |
| Meter                | `Waystone.Monads`                         |
| `DiagnosticListener` | `Waystone.Monads`                         |
| Event                | `Waystone.Monads.ExceptionHandled`        |
| Event                | `Waystone.Monads.ScopeDisposedOutOfOrder` |
| Event                | `Waystone.Monads.ConfigurationNotApplied` |
| Counter              | `waystone.monads.exceptions_handled`      |
| Tags                 | `error.type`, `waystone.monads.monad`     |
| Log category         | `Waystone.Monads`                         |

`MonadDiagnostics` holds every one of them as a constant, so you never have to type one out. Use the constants for anything that names a signal — a dashboard query, a log line, a hand-written subscription. To subscribe, use the event tokens instead: they name the event for you and fix the payload type at the same time.

We will not rename these names outside a major release, and we will tell you in [Deprecations](/upgrading/deprecations) when we do.


# Coming from Rust

Waystone.Monads ports Rust's `std::option::Option` and `std::result::Result`. If you already know those types, most of what you know carries over. This page covers the parts that do not.

Read it for three things:

* the name a Rust member has here
* the four behaviours that differ, not just in spelling
* what this library adds that Rust has no counterpart for

## Option

Most members keep their meaning and change only their casing.

| Rust                          | Waystone          | Note                                                                                          |
| ----------------------------- | ----------------- | --------------------------------------------------------------------------------------------- |
| `is_some`                     | `IsSome`          | A property, not a method                                                                      |
| `is_none`                     | `IsNone`          | A property, not a method                                                                      |
| `is_some_and`                 | `IsSomeAnd`       |                                                                                               |
| `is_none_or`                  | `IsNoneOr`        |                                                                                               |
| `expect`                      | `Expect`          |                                                                                               |
| `unwrap`                      | `Unwrap`          |                                                                                               |
| `unwrap_or`                   | `UnwrapOr`        |                                                                                               |
| `unwrap_or_else`              | `UnwrapOrElse`    |                                                                                               |
| `unwrap_or_default`           | `UnwrapOrDefault` | Behaves differently — see [UnwrapOrDefault is riskier here](#unwrapordefault-is-riskier-here) |
| `map`                         | `Map`             |                                                                                               |
| `map_or`                      | `MapOr`           |                                                                                               |
| `map_or_else`                 | `MapOrElse`       |                                                                                               |
| `inspect`                     | `Inspect`         |                                                                                               |
| `ok_or`                       | `OkOr`            |                                                                                               |
| `ok_or_else`                  | `OkOrElse`        |                                                                                               |
| `and`                         | `And`             |                                                                                               |
| `and_then`                    | `AndThen`         |                                                                                               |
| `filter`                      | `Filter`          |                                                                                               |
| `or`                          | `Or`              |                                                                                               |
| `or_else`                     | `OrElse`          |                                                                                               |
| `xor`                         | `Xor`             |                                                                                               |
| `zip`                         | `Zip`             |                                                                                               |
| `zip_with`                    | `ZipWith`         |                                                                                               |
| `unzip`                       | `Unzip`           |                                                                                               |
| `Option::flatten`             | `Flatten`         | Collapses a nested `Option<Option<T>>`                                                        |
| `Iterator::flatten`           | `Flatten`         | On a sequence of `Option<T>` — drops the `None`s                                              |
| `transpose`                   | `Transpose`       |                                                                                               |
| `iter`                        | `AsEnumerable`    | Renamed to the .NET convention                                                                |
| `collect::<Option<Vec<T>>>()` | `Collect`         | On a sequence of `Option<T>`                                                                  |
| `match`                       | `Match`           | A method, not a language expression                                                           |

## Result

| Rust                             | Waystone          | Note                                                                                          |
| -------------------------------- | ----------------- | --------------------------------------------------------------------------------------------- |
| `is_ok`                          | `IsOk`            | A property, not a method                                                                      |
| `is_err`                         | `IsErr`           | A property, not a method                                                                      |
| `is_ok_and`                      | `IsOkAnd`         |                                                                                               |
| `is_err_and`                     | `IsErrAnd`        |                                                                                               |
| `ok`                             | `GetOk`           | Renamed, since `Ok` is the case type                                                          |
| `err`                            | `GetErr`          | Renamed, since `Err` is the case type                                                         |
| `expect`                         | `Expect`          |                                                                                               |
| `expect_err`                     | `ExpectErr`       |                                                                                               |
| `unwrap`                         | `Unwrap`          |                                                                                               |
| `unwrap_err`                     | `UnwrapErr`       |                                                                                               |
| `unwrap_or`                      | `UnwrapOr`        |                                                                                               |
| `unwrap_or_else`                 | `UnwrapOrElse`    |                                                                                               |
| `unwrap_or_default`              | `UnwrapOrDefault` | Behaves differently — see [UnwrapOrDefault is riskier here](#unwrapordefault-is-riskier-here) |
| `map`                            | `Map`             |                                                                                               |
| `map_err`                        | `MapErr`          |                                                                                               |
| `map_or`                         | `MapOr`           |                                                                                               |
| `map_or_else`                    | `MapOrElse`       |                                                                                               |
| `inspect`                        | `Inspect`         |                                                                                               |
| `inspect_err`                    | `InspectErr`      |                                                                                               |
| `and`                            | `And`             |                                                                                               |
| `and_then`                       | `AndThen`         |                                                                                               |
| `or`                             | `Or`              |                                                                                               |
| `or_else`                        | `OrElse`          |                                                                                               |
| `transpose`                      | `Transpose`       |                                                                                               |
| `iter`                           | `AsEnumerable`    | Renamed to the .NET convention                                                                |
| `collect::<Result<Vec<T>, E>>()` | `Collect`         | On a sequence of `Result<TOk, TErr>`                                                          |
| `match`                          | `Match`           | A method, not a language expression                                                           |

`ok()` and `err()` are the renames that catch people out. Searching for `Ok` and `Err` finds the case types instead, so reach for `GetOk` and `GetErr`.

## What behaves differently

Four things differ beyond the name. The first two exist because C# has references and Rust does not.

### There is a fourth state: null

Rust's `Option<T>` is a value type. It cannot be uninitialised.

Here, `Option<T>` and `Result<TOk, TErr>` are reference types. `default(Option<T>)` is null, and calling anything on it throws a `NullReferenceException`. Your Rust instincts will not warn you about this, because the state does not exist there.

The analyzer covers it. `WM1003` reports `default` on either type, and `WM1002` reports null assigned to one. See [Analyzers](/reference/analyzers).

### Null is rejected at run time, not compile time

`Option.Some(null)` and `Result.Ok<T, E>(null)` throw `ArgumentNullException` when you construct them. In Rust you cannot write the equivalent at all.

This moves a whole class of mistake from compile time to run time. Nullable reference types narrow the gap but do not close it, because they are annotations rather than guarantees.

### UnwrapOrDefault is riskier here

Rust gates `unwrap_or_default` behind a `T: Default` bound. You opt in by implementing the trait.

In C#, `default(T)` always exists. On a value type an absent value silently becomes `0`, `false`, or `Guid.Empty`, and nothing in the signature warns you. A missing count and a real count of zero look identical afterwards.

Two things help:

* `WM2015` reports `UnwrapOrDefault` and `MapOrDefault` on a value type.
* `UnwrapOrNull` and `MapOrNull` return `null` instead, so the absent case stays distinguishable.

Prefer `UnwrapOrNull` on value types unless you genuinely want the default.

### `match` becomes a method, and it is the only exhaustive option

Use `Match` wherever you would reach for Rust's `match`. It is the only way to consume either type exhaustively.

C#'s exhaustiveness check cannot see that the hierarchy is closed. The `internal` member that stops anything outside the assembly deriving from `Option<T>` is invisible to it, so a `switch` expression covering both cases still reports `CS8509` and asks for a `_` arm you can never reach. `Match` takes exactly two branches, both required, and warns about nothing.

### `if let Some(x)` becomes a positional pattern

From 7.0.0 the case types deconstruct, so the closest thing to Rust's `if let` is:

| Rust                      | Waystone                                                            |
| ------------------------- | ------------------------------------------------------------------- |
| `if let Some(x) = option` | `if (option is Some<T>(var x))`                                     |
| `if let Ok(x) = result`   | `if (result is Ok<TOk, TErr>(var x))`                               |
| `if let Err(e) = result`  | `if (result is Err<TOk, TErr>(var e))`                              |
| `if let None = option`    | `if (option is None<T>)` — no parentheses; there is nothing to bind |

Reach for these in statement position, where Rust would use `if let`. Reach for `Match` where Rust would use `match`. See [Pattern matching with Deconstruct](/reference/option/consume#pattern-matching-with-deconstruct).

Do not name a case type in a declaration either. A variable, parameter or return typed as `Some<T>` can hold only one of the two states, which defeats the point. `WM2011` reports it and points you at the base type.

## What is not ported

| Rust                                                               | Why                                                                             |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
| `?` operator                                                       | A language feature. Nothing a library can supply. Chain `AndThen` instead.      |
| `unwrap_unchecked`, `unwrap_err_unchecked`                         | C# has no unsafe-contract idiom to hang it on.                                  |
| `contains`                                                         | Unstable in Rust too. `IsSomeAnd(x => x == value)` does the same job.           |
| `take`, `replace`, `insert`, `get_or_insert`, `as_mut`, `iter_mut` | These mutate in place. Both types here are immutable records.                   |
| `as_ref`, `as_deref`, `as_slice`, `copied`, `cloned`               | Borrow and ownership projections. C# reference semantics make them unnecessary. |

## What Waystone adds

These have no Rust counterpart. Do not go looking for the original.

| Member                                                | What it does                                                                                                |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `Reduce`                                              | Merges two `Option<T>` of the same type                                                                     |
| `FromNullable`                                        | Builds an `Option<T>` from a `T?`                                                                           |
| `UnwrapOrNull`, `MapOrNull`                           | Return `null` rather than `default` on value types                                                          |
| `MapOrDefault`                                        | `MapOr` with `default(T)` as the fallback                                                                   |
| `FirstOrNone`, `LastOrNone`, `FirstOr`, `FirstOrElse` | Predicate searches over a sequence of `Option<T>`                                                           |
| `Map`, `Filter` over sequences                        | Apply a transform or predicate to every element in place                                                    |
| `Flatten`, `FlattenErr` on a sequence of `Result`     | One side of the sequence, dropping the other. `Result::flatten` is still unstable in Rust.                  |
| `Partition`                                           | Splits a sequence into successes and failures — close to itertools' `partition_result`                      |
| `Try`, `TryAsync`                                     | Run a delegate and turn a thrown exception into a `None` or an `Err`                                        |
| The `*Async` surface                                  | Every operation over a `Task` or `ValueTask` receiver                                                       |
| The state overloads                                   | Pass a captured value as an argument so the delegate allocates no closure                                   |
| `Deconstruct` on the case types                       | Positional patterns, so `if let Some(x)` has a C# spelling                                                  |
| `MonadOptions`                                        | Global configuration — see [Configuration](/guides/configuration)                                           |
| `Select`, `SelectMany`, `Where`                       | C# query syntax over a monad, in a companion package — see [Waystone.Monads.Linq](/reference/packages/linq) |


# Option\<T> API

Every method on Option\<T>, grouped by what it does.

`Option<T>` holds either a value (`Some<T>`) or nothing (`None<T>`). It has no third state, and it cannot hold `null`.

This is the lookup. If you are learning the type rather than looking a method up, read the [Option\<T> guide](/guides/option) first.

## Creation

[Full page →](/reference/option/creation)

| Method                       | What it does                                                            |
| ---------------------------- | ----------------------------------------------------------------------- |
| `Option.Some(value)`         | Wraps a value. Throws on `null`.                                        |
| `Option.None<T>()`           | The empty case.                                                         |
| `Option.FromNullable(value)` | `Some` if the value is not `null`, `None` if it is.                     |
| `Option.Try(factory)`        | Runs a factory that might throw. `None` if it throws or returns `null`. |
| `Option.TryAsync(factory)`   | The same, for a factory returning a `Task`.                             |

## Transform

[Full page →](/reference/option/transform)

| Method    | What it does                                                    |
| --------- | --------------------------------------------------------------- |
| `Map`     | Changes the value if there is one.                              |
| `AndThen` | Chains a step that itself returns an `Option`, without nesting. |
| `Filter`  | Keeps the value only if it passes a predicate.                  |
| `Zip`     | Pairs two options into one holding a tuple.                     |
| `ZipWith` | Pairs two options, combining them yourself.                     |
| `Unzip`   | Splits an option holding a tuple back into two.                 |
| `And`     | The second option, if the first was `Some`.                     |
| `Or`      | The first `Some` of the two.                                    |
| `OrElse`  | The same, building the second only if needed.                   |
| `Xor`     | The value only if exactly one of the two is `Some`.             |
| `Reduce`  | Merges two options of the same type.                            |

## Consume

[Full page →](/reference/option/consume)

| Member                   | What it does                                          |
| ------------------------ | ----------------------------------------------------- |
| `IsSome` / `IsNone`      | The state, as a `bool`.                               |
| `IsSomeAnd` / `IsNoneOr` | The state combined with a predicate.                  |
| `Match`                  | Both branches, one plain value out.                   |
| `Deconstruct`            | Lets C# pattern matching bind the value positionally. |
| `Unwrap`                 | The value, or an `UnwrapException`.                   |
| `UnwrapOr`               | The value, or the fallback you supplied.              |
| `UnwrapOrElse`           | The value, or one built by a factory.                 |
| `UnwrapOrDefault`        | The value, or `default(T)`.                           |
| `UnwrapOrNull`           | The value, or `null`. Value types only.               |
| `Expect`                 | Like `Unwrap`, with your own exception message.       |
| `MapOr`                  | Transforms the value, or returns your fallback.       |
| `MapOrElse`              | The same, building the fallback lazily.               |
| `MapOrDefault`           | The same, falling back to `default(TOut)`.            |
| `MapOrNull`              | The same, falling back to `null`. Value types only.   |

## Side effects

[Full page →](/reference/option/side-effects)

| Method    | What it does                                                     |
| --------- | ---------------------------------------------------------------- |
| `Inspect` | Runs an action on the value and hands the option back unchanged. |

## Nesting and conversion

[Full page →](/reference/option/nesting)

| Method      | What it does                                                   |
| ----------- | -------------------------------------------------------------- |
| `Flatten`   | Collapses an `Option<Option<T>>` into an `Option<T>`.          |
| `Transpose` | Turns an `Option<Result<T, E>>` into a `Result<Option<T>, E>`. |
| `OkOr`      | Converts to a `Result`, with an error you already have.        |
| `OkOrElse`  | The same, building the error only on a `None`.                 |

## Collections

[Full page →](/reference/option/collections)

| Method         | What it does                                           |
| -------------- | ------------------------------------------------------ |
| `Filter`       | Flips every option that fails the predicate to `None`. |
| `Map`          | Transforms every `Some` in the sequence.               |
| `Flatten`      | Drops the `None`s and keeps the values.                |
| `Collect`      | `Some` of every value, or `None` if any is missing.    |
| `CollectAsync` | The same, over an `IAsyncEnumerable`.                  |
| `AsEnumerable` | Treats one option as a sequence of nothing or one.     |
| `FirstOrNone`  | The first match, or `None`.                            |
| `FirstOr`      | The first match, or a fallback you supplied.           |
| `FirstOrElse`  | The same, building the fallback lazily.                |

## Async

Every method above has an `Async` counterpart. They are extension methods on the task that wraps the option, not on the option itself — see [Async](/guides/async).

## State overloads

Most methods that take a delegate also take one that receives your data instead of capturing it. See [State overloads](/reference/state-overloads).


# Creation

The factory methods that build an Option\<T>.

## Option.Some

```csharp
Option<T> Option.Some<T>(T value)
```

Wraps a value. The result is always `Some`.

```csharp
Option<string> some = Option.Some("Hello Bees!");
```

**On `null`:** throws `ArgumentNullException`. `T` is constrained `notnull`, and the constructor enforces it. A *default* value is fine — `Option.Some(0)` is a `Some` holding zero.

## Option.None

```csharp
Option<T> Option.None<T>()
```

The empty case. You supply the type parameter because there is no value to infer it from.

```csharp
Option<string> none = Option.None<string>();
```

## Option.FromNullable

```csharp
Option<T> Option.FromNullable<T>(T? value)
```

`Some` when the value is not `null`, `None` when it is. Use it at the edge of your code, where the shape is not yours to choose.

## Option.Try

```csharp
Option<T> Option.Try<T>(Func<T> factory)
```

Runs a factory that might throw, and asks one question: did it hand back a value you can work with?

```csharp
Option<Adventurer> maybeAdventurer = Option.Try(() => GetCurrentAdventurer());
```

**On a throw:** the exception is caught, sent to your [configured exception logger](/guides/configuration), and you get `None`.

**On `null`:** you get `None`, because a `Some` cannot hold one. Nothing is logged, because nothing threw. `Option.Try(() => 0)` gives you `Some(0)` — only `null` is rejected.

{% hint style="warning" %}
**A cancellation is not caught.** `Try` and `TryAsync` let an `OperationCanceledException` propagate, so a cancelled operation throws rather than becoming a `None`. Cancelling is you asking the work to stop, not the work failing. See [Configuration](/guides/configuration#cancellation) to get the pre-6.0.0 behaviour back.
{% endhint %}

{% hint style="danger" %}
**Do not pass an async factory to `Try`.** It compiles, gives you an `Option<Task<T>>`, and catches nothing. Use `TryAsync`. [`WM1011`](/reference/analyzers/runtime-bugs#wm1011) reports every occurrence.
{% endhint %}

## Option.TryAsync

```csharp
ValueTask<Option<T>> Option.TryAsync<T>(Func<Task<T>> asyncFactory)
```

The same, for a factory that returns a `Task`. See [Async](/guides/async#create-a-monad-from-async-work).

## Passing state to the factory

`Try` and `TryAsync` each take an optional first argument that they hand to your factory. Use it to keep the factory from capturing:

```csharp
Option<int> parsed = Option.Try(text, static value => int.Parse(value));
```

See [State overloads](/reference/state-overloads) for why this matters.


# Transform

Methods that take an Option\<T> and give you back an Option.

Every method here returns an `Option`, so the chain continues. To end it, see [Consume](/reference/option/consume).

## Map

```csharp
Option<TOut> Map<TOut>(Func<T, TOut> map)
```

Applies a transformation to the value if there is one.

```csharp
Option<string> maybeName = Option.Some("Henry Crabgrass");
Option<int> maybeLength = maybeName.Map(name => name.Length);
```

**On a `None`:** the delegate never runs, and you get `None<TOut>`.

## AndThen

```csharp
Option<TOut> AndThen<TOut>(Func<T, Option<TOut>> map)
```

Chains a step that itself returns an `Option`. `Map` would give you `Option<Option<TOut>>`; `AndThen` keeps it flat.

```csharp
Option<string> maybeDomain = maybeSigil.AndThen(TryExtractDomain);
```

**On a `None`:** short-circuits. Later steps never run.

{% hint style="warning" %}
Called `FlatMap` before 5.4.0. `FlatMap` was `[Obsolete]` through 5.x and 6.0.0 removed it, so a call to it is `CS0117` rather than a warning. `WM2014`, the rule that reported each call site, retired with it — delete any `.editorconfig` entry for that id.
{% endhint %}

## Filter

```csharp
Option<T> Filter(Predicate<T> predicate)
```

Keeps the value only if it passes. If it does not, you get `None`.

```csharp
Option<string> maybeName = Option.Some("Thordak");

Option<string> nonEmpty = maybeName.Filter(name => name.Length > 0); // Some("Thordak")
Option<string> blank = maybeName.Filter(name => name.Length == 0);   // None
```

**On a `None`:** the predicate never runs.

## Zip

```csharp
Option<(T, T2)> Zip<T2>(Option<T2> other)
```

Pairs two options into one holding a tuple.

```csharp
Option<string> vex = Option.Some("Vex'ahlia");
Option<string> vax = Option.Some("Vax'ildan");
Option<string> missing = Option.None<string>();

Option<(string, string)> twins = vex.Zip(vax);     // Some(("Vex'ahlia", "Vax'ildan"))
Option<(string, string)> alone = vex.Zip(missing); // None
```

**If either side is `None`:** you get `None`.

## ZipWith

```csharp
Option<TOut> ZipWith<T2, TOut>(Option<T2> other, Func<T, T2, TOut> zip)
```

The same, but you combine the two values instead of getting a tuple.

```csharp
Option<int> fireball = Option.Some(24);
Option<int> sneakAttack = Option.Some(18);

Option<int> total = fireball.ZipWith(sneakAttack, (a, b) => a + b);
//         ^? Some(42)
```

**If either side is `None`:** you get `None`, and the delegate never runs.

`ZipWith` has no state overload and is not getting one — it already hands the delegate both values.

## Unzip

```csharp
(Option<T1>, Option<T2>) Unzip<T1, T2>(this Option<(T1, T2)> option)
```

Reverses a `Zip`. An extension method, in `Waystone.Monads.Options.Extensions`.

```csharp
Option<(string, string)> twins = Option.Some(("Vex'ahlia", "Vax'ildan"));
Option<(string, string)> none = Option.None<(string, string)>();

twins.Unzip(); // (Some("Vex'ahlia"), Some("Vax'ildan"))
none.Unzip();  // (None, None)
```

A component that equals its type's default is an ordinary value, so `Option.Some((0, "x")).Unzip()` gives `(Some(0), Some("x"))`. This threw before 6.0.0.

## And

```csharp
Option<T2> And<T2>(Option<T2> other)
```

Returns the second option, but only if the first was `Some`. It ignores what the first held, so it answers "did both arrive?" rather than combining them.

```csharp
Option<string> maybeName = Option.Some("Grog");
Option<int> maybeLevel = Option.Some(19);

Option<int> both = maybeName.And(maybeLevel);                // Some(19)
Option<int> neither = Option.None<string>().And(maybeLevel); // None
```

**Evaluated eagerly.** Reach for [`AndThen`](#andthen) when producing the second one costs something, or when it depends on the first one's value.

## Reduce

```csharp
Option<T> Reduce(Option<T> other, Func<T, T, T> reduce)
```

Merges two options of the same type. When both hold a value your function combines them; when only one does, you get that one back untouched.

```csharp
Option<int> firstRoll = Option.Some(3);
Option<int> secondRoll = Option.Some(4);

firstRoll.Reduce(secondRoll, (a, b) => a + b);          // Some(7)
firstRoll.Reduce(Option.None<int>(), (a, b) => a + b);  // Some(3)
Option.None<int>().Reduce(secondRoll, (a, b) => a + b); // Some(4)
```

**When both are `None`:** you get `None`, and the delegate never runs.

`Reduce` has no state overload and is not getting one.

## Or

```csharp
Option<T> Or(Option<T> other)
```

The first `Some` of the two.

```csharp
Option<string> result = chosen.Or(absent).Or(fallback);
//             ^? Some("Keyleth")
```

**Evaluated eagerly.** Use [`OrElse`](#orelse) if the fallback costs something.

## OrElse

```csharp
Option<T> OrElse(Func<Option<T>> createElse)
```

The same as `Or`, but the factory runs only when the receiver is `None`.

```csharp
Option<string> result = first
    .OrElse(() => RollForAnother())
    .OrElse(() => SendInTheHireling());
//     ^? Some("The understudy")
```

## Xor

```csharp
Option<T> Xor(Option<T> other)
```

Exclusive or. The value comes back only if exactly one of the two is `Some`.

```csharp
Option<string> result = bardsong
    .Xor(silence)     // Some("Scanlan")
    .Xor(secondBard); // None
```

**When both are `Some`, or both `None`:** you get `None`.


# Consume

Methods that end the chain and hand you a plain value.

Everything on this page takes you out of the `Option`. To keep chaining, see [Transform](/reference/option/transform).

## IsSome and IsNone

```csharp
bool IsSome { get; }
bool IsNone { get; }
```

The state, as a `bool`. Properties, not methods.

```csharp
Option<string> maybeName = Option.Some("Laudna");

maybeName.IsSome; // true
maybeName.IsNone; // false
```

{% hint style="info" %}
Good for a short-circuit or a guard. Reach for [`Match`](#match) when both branches matter.
{% endhint %}

## IsSomeAnd

```csharp
bool IsSomeAnd(Predicate<T> predicate)
```

There is a value **and** it passes the predicate.

```csharp
Option<string> maybePatron = Option.Some("The Raven Queen");
maybePatron.IsSomeAnd(patron => patron.Length > 0); // true
```

## IsNoneOr

```csharp
bool IsNoneOr(Predicate<T> predicate)
```

There is no value, **or** the one there passes.

```csharp
maybePatron.IsNoneOr(patron => patron.Length > 0);                 // true
maybePatron.IsNoneOr(patron => string.IsNullOrWhiteSpace(patron)); // false
```

## Match

```csharp
TOut Match<TOut>(Func<T, TOut> onSome, Func<TOut> onNone)
void Match(Action<T> onSome, Action onNone)
```

Both branches, one plain value out. This is the default way to end a chain.

```csharp
Option<string> maybeName = Option.Some("Travis");

int length = maybeName.Match(
    name => name.Length,
    () => 0);
```

**On a `None`:** `length` is `0` — the `onNone` branch runs and `onSome` does not.

`Match` also has the state overload that saves the most, because a capturing call pays for two delegates. See [Match saves the most](/reference/state-overloads#match-saves-the-most).

## Pattern matching with Deconstruct

From 7.0.0 the case types deconstruct, so C# pattern matching binds the value positionally.

```csharp
using Waystone.Monads.Options;

if (maybeName is Some<string>(var name))
{
    Console.WriteLine(name.Length);
}
```

Three `Deconstruct` methods exist across both types, and only three:

| Type             | Signature                     | Binds               |
| ---------------- | ----------------------------- | ------------------- |
| `Some<T>`        | `Deconstruct(out T value)`    | The contained value |
| `Ok<TOk, TErr>`  | `Deconstruct(out TOk value)`  | The Ok value        |
| `Err<TOk, TErr>` | `Deconstruct(out TErr error)` | The error           |

Each is documented as never handing you `null`.

### None has none, deliberately

There is nothing to bind, and `option is None<string>` already tests the case.

So `option is None<string>()`, **with** the parentheses, is a compile error rather than a redundant spelling. An empty positional pattern still needs a `Deconstruct` to bind against:

```
CS8129: No suitable 'Deconstruct' instance or extension method was found for type
        'None<string>', with 0 out parameters and a void return type.
```

Write it without the parentheses.

### You cannot deconstruct the monad itself

`Deconstruct` is on the case types, not on `Option<T>`. So this does not compile:

```csharp
var (a, b) = maybeName; // CS1061, CS8129, CS8130
```

There is no state-plus-value tuple to destructure. Test the case first, then bind.

### Where Match still wins

**A `switch` expression over the closed hierarchy warns**, even with both cases covered:

```csharp
int length = maybeName switch
{
    Some<string>(var name) => name.Length,
    None<string> => 0,
};
```

```
CS8509: The switch expression does not handle all possible values of its input type
        (it is not exhaustive). For example, the pattern '_' is not covered.
```

The hierarchy really is closed — an internal member stops anything outside the assembly deriving from `Option<T>` — but the compiler's exhaustiveness check has no knowledge of that idiom. Silencing the warning means an unreachable arm:

```csharp
_ => throw new UnreachableException(),
```

`Match` needs neither. It takes exactly two branches, both required, and returns a value with no warning to suppress.

**So use `Match` when you want a value out of both cases**, which is most of the time. **Reach for a positional pattern in statement position** — an `if` guarding a block, a `switch` statement, a `when` clause — where `Match` would mean wrapping statements in a lambda that returns nothing.

{% hint style="info" %}
**A positional pattern does not trip `WM2021`.** That rule reports a *property* pattern reading `IsSome`, `IsNone`, `IsOk` or `IsErr` — a state check written so nothing recognises it as one. A positional pattern reads none of those properties. See [`WM2021`](/reference/analyzers/idioms#wm2021).
{% endhint %}

## Unwrap

```csharp
T Unwrap()
```

The value, or a throw.

```csharp
Option<string> maybeName = Option.Some("Lorekeeper");
string name = maybeName.Unwrap();
```

**On a `None`:** throws `UnwrapException`.

{% hint style="info" %}
An intentional point of failure, like `First` on an empty sequence. Use it only when you have established the value is there upstream. Otherwise reach for [`Match`](#match).
{% endhint %}

## UnwrapOr

```csharp
T UnwrapOr(T value)
```

The value, or the fallback you already have.

```csharp
Option<string> maybeNickname = Option.None<string>();
string nickname = maybeNickname.UnwrapOr("Lautna");
//     ^? "Lautna"
```

## UnwrapOrElse

```csharp
T UnwrapOrElse(Func<T> createElse)
```

The same, but the factory runs only on a `None`. Use it when the fallback costs something to build.

```csharp
Option<Uri> maybePortrait = Option.None<Uri>();
Uri portrait = maybePortrait.UnwrapOrElse(() => GeneratePortrait());
//  ^? generated portrait
```

## UnwrapOrDefault

```csharp
T? UnwrapOrDefault()
```

The value, or `default(T)`.

```csharp
Option<string> maybeName = Option.None<string>();
string? name = maybeName.UnwrapOrDefault();
//      ^? null
```

{% hint style="warning" %}
**On a value type this is the catch.** The signature reads `T?`, but `T` is constrained `notnull`, so the `?` is an annotation rather than a `Nullable<T>`. `UnwrapOrDefault` on an `Option<int>` hands you `0`, and nothing tells you whether that `0` came from a `Some` or from the absent case. Reach for [`UnwrapOrNull`](#unwrapornull) there. `WM2015` points this out for you.
{% endhint %}

## UnwrapOrNull

```csharp
T? UnwrapOrNull<T>(this Option<T> option) where T : struct
```

The value, or `null` — a real `Nullable<T>`, so absence stays visible. An extension method, in `Waystone.Monads.Options.Extensions`.

```csharp
using Waystone.Monads.Options.Extensions;

Option<int> maybeCount = Option.None<int>();
int? count = maybeCount.UnwrapOrNull();
//   ^? null, where UnwrapOrDefault would have given you 0
```

{% hint style="info" %}
Constrained to `T : struct`, so it does not appear on an `Option<string>`. A reference type needs no equivalent — `UnwrapOrDefault` already gives `null`.
{% endhint %}

## Expect

```csharp
T Expect(string message)
```

Like `Unwrap`, but you supply the message the exception carries.

```csharp
Option<string> maybeName = Option.Some("Greymore");
string name = maybeName.Expect("Expected a name, but got nothing.");
```

**On a `None`:** throws `UnmetExpectationException` carrying your message.

Use it where an absent value means a logic error rather than a runtime condition to recover from.

## MapOr

```csharp
TOut MapOr<TOut>(TOut defaultValue, Func<T, TOut> map)
```

Transforms the value, or returns your fallback. Unlike [`Map`](/reference/option/transform#map), it ends the chain.

```csharp
Option<string> maybeName = Option.None<string>();
int length = maybeName.MapOr(0, name => name.Length);
//  ^? 0
```

## MapOrElse

```csharp
TOut MapOrElse<TOut>(Func<TOut> createDefault, Func<T, TOut> map)
```

The same, building the fallback lazily.

```csharp
Option<Adventurer> maybeAdventurer = Option.None<Adventurer>();

Uri portrait = maybeAdventurer.MapOrElse(
    () => GeneratePortrait(),
    adventurer => adventurer.Portrait);
```

Its state overload threads the same state through *both* delegates — see [MapOrElse threads state through both delegates](/reference/state-overloads#maporelse-threads-state-through-both-delegates).

## MapOrDefault

```csharp
TOut? MapOrDefault<TOut>(Func<T, TOut> map)
```

The same, falling back to `default(TOut)`, so you write no fallback at all.

```csharp
Option<string> maybeName = Option.None<string>();
int length = maybeName.MapOrDefault(name => name.Length);
//  ^? 0
```

{% hint style="warning" %}
The signature reads `TOut?`, but `TOut` is constrained `notnull`, so on a value type that `?` is an annotation and not a `Nullable<TOut>`. Map to an `int` and the absent case gives you `0`, not `null`. That is what `MapOrNull` is for.
{% endhint %}

## MapOrNull

```csharp
TOut? MapOrNull<TOut>(Func<T, TOut> map) where TOut : struct
```

The same, falling back to `null`. This one is on `Option<T>` itself, so it needs no extra `using` — unlike [`UnwrapOrNull`](#unwrapornull), which is an extension.

```csharp
Option<string> maybeName = Option.None<string>();
int? length = maybeName.MapOrNull(name => name.Length);
//   ^? null, where MapOrDefault would have given you 0
```

{% hint style="info" %}
Constrains its result to `TOut : struct`. Map to a reference type and `MapOrDefault` already gives you `null`.
{% endhint %}


# Side effects

Run something against the value without changing it.

## Inspect

```csharp
Option<T> Inspect(Action<T> action)
```

Runs an action against the value when the option is `Some`, and hands the option back unchanged so the chain continues. Logging is the usual reason.

```csharp
Option<string> maybeName = Option.Some("Geladon");
maybeName.Inspect(name => Console.WriteLine(name.Length));
```

**On a `None`:** the action never runs, and the `None` comes back.

{% hint style="info" %}
Reach for [`Map`](/reference/option/transform#map) instead if you want to *change* the value.
{% endhint %}

There is no `InspectNone`. Use [`Match`](/reference/option/consume#match) when both branches need to do something.

## Why not just ToString it?

**`ToString()` never shows the wrapped value.** You get the state and nothing else:

```csharp
Option.Some("Vex'ahlia").ToString() // "Some { IsSome = True, IsNone = False }"
Option.None<string>().ToString()    // "None { IsSome = False, IsNone = True }"
```

`Option<T>` is a record and `Some<T>` keeps its value in a private property, so the compiler-generated `ToString()` has nothing to print. Interpolating an option into a log message tells you whether a value was there, never what it was. That is what `Inspect` is for.


# Nesting and conversion

Remove a level of nesting, or convert to a Result.

`Flatten` and `Transpose` are extension methods, in `Waystone.Monads.Options.Extensions` — add the `using`. `OkOr` and `OkOrElse` are on `Option<T>` itself and need nothing.

## Flatten

```csharp
Option<T> Flatten<T>(this Option<Option<T>> option)
```

Removes one level of nesting.

```csharp
Option<Option<string>> some = Option.Some(Option.Some("Chetney"));
Option<string> result = some.Flatten();
```

**On a `None` at either level:** you get `None`.

{% hint style="info" %}
This is the single-option `Flatten`. The one that drops the `None`s out of a *sequence* is a different method — see [Collections](/reference/option/collections#flatten). No receiver is both, so the two never compete.
{% endhint %}

## Transpose

```csharp
Result<Option<T>, E> Transpose<T, E>(this Option<Result<T, E>> option)
```

Turns an option holding a result into a result holding an option.

```csharp
Option<int> maybeNumber = Option.Try(() => RollD20());

Option<Result<int, string>> maybeResult = maybeNumber
    .Map(number => Divide(number, 2));

Result<Option<int>, string> result = maybeResult.Transpose();
```

Calling `Transpose` here declares that an `Ok` holding `None` is a valid outcome in your business rules.

**On a `None`:** you get `Ok(None)` — nothing failed, there was just nothing there.

`Result<Option<T>, E>` transposes the other way. See [the Result page](/reference/result/nesting#transpose).

## OkOr

```csharp
Result<T, TErr> OkOr<TErr>(TErr error)
```

Converts to a `Result`. A `Some` becomes an `Ok`, a `None` becomes an `Err` carrying the error you supplied.

```csharp
Option<int> some = Option.Some(1);
Option<int> none = Option.None<int>();
Error error = new("ER1", "Missing number.");

Result<int, Error> ok = some.OkOr(error);
//                 ^? Ok(1)

Result<int, Error> err = none.OkOr(error);
//                 ^? Err(error)
```

**Evaluated eagerly.** If the error comes from a function call, use [`OkOrElse`](#okorelse).

## OkOrElse

```csharp
Result<T, TErr> OkOrElse<TErr>(Func<TErr> errorFactory)
```

The same, but the error is built only when the option is `None`.

```csharp
Result<int, string> ok = some.OkOrElse(() => DescribeMissingNumber());
//                  ^? Ok(1), and DescribeMissingNumber never runs

Result<int, string> err = none.OkOrElse(() => DescribeMissingNumber());
//                  ^? Err("No number between 1 and 20")
```

Pass a factory only when there is something to defer. An error you already hold goes to [`OkOr`](#okor) — wrapping it in a lambda builds it just the same and allocates a delegate on top.

## Going the other way

To convert a `Result` into an `Option`, see [`GetOk` and `GetErr`](/reference/result/nesting#getok).


# Collections

Methods for working with a sequence of Option\<T>.

A `List<Option<T>>` — the results of looking something up once per item — comes up often enough to have its own methods.

Every method here is an extension method, in `Waystone.Monads.Options.Extensions`. Add the `using`.

## Filter

```csharp
IEnumerable<Option<T>> Filter<T>(this IEnumerable<Option<T>> source, Predicate<T> predicate)
```

The sequence version of [`Filter`](/reference/option/transform#filter). Every option that fails the predicate is flipped to `None`. Nothing is dropped.

```csharp
List<Option<string>> collection = [
    Option.Some("Hello"),
    Option.Some("World"),
    Option.None<string>()
];

IEnumerable<Option<string>> filtered = collection.Filter(x => x == "Hello");
//                          ^? [Some("Hello"), None, None]
```

## Map

```csharp
IEnumerable<Option<TOut>> Map<T, TOut>(this IEnumerable<Option<T>> source, Func<T, TOut> map)
```

The sequence version of [`Map`](/reference/option/transform#map). The transformation runs on every `Some`; the `None`s pass through untouched.

```csharp
IEnumerable<Option<string>> mapped = collection.Map(x => $"{x}!");
//                          ^? [Some("Hello!"), Some("World!"), None]
```

## Flatten

```csharp
IEnumerable<T> Flatten<T>(this IEnumerable<Option<T>> source)
```

Drops the `None`s and keeps the values, in order.

```csharp
List<Option<string>> collection = [
    Option.Some("Hello"),
    Option.None<string>(),
    Option.Some("World")
];

IEnumerable<string> values = collection.Flatten();
//                  ^? ["Hello", "World"]
```

**Lazy.** It walks the source once and composes with the rest of LINQ as you would expect. Nothing runs until you enumerate the result.

{% hint style="info" %}
This is the sequence version. The `Flatten` that collapses a single nested `Option<Option<T>>` is a different method — see [Nesting](/reference/option/nesting#flatten). No receiver is both, so the two never compete.
{% endhint %}

## Collect

```csharp
Option<IReadOnlyList<T>> Collect<T>(this IEnumerable<Option<T>> source)
```

For when every value has to be present. You get a `Some` holding all of them, or a single `None` if any is missing.

```csharp
List<Option<string>> collection = [
    Option.Some("Hello"),
    Option.Some("World")
];

Option<IReadOnlyList<string>> all = collection.Collect();
//                            ^? Some(["Hello", "World"])
```

One `None` anywhere fails the whole call:

```csharp
List<Option<string>> withAGap = [
    Option.Some("Hello"),
    Option.None<string>(),
    Option.Some("World")
];

Option<IReadOnlyList<string>> all = withAGap.Collect();
//                            ^? None
```

This is the opposite of `Flatten`. `Flatten` drops what is missing and carries on; `Collect` treats one missing value as a failure of the whole batch.

**It stops at the first `None`.** It never looks at the rest of the source, so anything that would have produced the later elements does not run.

**On an empty sequence:** you get `Some` of an empty list, not `None`. There is nothing missing in it.

**The result does not tell you&#x20;*****which*****&#x20;element was absent.** Use `Partition` on a sequence of `Result` when you need to know what failed.

{% hint style="info" %}
`Collect` is eager, and builds a list as it goes. Do not call it on an unbounded sequence.
{% endhint %}

## CollectAsync

```csharp
ValueTask<Option<IReadOnlyList<T>>> CollectAsync<T>(
    this IAsyncEnumerable<Option<T>> source,
    CancellationToken cancellationToken = default)
```

The same job over an `IAsyncEnumerable`.

```csharp
Option<IReadOnlyList<string>> all = await stream.CollectAsync(cancellationToken);
```

It stops pulling from the stream at the first `None`, so the work behind the later elements never happens. That is the reason to use it rather than reading the whole stream into a list and calling `Collect`.

Returned `Task` up to 6.7.0. Returns `ValueTask` from 7.0.0.

## AsEnumerable

```csharp
IEnumerable<T> AsEnumerable<T>(this Option<T> option)
```

Treats a single option as a sequence of nothing or one. `Flatten` above is built out of it, and it is the way out of the monad into `System.Linq`.

```csharp
Option<string> maybeName = Option.Some("Pike");

IEnumerable<string> sequence = maybeName.AsEnumerable();
//                  ^? ["Pike"], and [] for a None
```

It is **not** how you write a LINQ query over an `Option`. For that — `from`, `select`, `where`, staying inside the `Option` throughout — see [Waystone.Monads.Linq](/reference/packages/linq).

## FirstOrNone

```csharp
Option<T> FirstOrNone<T>(this IEnumerable<Option<T>> source, Predicate<T> predicate)
```

The first element matching the predicate, or `None`.

```csharp
List<Option<string>> collection = [
    Option.Some("Hello"),
    Option.Some("World")
];

Option<string> first = collection.FirstOrNone(x => x.StartsWith("H"));
//             ^? Some("Hello")
```

## FirstOr

```csharp
T FirstOr<T>(this IEnumerable<Option<T>> source, Predicate<T> predicate, T fallback)
```

The first match, or the fallback you supplied.

```csharp
string first = collection.FirstOr(x => x.StartsWith("V"), "Victor");
//     ^? "Victor"
```

**Evaluated eagerly.** Use [`FirstOrElse`](#firstorelse) if the fallback costs something to build.

## FirstOrElse

```csharp
T FirstOrElse<T>(this IEnumerable<Option<T>> source, Predicate<T> predicate, Func<T> createFallback)
```

The same, building the fallback only when there is no match.

```csharp
string first = collection.FirstOrElse(x => x.StartsWith("V"), () => "Victor");
//     ^? "Victor"
```


# Result\<T, E> API

Every method on Result\<TOk, TErr>, grouped by what it does.

`Result<TOk, TErr>` holds either a success value (`Ok<TOk, TErr>`) or a failure (`Err<TOk, TErr>`). Neither side can hold `null`.

This is the lookup. If you are learning the type rather than looking a method up, read the [Result\<T, E> guide](/guides/result) first.

The five categories match the [Option\<T> reference](/reference/option) exactly, so a reader who knows one tree can navigate the other.

## Creation

[Full page →](/reference/result/creation)

| Method                         | What it does                                                   |
| ------------------------------ | -------------------------------------------------------------- |
| `Result.Ok<TOk, TErr>(value)`  | The success case. Throws on `null`.                            |
| `Result.Err<TOk, TErr>(error)` | The failure case. Throws on `null`.                            |
| `Result.Ok<TOk>(value)`        | The same, defaulting `TErr` to `Error`.                        |
| `Result.Err<TOk>(error)`       | The same, defaulting `TErr` to `Error`.                        |
| `Result.Try(factory, onError)` | Runs a factory that might throw.                               |
| `Result.Try<TOk>(factory)`     | The same, converting the exception with `Error.FromException`. |
| `Result.TryAsync(…)`           | The same, for a factory returning a `Task`.                    |

## Transform

[Full page →](/reference/result/transform)

| Method    | What it does                                                   |
| --------- | -------------------------------------------------------------- |
| `Map`     | Changes the success value.                                     |
| `MapErr`  | Changes the error.                                             |
| `AndThen` | Chains a step that itself returns a `Result`, without nesting. |
| `And`     | The second result, if the first was `Ok`.                      |
| `Or`      | The first `Ok` of the two.                                     |
| `OrElse`  | The same, building the second only if needed.                  |

## Consume

[Full page →](/reference/result/consume)

| Member                 | What it does                                            |
| ---------------------- | ------------------------------------------------------- |
| `IsOk` / `IsErr`       | The state, as a `bool`.                                 |
| `IsOkAnd` / `IsErrAnd` | The state combined with a predicate.                    |
| `Match`                | Both branches, one plain value out.                     |
| `Deconstruct`          | Lets C# pattern matching bind either side positionally. |
| `Unwrap`               | The success value, or an `UnwrapException`.             |
| `UnwrapErr`            | The error, or an `UnwrapException`.                     |
| `UnwrapOr`             | The success value, or the fallback you supplied.        |
| `UnwrapOrElse`         | The success value, or one built from the error.         |
| `UnwrapOrDefault`      | The success value, or `default(TOk)`.                   |
| `UnwrapOrNull`         | The success value, or `null`. Value types only.         |
| `Expect`               | Like `Unwrap`, with your own exception message.         |
| `ExpectErr`            | Like `UnwrapErr`, with your own exception message.      |
| `MapOr`                | Transforms the success value, or returns your fallback. |
| `MapOrElse`            | The same, building the fallback from the error.         |
| `MapOrDefault`         | The same, falling back to `default(TOut)`.              |
| `MapOrNull`            | The same, falling back to `null`. Value types only.     |

## Side effects

[Full page →](/reference/result/side-effects)

| Method       | What it does                                                   |
| ------------ | -------------------------------------------------------------- |
| `Inspect`    | Runs an action on the success value and hands the result back. |
| `InspectErr` | The same, on the error.                                        |

## Nesting and conversion

[Full page →](/reference/result/nesting)

| Method      | What it does                                                   |
| ----------- | -------------------------------------------------------------- |
| `Flatten`   | Collapses a `Result<Result<TOk, TErr>, TErr>`.                 |
| `Transpose` | Turns a `Result<Option<T>, E>` into an `Option<Result<T, E>>`. |
| `GetOk`     | Converts to an `Option`, keeping the success value.            |
| `GetErr`    | Converts to an `Option`, keeping the error.                    |

## Collections

[Full page →](/reference/result/collections)

| Method         | What it does                                              |
| -------------- | --------------------------------------------------------- |
| `Flatten`      | Keeps the successes, drops the failures.                  |
| `FlattenErr`   | Keeps the failures, drops the successes.                  |
| `Partition`    | Both halves, reading the source once.                     |
| `Collect`      | `Ok` of every value, or `Err` carrying the first failure. |
| `CollectAsync` | The same, over an `IAsyncEnumerable`.                     |
| `AsEnumerable` | Treats one result as a sequence of nothing or one.        |

## Async

Every method above has an `Async` counterpart. They are extension methods on the task that wraps the result, not on the result itself — see [Async](/guides/async).

## State overloads

Most methods that take a delegate also take one that receives your data instead of capturing it. See [State overloads](/reference/state-overloads).


# Creation

The factory methods that build a Result\<TOk, TErr>.

## Result.Ok and Result.Err

```csharp
Result<TOk, TErr> Result.Ok<TOk, TErr>(TOk value)
Result<TOk, TErr> Result.Err<TOk, TErr>(TErr error)
```

Supply both type parameters when you use your own error type.

```csharp
Result<int, string> ok = Result.Ok<int, string>(1);
Result<int, string> err = Result.Err<int, string>("Something went wrong...");
```

{% hint style="warning" %}
**Neither an `Ok` nor an `Err` can hold `null`.** Pass one and you get an `ArgumentNullException`. Every factory funnels through the same guard.

```csharp
Result.Ok<string, Error>(null!); // throws
```

New in 5.5.0. Before that, an `Ok` could hold `null` and the `null` surfaced later as a `NullReferenceException` in your own code. `TOk` and `TErr` are constrained `notnull`, so the compiler already warned you; now the runtime agrees.

A **default** value is fine and always has been. `Result.Ok<int, string>(0)` is an `Ok` holding `0`. Only `null` is rejected.
{% endhint %}

## The single-type-parameter overloads

```csharp
Result<TOk, Error> Result.Ok<TOk>(TOk value)
Result<TOk, Error> Result.Err<TOk>(Error error)
```

If you are happy with the built-in [`Error`](/guides/errors) type, leave `TErr` off and it defaults to `Error`.

```csharp
Result<int, Error> ok = Result.Ok<int>(1);
Result<int, Error> err = Result.Err<int>(
    new Error("MyCode", "Something went wrong..."));
```

### From a generated catalog

Mark an enum with `[ErrorCodeCatalog]` and the source generator gives you a factory per member, with the message required.

```csharp
[ErrorCodeCatalog]
enum PartyErrors
{
    NotFound
}

Result<Adventurer, Error> err = Result.Err<Adventurer>(
    PartyErrorsCatalog.Errors.NotFound("The adventurer was not found"));
```

{% hint style="info" %}
Passing an enum straight to `Result.Err` was removed in 7.0.0. See [Generated error codes](/reference/source-generation).
{% endhint %}

## Result.Try

```csharp
Result<TOk, TErr> Result.Try<TOk, TErr>(Func<TOk> factory, Func<Exception, TErr> onError)
```

Runs a factory that might throw, and asks one question: did it hand back a value you can work with?

```csharp
Result<Adventurer, string> result = Result.Try(
    factory: () => GetCurrentAdventurer(),
    onError: ex => ex.Message);
```

**On a throw:** the exception is caught, sent to your [configured exception logger](/guides/configuration), and `onError` runs.

**On `null`:** `onError` runs too, passed an `ArgumentNullException` naming the `factory` argument. Nothing is logged, because nothing threw.

That last case is the one place a `null` does not throw. `Try` exists so you can hand over a delegate and learn whether a workable value came back without wrapping the call yourself — so it turns the `null` into an `Err` for you.

{% hint style="warning" %}
**A cancellation is not caught.** `Try` and `TryAsync` let an `OperationCanceledException` propagate. See [Configuration](/guides/configuration#cancellation).
{% endhint %}

{% hint style="danger" %}
**Do not pass an async factory to `Try`.** It compiles, gives you a `Result<Task<T>, E>`, and catches nothing. Use `TryAsync`. [`WM1011`](/reference/analyzers/runtime-bugs#wm1011) reports every occurrence.
{% endhint %}

### Defaulting to Error

```csharp
Result<TOk, Error> Result.Try<TOk>(Func<TOk> factory)
```

Converts the exception with [`Error.FromException`](/guides/exceptions#turning-an-exception-into-an-error), so you pass no `onError` delegate.

```csharp
Result<Adventurer, Error> result = Result.Try<Adventurer>(
    () => GetCurrentAdventurer());
```

## Result.TryAsync

```csharp
ValueTask<Result<TOk, TErr>> Result.TryAsync<TOk, TErr>(
    Func<Task<TOk>> asyncFactory,
    Func<Exception, TErr> onError)
```

The same, for a factory that returns a `Task`. See [Async](/guides/async#create-a-monad-from-async-work).

## Passing state to the factory

`Try` and `TryAsync` each take an optional first argument that they hand to your factory. Use it to keep the factory from capturing:

```csharp
Result<int, Error> result = Result.Try(text, static value => int.Parse(value));
```

See [State overloads](/reference/state-overloads) for why this matters.


# Transform

Methods that take a Result and give you back a Result.

Every method here returns a `Result`, so the chain continues. To end it, see [Consume](/reference/result/consume).

## Map

```csharp
Result<TOut, TErr> Map<TOut>(Func<TOk, TOut> map)
```

Applies a transformation to the success value.

```csharp
Result<string, string> nameResult = Result.Ok<string, string>("Consent");
Result<int, string> lengthResult = nameResult.Map(name => name.Length);
```

**On an `Err`:** the delegate never runs, and the error passes through untouched.

## MapErr

```csharp
Result<TOk, TOut> MapErr<TOut>(Func<TErr, TOut> map)
```

The counterpart. Transforms the error, leaving the success value alone. Reach for it when two pieces of code disagree about the error type and you need them to chain.

```csharp
Result<int, Error> lengthResult = RollForName()             // Result<string, string>
    .MapErr(message => new Error("name.failed", message))   // Result<string, Error>
    .AndThen(name => CountRunes(name));                     // Result<int, Error>
```

**On an `Ok`:** the delegate never runs.

## AndThen

```csharp
Result<TOut, TErr> AndThen<TOut>(Func<TOk, Result<TOut, TErr>> map)
```

Chains a step that itself returns a `Result`. It performs the same operation as [`And`](#and), for each lazily evaluated function.

```csharp
Result<string, string> SquareThenToString(int value)
    => Result.Try<int, string>(() => checked(value * value), _ => "overflow")
        .Map(x => x.ToString());

Result<int, string> two = Result.Ok<int, string>(2);
two.AndThen(SquareThenToString);          // Ok("4")

Result<int, string> big = Result.Ok<int, string>(int.MaxValue);
big.AndThen(SquareThenToString);          // Err("overflow")

Result<int, string> nan = Result.Err<int, string>("NaN");
nan.AndThen(SquareThenToString);          // Err("NaN")
```

**On an `Err`:** short-circuits. Later steps never run.

`Map` followed by [`Flatten`](/reference/result/nesting#flatten) does the same thing in two calls. Prefer `AndThen`.

## And

```csharp
Result<TOut, TErr> And<TOut>(Result<TOut, TErr> other)
```

Gives you the first `Err`, or the last `Ok`.

| Left   | Right  | Output |
| ------ | ------ | ------ |
| `Ok1`  | `Ok2`  | `Ok2`  |
| `Ok`   | `Err`  | `Err`  |
| `Err`  | `Ok`   | `Err`  |
| `Err1` | `Err2` | `Err1` |

```csharp
Result.Ok<int, string>(1).And(Result.Err<int, string>("late error"));
//     ^? Err("late error")

Result.Err<int, string>("early error").And(Result.Ok<int, string>(1));
//     ^? Err("early error")
```

{% hint style="warning" %}
**Evaluated eagerly.** If the argument is the result of a function call, use [`AndThen`](#andthen).
{% endhint %}

## Or

```csharp
Result<TOk, TOut> Or<TOut>(Result<TOk, TOut> other)
```

Gives you the first `Ok`, or the last `Err`.

| Left   | Right  | Output |
| ------ | ------ | ------ |
| `Ok1`  | `Ok2`  | `Ok1`  |
| `Ok`   | `Err`  | `Ok`   |
| `Err`  | `Ok`   | `Ok`   |
| `Err1` | `Err2` | `Err2` |

```csharp
Result.Ok<int, string>(1).Or(Result.Err<int, string>("error"));
//     ^? Ok(1)

Result.Err<int, string>("error 1").Or(Result.Err<int, string>("error 2"));
//     ^? Err("error 2")
```

{% hint style="warning" %}
**Evaluated eagerly.** Use [`OrElse`](#orelse) if the argument costs something.
{% endhint %}

## OrElse

```csharp
Result<TOk, TOut> OrElse<TOut>(Func<TErr, Result<TOk, TOut>> createElse)
```

The same as `Or`, lazily. This is how you recover — and note the factory takes the **error**, not the success value.

```csharp
Result<int, string> Recover(string error)
    => error == "NaN"
        ? Result.Ok<int, string>(0)
        : Result.Err<int, string>(error);

Result.Ok<int, string>(2).OrElse(Recover);            // Ok(2), untouched
Result.Err<int, string>("NaN").OrElse(Recover);       // Ok(0), recovered
Result.Err<int, string>("overflow").OrElse(Recover);  // Err("overflow"), still failed
```

**On an `Ok`:** the factory never runs.


# Consume

Methods that end the chain and hand you a plain value.

Everything on this page takes you out of the `Result`. To keep chaining, see [Transform](/reference/result/transform).

## IsOk and IsErr

```csharp
bool IsOk { get; }
bool IsErr { get; }
```

The state, as a `bool`. Properties, not methods.

```csharp
Result<DateTime, Error> safeParseResult = SafeParse("2025-01-01");

safeParseResult.IsOk;  // true
safeParseResult.IsErr; // false
```

{% hint style="info" %}
Good for a short-circuit or a guard. Reach for [`Match`](#match) when both branches matter.
{% endhint %}

## IsOkAnd

```csharp
bool IsOkAnd(Predicate<TOk> predicate)
```

It succeeded **and** the value passes the predicate.

```csharp
safeParseResult.IsOkAnd(dateTime => dateTime > new DateTime(2024, 1, 1)); // true
```

## IsErrAnd

```csharp
bool IsErrAnd(Predicate<TErr> predicate)
```

It failed **and** the error passes the predicate.

```csharp
Result<DateTime, Error> failed = SafeParse("2025");
//                      ^? Err(new Error(ErrorCodes.MalformedDateTime, "not a date"))

failed.IsErrAnd(error => error.Code == ErrorCodes.MalformedDateTime); // true
```

## Match

```csharp
TOut Match<TOut>(Func<TOk, TOut> onOk, Func<TErr, TOut> onErr)
void Match(Action<TOk> onOk, Action<TErr> onErr)
```

Both branches, one plain value out. This is the default way to end a chain.

```csharp
Result<string, string> nameResult = Result.Ok<string, string>("Sam");

int length = nameResult.Match(
    name => name.Length,
    _ => 0);
```

**On an `Err`:** `length` is `0` — the `onErr` branch runs and `onOk` does not.

`Match` also has the state overload that saves the most. See [Match saves the most](/reference/state-overloads#match-saves-the-most).

## Pattern matching with Deconstruct

From 7.0.0 the case types deconstruct, so C# pattern matching binds either side positionally.

```csharp
using Waystone.Monads.Results;

if (nameResult is Ok<string, string>(var name)) { /* ... */ }
if (nameResult is Err<string, string>(var error)) { /* ... */ }
```

The full list of `Deconstruct` methods, and why a `switch` expression still warns, is on the [Option page](/reference/option/consume#pattern-matching-with-deconstruct). Both types behave the same way.

Unlike `Option`, **both** of a `Result`'s cases deconstruct, because both carry a value.

## Unwrap

```csharp
TOk Unwrap()
```

The success value, or a throw.

```csharp
Result<string, string> nameResult = Result.Ok<string, string>("Danny");
string name = nameResult.Unwrap();
```

**On an `Err`:** throws `UnwrapException`.

{% hint style="info" %}
An intentional point of failure, like `First` on an empty sequence. Otherwise reach for [`Match`](#match).
{% endhint %}

## UnwrapErr

```csharp
TErr UnwrapErr()
```

The other direction. The error, or a throw.

```csharp
Result<int, string> ok = Result.Ok<int, string>(10);
ok.UnwrapErr(); // throws UnwrapException

Result<int, string> err = Result.Err<int, string>("Error");
err.UnwrapErr(); // returns "Error"
```

**On an `Ok`:** throws `UnwrapException`.

## UnwrapOr

```csharp
TOk UnwrapOr(TOk value)
```

The success value, or the fallback you already have.

```csharp
Result<string, Error> nameResult =
    Result.Err<string, Error>(new Error(ErrorCodes.MissingName, "no name was supplied"));

string name = nameResult.UnwrapOr("Unknown");
//     ^? "Unknown"
```

## UnwrapOrElse

```csharp
TOk UnwrapOrElse(Func<TErr, TOk> createElse)
```

The same, but the factory runs only on an `Err` — and it receives the error, so the fallback can depend on what went wrong.

```csharp
Result<Loadout, Error> getLoadoutResult = GetLoadout("Ashton");
//                     ^? Err<Loadout, Error>

Loadout loadout = getLoadoutResult.UnwrapOrElse(error => GenerateDefaultLoadout());
//      ^? generated loadout
```

## UnwrapOrDefault

```csharp
TOk? UnwrapOrDefault()
```

The success value, or `default(TOk)`.

```csharp
Result<int, string> numberResult = Result.Err<int, string>("Error");
int number = numberResult.UnwrapOrDefault();
//  ^? 0
```

{% hint style="warning" %}
**On a value type this is the catch.** The signature reads `TOk?`, but `TOk` is constrained `notnull`, so the `?` is an annotation rather than a `Nullable<TOk>`. `UnwrapOrDefault` on a `Result<int, string>` hands you `0`, and nothing tells you whether that `0` came from an `Ok` or from the failure. Reach for [`UnwrapOrNull`](#unwrapornull) there. `WM2015` points this out for you.
{% endhint %}

## UnwrapOrNull

```csharp
TOk? UnwrapOrNull<TOk, TErr>(this Result<TOk, TErr> result) where TOk : struct
```

The success value, or `null` — a real `Nullable<TOk>`, so failure stays visible. An extension method, in `Waystone.Monads.Results.Extensions`.

```csharp
using Waystone.Monads.Results.Extensions;

Result<int, string> countResult = Result.Err<int, string>("Error");
int? count = countResult.UnwrapOrNull();
//   ^? null
```

{% hint style="info" %}
Constrained to `TOk : struct`. A reference type needs no equivalent — `UnwrapOrDefault` already gives `null`.
{% endhint %}

## Expect

```csharp
TOk Expect(string message)
```

Like `Unwrap`, but you supply the message the exception carries.

```csharp
Result<string, string> nameResult = Result.Ok<string, string>("Pelor");
string name = nameResult.Expect("Expected a name, but got an error");
```

**On an `Err`:** throws `UnmetExpectationException` carrying your message.

## ExpectErr

```csharp
TErr ExpectErr(string message)
```

The other direction, and the same idea.

```csharp
Result.Ok<int, string>(10).ExpectErr("Must be error");
// throws UnmetExpectationException with message "Must be error"
```

**On an `Ok`:** throws `UnmetExpectationException` carrying your message.

## MapOr

```csharp
TOut MapOr<TOut>(TOut defaultValue, Func<TOk, TOut> map)
```

Transforms the success value, or returns your fallback. Unlike [`Map`](/reference/result/transform#map), it ends the chain.

```csharp
Result<string, string> nameResult = Result.Err<string, string>("Error");

int length = nameResult.MapOr(0, name => name.Length);
//  ^? 0
```

## MapOrElse

```csharp
TOut MapOrElse<TOut>(Func<TErr, TOut> createDefault, Func<TOk, TOut> map)
```

The same, building the fallback from the error.

```csharp
Result<Adventurer, Error> getAdventurerResult = GetAdventurer("Changebringer");

Uri portrait = getAdventurerResult.MapOrElse(
    error => GeneratePortrait(),
    adventurer => adventurer.Portrait);
```

Its state overload threads the same state through *both* delegates — see [MapOrElse threads state through both delegates](/reference/state-overloads#maporelse-threads-state-through-both-delegates).

## MapOrDefault

```csharp
TOut? MapOrDefault<TOut>(Func<TOk, TOut> map)
```

The same, falling back to `default(TOut)`.

```csharp
Result<string, string> nameResult = Result.Err<string, string>("Error");
int length = nameResult.MapOrDefault(name => name.Length);
//  ^? 0
```

{% hint style="warning" %}
The signature reads `TOut?`, but `TOut` is constrained `notnull`, so on a value type that `?` is an annotation and not a `Nullable<TOut>`. Map to an `int` and the failure case gives you `0`, not `null`.
{% endhint %}

## MapOrNull

```csharp
TOut? MapOrNull<TOut>(Func<TOk, TOut> map) where TOut : struct
```

The same, falling back to `null`. This one is on `Result<TOk, TErr>` itself, so it needs no extra `using` — unlike [`UnwrapOrNull`](#unwrapornull), which is an extension.

```csharp
Result<string, string> nameResult = Result.Err<string, string>("Error");
int? length = nameResult.MapOrNull(name => name.Length);
//   ^? null, where MapOrDefault would have given you 0
```


# Side effects

Run something against either side without changing it.

## Inspect

```csharp
Result<TOk, TErr> Inspect(Action<TOk> action)
```

Runs an action against the success value and hands the result back unchanged, so the chain continues.

```csharp
Result<string, string> nameResult = Result.Ok<string, string>("Percival");
nameResult.Inspect(name => Console.WriteLine(name.Length));
```

**On an `Err`:** the action never runs.

{% hint style="info" %}
Reach for [`Map`](/reference/result/transform#map) instead if you want to *change* the value.
{% endhint %}

## InspectErr

```csharp
Result<TOk, TErr> InspectErr(Action<TErr> action)
```

The counterpart. Runs against the error, and again hands the result back unchanged. Logging a failure is what it is for.

```csharp
Result<string, string> username = FindCharacter("Percy")
    .InspectErr(err => Console.WriteLine($"Find character failed: {err.Message}"))
    .Map(character => character.Username)
    .MapErr(err => err.Message);
```

**On an `Ok`:** the action never runs.

{% hint style="info" %}
Reach for [`MapErr`](/reference/result/transform#maperr) if you want to *change* the error.
{% endhint %}

## Using both together

Each runs only on its own branch, so a chain that carries both logs exactly once either way:

```csharp
Result<Quest, Error> quest = LoadQuest(id)
    .Inspect(q => logger.LogInformation("Loaded quest {Id}", q.Id))
    .InspectErr(e => logger.LogWarning("Load failed: {Code} {Message}", e.Code, e.Message));
```

## Why not just ToString it?

**`ToString()` never shows the wrapped value.** You get the state and nothing else:

```csharp
Result.Ok<int, Error>(1).ToString()  // "Ok { IsOk = True, IsErr = False }"
Result.Err<int, Error>(e).ToString() // "Err { IsOk = False, IsErr = True }"
```

`Result<TOk, TErr>` is a record and both sides keep their value in a private property, so the compiler-generated `ToString()` has nothing to print. Interpolating a result into a log message tells you which branch you are on, never what it holds. That is what the inspect pair is for.


# Nesting and conversion

Remove a level of nesting, or convert to an Option.

`Flatten` and `Transpose` are extension methods, in `Waystone.Monads.Results.Extensions` — add the `using`. `GetOk` and `GetErr` are on `Result<TOk, TErr>` itself and need nothing.

## Flatten

```csharp
Result<TOk, TErr> Flatten<TOk, TErr>(this Result<Result<TOk, TErr>, TErr> result)
```

Removes one level of nesting. You get here by calling `Map` with a function that itself returns a `Result`.

```csharp
Result<string, string> start = Result.Ok<string, string>("Storm Weaver");
Result<Result<int, string>, string> output = start.Map(x => CountRunes(x));
Result<int, string> flattened = output.Flatten();
```

**Prefer** [**`AndThen`**](/reference/result/transform#andthen), which does both steps at once. This exists for code that grew one method at a time.

{% hint style="info" %}
This is the single-result `Flatten`. The one that drops the failures out of a *sequence* is a different method — see [Collections](/reference/result/collections#flatten).
{% endhint %}

## Transpose

```csharp
Option<Result<TOk, TErr>> Transpose<TOk, TErr>(this Result<Option<TOk>, TErr> result)
```

Turns a result holding an option into an option holding a result.

```csharp
Result<Option<decimal>, string> calculationResult =
    CreateCalculator(Realm.TalDorei)
        .Map(calculator => calculator.GetToll(100.00m));

Option<Result<decimal, string>> maybeToll = calculationResult.Transpose();
```

Calling `Transpose` here declares that the absence of a toll is a valid outcome in your business rules.

**On an `Err`:** you get `Some(Err(…))` — the failure survives.

`Option<Result<T, E>>` transposes the other way. See [the Option page](/reference/option/nesting#transpose).

## GetOk

```csharp
Option<TOk> GetOk()
```

Converts to an `Option`, keeping the success value and discarding the error.

```csharp
Result<int, string> ok = Result.Ok<int, string>(1);
Result<int, string> err = Result.Err<int, string>("Error");

Option<int> some = ok.GetOk();
//          ^? Some(1)

Option<int> none = err.GetOk();
//          ^? None()
```

**The error is gone.** Use this only when you have already dealt with it, or do not care.

## GetErr

```csharp
Option<TErr> GetErr()
```

The other direction. Keeps the error and discards the success value.

```csharp
Option<string> none = ok.GetErr();
//             ^? None()

Option<string> some = err.GetErr();
//             ^? Some("Error")
```

## Going the other way

To convert an `Option` into a `Result`, see [`OkOr` and `OkOrElse`](/reference/option/nesting#okor).


# Collections

Methods for working with a sequence of Result\<TOk, TErr>.

A `List<Result<TOk, TErr>>` — the results of validating a batch, or of calling something once per item — comes up often enough to have its own methods.

Every method here is an extension method, in `Waystone.Monads.Results.Extensions`. Add the `using`.

## Flatten

```csharp
IEnumerable<TOk> Flatten<TOk, TErr>(this IEnumerable<Result<TOk, TErr>> source)
```

Keeps the successes and drops the failures, in the original order.

```csharp
List<Result<int, string>> results = [
    Result.Ok<int, string>(1),
    Result.Err<int, string>("bad"),
    Result.Ok<int, string>(3)
];

IEnumerable<int> values = results.Flatten();
//               ^? [1, 3]
```

## FlattenErr

```csharp
IEnumerable<TErr> FlattenErr<TOk, TErr>(this IEnumerable<Result<TOk, TErr>> source)
```

The other half. Keeps the failures and drops the successes.

```csharp
IEnumerable<string> errors = results.FlattenErr();
//                  ^? ["bad"]
```

{% hint style="info" %}
Both are lazy and each walks the source once. Call both on the same sequence and you enumerate it twice, which matters when the source is a database query or anything else you would rather not run again. Reach for [`Partition`](#partition) there.
{% endhint %}

## Partition

```csharp
(IReadOnlyList<TOk>, IReadOnlyList<TErr>) Partition<TOk, TErr>(
    this IEnumerable<Result<TOk, TErr>> source)
```

Both halves, reading the source once.

```csharp
(IReadOnlyList<int> oks, IReadOnlyList<string> errs) = results.Partition();
//                  ^? [1, 3]              ^? ["bad"]
```

**Eager.** It enumerates the source immediately and hands back two materialised lists.

```csharp
var (succeeded, failed) = items.Select(Validate).Partition();

if (failed.Count > 0)
{
    return Result.Err<Report, IReadOnlyList<string>>(failed);
}
```

This is the method to use when you owe the caller *every* failure, not just the first.

## Collect

```csharp
Result<IReadOnlyList<TOk>, TErr> Collect<TOk, TErr>(
    this IEnumerable<Result<TOk, TErr>> source)
```

For when the batch has to succeed as a whole. You get an `Ok` holding every value, or an `Err` carrying the **first** failure.

```csharp
List<Result<int, string>> results = [
    Result.Ok<int, string>(1),
    Result.Ok<int, string>(3)
];

Result<IReadOnlyList<int>, string> all = results.Collect();
//                                 ^? Ok([1, 3])
```

One failure fails the whole call:

```csharp
List<Result<int, string>> withAFailure = [
    Result.Ok<int, string>(1),
    Result.Err<int, string>("bad"),
    Result.Err<int, string>("worse")
];

Result<IReadOnlyList<int>, string> all = withAFailure.Collect();
//                                 ^? Err("bad")
```

**It stops at the first `Err`.** `"worse"` above is never seen, and anything that would have produced the later elements does not run.

**On an empty sequence:** you get `Ok` of an empty list. There is nothing in it to fail.

**The values before the failure are discarded.** If you need them, use [`Partition`](#partition).

Choose between the three by what you owe the caller:

| You need                               | Use         |
| -------------------------------------- | ----------- |
| All the values, or one failure         | `Collect`   |
| Every failure, to report them together | `Partition` |
| The successes, ignoring failures       | `Flatten`   |

{% hint style="info" %}
`Collect` is eager, and builds a list as it goes. Do not call it on an unbounded sequence.
{% endhint %}

## CollectAsync

```csharp
ValueTask<Result<IReadOnlyList<TOk>, TErr>> CollectAsync<TOk, TErr>(
    this IAsyncEnumerable<Result<TOk, TErr>> source,
    CancellationToken cancellationToken = default)
```

The same job over an `IAsyncEnumerable`.

```csharp
Result<IReadOnlyList<int>, string> all = await stream.CollectAsync(cancellationToken);
```

It stops pulling from the stream at the first `Err`, so the work behind the later elements never happens. That is the reason to use it rather than reading the whole stream into a list and calling `Collect`.

Returned `Task` up to 6.7.0. Returns `ValueTask` from 7.0.0.

## AsEnumerable

```csharp
IEnumerable<TOk> AsEnumerable<TOk, TErr>(this Result<TOk, TErr> result)
```

Treats a single result as a sequence of nothing or one, which is what lets the methods above compose out of LINQ. **It discards the error** — an `Err` becomes an empty sequence.

```csharp
Result<int, string> result = Result.Ok<int, string>(1);

IEnumerable<int> sequence = result.AsEnumerable();
//               ^? [1], and [] for an Err
```

To write a query that stays a `Result` and keeps the error, see [Waystone.Monads.Linq](/reference/packages/linq).

`Option<T>` has the same method, and `Flatten` on a sequence of either is built out of it.


# State overloads

Pass your data to a delegate instead of capturing it, and stop allocating a closure on every call.

There are two ways to hand your data to a delegate instead of letting it capture. Bind the data to the receiver with `With`, or pass it as the call's first argument. Both remove the closure.

```diff
-option.Map(value => value + offset);
+option.With(offset).Map(static (value, state) => value + state);
```

Both lines do the same thing. The second allocates 88 fewer bytes every time it runs — 24 for the display class the closure needs, 64 for the delegate.

**Use `With` first.** It reads in call order, it covers the whole async surface where the overloads cover only part of it, and it is what [`WM2017`](/reference/analyzers/idioms#wm2017) tells you to use.

This page applies to both `Option<T>` and `Result<T, E>`.

## `With` comes from the extensions namespace

`With` is an extension method. Add `using Waystone.Monads.Options.Extensions;` for `Option<T>`, or `using Waystone.Monads.Results.Extensions;` for `Result<T, E>`.

If `With` does not turn up on your option, that using is what is missing. It is the first thing about this API that looks broken.

## Bind the data, then call the method

`With(state)` gives you a binder carrying the same methods you already know. Each one hands your data to the delegate as its **last** argument, so the value comes first and reads the way it always did.

```csharp
Option<int> share = reward
    .With(partySize)
    .Map(static (gold, party) => gold / party);
```

### The data is spent, not sticky

Every method on the binder returns a plain `Option<T>` or `Result<T, E>`. Your data is gone the moment one call uses it, so the rest of the chain is ordinary. Bind again when you need it again.

```csharp
Option<int> total = reward
    .With(partySize)
    .Map(static (gold, party) => gold / party) // the state is spent here
    .Filter(static share => share > 0)         // so this is an ordinary call
    .With(bonus)                               // bind again to spend more
    .Map(static (share, extra) => share + extra);
```

There is no method for getting off the binder, because you are never on it for more than one call.

### Pass a tuple for more than one value

There is one slot. C# names tuple members after the variables you put in them, so the delegate reads `state.partySize` because you wrote `partySize` — you never have to invent a name.

```csharp
// C# names the members after the variables, so nothing is invented
string summary = quest
    .With((partySize, fallback))
    .Match(
        static (found, state) =>
            $"{found.Name} splits {found.GoldReward / state.partySize}",
        static state => state.fallback);
```

### A branch with no value takes the data alone

On `Option<T>` the delegates that run for `None` have no value to receive, so they take your data by itself. That is the `onNone` branch of `Match`, and all of `UnwrapOrElse`, `OrElse` and `OkOrElse`.

```csharp
// None has no value to hand over, so the delegate takes the state alone
int gold = reward.With(fallback).UnwrapOrElse(static state => state);
```

On `Result<T, E>` every delegate receives a value first — the success value or the error, depending on the branch it runs in.

```csharp
Result<Quest, string> tagged = attempt
    .With(realm)
    .MapErr(static (error, where) => $"{where}: {error}");
```

### It works on async delegates

This is the part the overloads cannot do. Every method on the binder has an `…Async` form, so an async delegate receives your data exactly as a synchronous one does.

```csharp
Option<int> share = await quest
    .With(partySize)
    .MapAsync(static async (found, party) => await ShareOf(found, party));
```

### The factories bind as well

`Try` and `TryAsync` are static, so you bind on the factory rather than on a value.

```csharp
Option<int> gold = Option.With(entry).Try(static text => int.Parse(text));
```

## Write the lambda `static`

This applies to both forms, and it is the part that is easy to get wrong. A lambda that happens not to capture measures the same as a `static` one — the compiler caches both. But nothing stops a later edit from reaching for an outer variable again, and the allocation comes straight back with no warning.

Marking the lambda `static` makes the compiler enforce it. Every example on this page does it — here it is on the overload form, but it makes no difference which form you pick:

```csharp
// the compiler rejects any capture in here
reward.Map(partySize, static (gold, party) => gold / party);
```

Write `static` every time. It costs nothing, and it is the only part of this the compiler checks for you.

## Passing the data as the first argument

The older form, and it is not going away. It still works, we still support it, and for a single call it does marginally less work because there is no binder to build. Use it where you prefer it.

### Where you can use it

| Type           | Methods                                                                                                                                             |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Option<T>`    | `IsSomeAnd`, `IsNoneOr`, `Match`, `Map`, `MapOr`, `MapOrDefault`, `MapOrElse`, `AndThen`, `Filter`, `Inspect`, `UnwrapOrElse`, `OrElse`, `OkOrElse` |
| `Result<T, E>` | `IsOkAnd`, `IsErrAnd`, `Match`, `Map`, `MapOr`, `MapOrDefault`, `MapOrElse`, `MapErr`, `AndThen`, `OrElse`, `UnwrapOrElse`, `Inspect`, `InspectErr` |
| Factories      | `Option.Try`, `Option.TryAsync`, `Result.Try`, `Result.TryAsync`                                                                                    |

`ZipWith` and `Reduce` are the exceptions, and they are not getting one. Both already hand their delegate every value the call involves, so there is normally nothing left for it to capture. The binder does not carry them either.

### What the delegate receives

Here your data is always the **first** argument of the call. What the delegate receives depends on whether the branch it runs in has a value to give it.

On `Result<T, E>` every delegate takes the value and then your data — the success value or the error, depending on which branch it is.

```csharp
result.UnwrapOrElse(fallback, static (error, state) => state);
```

On `Option<T>` the delegates that run when the option is `None` take your data alone, because there is no value to pass. That is the `onNone` branch of `Match`, and all of `UnwrapOrElse`, `OrElse` and `OkOrElse`.

```csharp
option.UnwrapOrElse(fallback, static state => state);
```

### Match saves the most

`Match` takes two delegates, so a capturing call pays twice. The two branches share one display class, but each one needs its own delegate — 152 bytes a call rather than 88.

```diff
-option.Match(
-    name => name.Length + offset,
-    () => fallback);
+option.Match(
+    (offset, fallback),
+    static (name, state) => state.offset + name.Length,
+    static state => state.fallback);
```

This is one argument too, so pass a tuple when you need more than one value. It works for both forms of `Match` — the one that returns a value, and the one that takes `Action` delegates and returns nothing.

### MapOrElse threads the same data through both delegates

`MapOrElse` takes two delegates and hands the same value to each one. Passing it to the map alone would leave the other delegate capturing, and the allocation would still be there.

```csharp
option.MapOrElse(
    fallback,
    static state => state,
    static (value, state) => value + state);
```

### How the overloads stay apart

Each state overload adds its parameter at the front and takes exactly one more argument than its closure sibling. That is what keeps the compiler from having to choose between them. Do not "tidy" a future overload by reusing an existing slot.

### On the async surface

Only some of the `…Async` methods have a state overload.

| Type           | Async methods that take state                                                                         |
| -------------- | ----------------------------------------------------------------------------------------------------- |
| `Option<T>`    | `IsNoneOrAsync`, `InspectAsync`, `MapOrDefaultAsync`                                                  |
| `Result<T, E>` | `IsOkAndAsync`, `IsErrAndAsync`, `MatchAsync`, `InspectAsync`, `InspectErrAsync`, `MapOrDefaultAsync` |

Everywhere else on the [async surface](/guides/async#the-full-surface), use `With`. The binder has an `…Async` form for every method it carries, so there is no gap to work around. You never have to `await` the task early just to get at a synchronous overload.

{% hint style="info" %}
[`WM2017`](/reference/analyzers/idioms#wm2017) reports a delegate that captures where `With` would avoid it, and its quick fix does the rewrite for you, so you do not have to find these by hand.
{% endhint %}


# Add-ons

Packages that add to Waystone.Monads itself. None is required, and none changes how the library behaves.

These extend `Waystone.Monads` itself. They keep their own `Waystone.Monads.*` namespaces, ship from the same repository on the same version number, and are purely additive. You install one because you want what it adds, not because an upgrade made you.

Packages that bridge to a library from somewhere else are under [Integrations](/reference/integrations).

## Pick a package

| If you need to                                       | Install                              | Page                                   |
| ---------------------------------------------------- | ------------------------------------ | -------------------------------------- |
| Parse untrusted input into a domain type             | `Waystone.Monads.Schemas`            | [Schemas](/reference/packages/schemas) |
| Write `from … select` over an `Option` or a `Result` | `Waystone.Monads.Linq`               | [LINQ](/reference/packages/linq)       |
| See the exceptions the library swallows              | `Waystone.Monads.Extensions.Logging` | [Logging](/reference/packages/logging) |

## Start with Schemas

[Schemas](/reference/packages/schemas) is the largest of the three, and the one most codebases have a use for. It parses untrusted input into a type you could not have built without passing, and reports every failure at once rather than the first. The [Schemas guide](/guides/schemas) builds one end to end and explains why you would want to.

## None of them changes the library

No package here changes the behaviour of anything in `Waystone.Monads`.

* Two add vocabulary. Remove [Schemas](/reference/packages/schemas) or [LINQ](/reference/packages/linq) and the code that used it stops compiling, and nothing else moves.
* One changes what reaches your logs. [Logging](/reference/packages/logging) reports the exceptions the library already swallowed. It does not stop them being swallowed.

[Observability](/guides/observability) covers the signals that need no package at all.


# Schemas

Parse untrusted input into a type you could not have built without passing, and get every failure back at once.

`Waystone.Monads.Schemas` — a parser that hands back a `Result`.

{% hint style="info" %}
**New to this? Start with the** [**Schemas guide**](/guides/schemas)**.** It builds one schema end to end and explains why you would want to. This page is the reference.
{% endhint %}

## What it adds

A schema takes one type in and gives another type out. The type it gives out is one your caller could not have constructed by hand, so holding it *is* the proof that the input passed.

Reach for it at the edge — a request body, a message off a queue, a row from a file. Skip it inside your domain, where the types already say what is true.

Comparing it against [FluentValidation](/reference/integrations/fluent-validation)? That one checks the object you built. This one builds it.

## Write the checks once

A schema is a value. Declare it, name it, and reuse it.

```csharp
public static class Guild
{
    public static readonly Schema<string, string> Title =
        Schema.Text.Trim().LengthBetween(3, 80);

    public static readonly Schema<string, string> Email =
        Schema.Text.Trim().Email();

    public static readonly Schema<decimal, decimal> Reward =
        Schema.Number.Decimal.Between(1m, 10_000m);
}
```

## Put them together

Two types are involved. The input is whatever arrived — every field nullable, nothing checked. The output is the type you actually wanted.

```csharp
public sealed record QuestDto(
    string? Title,
    string? PatronEmail,
    decimal? GoldReward,
    int? PartySize,
    QuestRank? Rank,
    string? Nickname);

/// <summary>The thing a parse produces.</summary>
public sealed class Quest
{
    internal Quest(
        string title,
        string patronEmail,
        decimal goldReward,
        Option<int> partySize)
    {
        Title = title;
        PatronEmail = patronEmail;
        GoldReward = goldReward;
        PartySize = partySize;
    }

    public string Title { get; }

    public string PatronEmail { get; }

    public decimal GoldReward { get; }

    public Option<int> PartySize { get; }
}
```

`Quest` has no public constructor, so the schema is the only way to get one. That is the part doing the work.

`QuestDto` carries two fields this schema ignores, which is what a real payload looks like — you parse what you need and leave the rest.

Now derive from `SchemaConfig<TIn, TOut>`, mark the class `partial`, and list the fields. The generator writes the rest.

```csharp
public partial class QuestSchema : SchemaConfig<QuestDto, Quest>
{
    protected override Result<Quest, SchemaViolation> Configure(QuestDto subject) =>
        Schema.Fields(
                   Schema.Required(subject.Title, Guild.Title),

                   // The path a caller is shown is "patron", not the property
                   // name the compiler read off the argument.
                   Schema.Required(subject.PatronEmail, Guild.Email)
                         .Named("patron"),
                   Schema.Required(subject.GoldReward, Guild.Reward),
                   Schema.Optional(subject.PartySize, Schema.Number.Int32.Positive()))
              .Into(
                   (title, patron, reward, party) =>
                       new Quest(title, patron, reward, party));
}
```

Two things in that snippet are worth a second look.

* `Schema.Fields` is generated into your class. It takes exactly the number of fields you passed, so `Into` is checked at compile time rather than at run time.
* `Schema.Optional` yields `Option<int>`, not `int?`. A missing value never reaches a rule and never reaches the constructor.

## Parse something

The generator also writes a shared `Instance`, so there is nothing to new up.

```csharp
Result<Quest, SchemaViolation> result =
    QuestSchema.Instance.Parse(posting);
```

## Read the failures

An `Err` carries a `SchemaViolation`. It holds every individual `Violation`, each with the path it was found at and a message written for a human.

```csharp
return QuestSchema.Instance.Parse(posting)
                  .Match(
                       quest => $"Accepted {quest.Title}.",
                       violation => string.Join(
                           "; ",
                           violation.Violations.Select(
                               failure =>
                                   $"{failure.Path}: {failure.Message}")));
```

`ToDictionary` gives the shape most APIs return — one entry per path, holding that path's messages.

```csharp
return QuestSchema.Instance.Parse(posting)
                  .Match(
                       _ => new Dictionary<string, string[]>(),
                       violation => violation.ToDictionary());
```

## One parse reports everything

A schema does not stop at the first problem. Three bad fields give three violations, so your caller fixes their payload once instead of three times.

```csharp
// An empty title, no patron, and a reward of zero.
SchemaViolation violation =
    QuestSchema.Instance
               .Parse(new QuestDto("", null, 0m, null, null, null))
               .UnwrapErr();

// Three, not one. A parse reports every field it could not accept.
int failures = violation.Violations.Count;
```

There is one exception, and it is deliberate. A failed [`Transform`](/reference/packages/schemas/composition#transform) produces no value, so the rules after it on *that* chain cannot run. Its siblings are unaffected and still report.

## Install it

```
dotnet add package Waystone.Monads.Schemas
```

The generator ships inside that package. There is nothing else to install and nothing to wire up.

The generator's own diagnostics use the `WMSC` prefix and are listed on [Generator diagnostics](/reference/source-generation/diagnostics#wmsc-schemas).

## Where to go next

| Page                                                   | Covers                                                                |
| ------------------------------------------------------ | --------------------------------------------------------------------- |
| [Primitives](/reference/packages/schemas/primitives)   | `Schema.Text`, `Schema.Number`, dates, enums, and the rules on each   |
| [Composition](/reference/packages/schemas/composition) | `Check`, `Transform`, `Not`, `When`, `All`, `Any`, messages and codes |
| [Structures](/reference/packages/schemas/structures)   | Lists, dictionaries, and the paths a violation carries                |
| [Field sets](/reference/packages/schemas/field-sets)   | `Required`, `Optional`, `Forbidden`, `Extend`, `Refine`               |
| [Asynchrony](/reference/packages/schemas/asynchrony)   | `CheckAsync`, `ParseAsync`, and where an async rule may not go        |


# Primitives

The schemas you start a chain from — text, numbers, identifiers, dates, booleans, enums — and the rules that hang off each.

Every chain starts at a primitive. The primitive fixes the type; the rules after it narrow what that type is allowed to hold.

## Text

`Schema.Text` accepts a `string`. Start with `Trim()` wherever the value came off a form — a trailing space is not something you want to reject over, and it is not something you want to store either.

```csharp
public static readonly Schema<string, string> Sigil =
    Schema.Text.Trim().LengthBetween(3, 24);

// A shape with a fixed width says so, rather than bounding both ends at the
// same number.
public static readonly Schema<string, string> CountryCode =
    Schema.Text.Trim().Length(2);

// A closed set of spellings. Schema.Enum is the better home when the domain
// already models the set as an enumeration.
public static readonly Schema<string, string> Difficulty =
    Schema.Text.Trim()
          .OneOf(
               global::System.StringComparison.OrdinalIgnoreCase,
               "easy",
               "standard",
               "deadly");
```

Length rules read as what they are. `Length(2)` is a fixed width; `LengthBetween`, `MinLength` and `MaxLength` are bounds; `NotEmpty` is the one you will reach for most.

### Patterns

`Matches` takes a `Regex`, not a pattern string. That is on purpose: it puts the choice of a match timeout in front of you rather than behind you.

```csharp
// Matches takes a Regex rather than a pattern string, which is what puts the
// choice of a match timeout in front of you. [GeneratedRegex] compiles the
// expression at build time instead of at start-up.
[GeneratedRegex("^[a-z-]+$", RegexOptions.None, matchTimeoutMilliseconds: 1000)]
private static partial Regex RunePattern { get; }

public static readonly Schema<string, string> Rune =
    Schema.Text.Matches(RunePattern);

// Build it by hand where the expression is not known at compile time. Give it
// a timeout: the pattern is yours, the value is not, and an expression with no
// ceiling runs against a crafted input for as long as that input takes.
public static readonly Schema<string, string> Incantation =
    Schema.Text.Matches(
        new Regex(
            @"^\p{L}[\p{L}\s]*$",
            RegexOptions.CultureInvariant,
            TimeSpan.FromSeconds(1)));
```

{% hint style="warning" %}
**Always give the expression a timeout.** The pattern is yours, but the value is not. An expression with no ceiling runs against a crafted input for as long as that input takes to defeat it.
{% endhint %}

### Shapes with names

Some shapes are common enough to have their own rule. Each one is checked by a scan rather than an expression, so there is no pattern to get subtly wrong.

```csharp
// Checked by a scan rather than an expression, so there is no pattern to get
// subtly wrong and no matching timeout to trip.
public static readonly Schema<string, string> PatronEmail =
    Schema.Text.Trim().Email();

// Restrict the scheme whenever the value will be followed or rendered. An
// absolute URL also includes javascript: and data:.
public static readonly Schema<string, string> Portrait =
    Schema.Text.Trim().Url("https");

// Literals, not expressions. A dot or a bracket here means itself.
public static readonly Schema<string, string> Tagged =
    Schema.Text.StartsWith("quest:").EndsWith(".md");
```

{% hint style="warning" %}
**`Url()` with no scheme accepts more than you think.** An absolute URL includes `javascript:`, `data:` and `file:`. Restrict the scheme whenever the value will be followed or rendered — which is nearly always. Passing an empty scheme list accepts nothing at all.
{% endhint %}

`StartsWith` and `EndsWith` take literals, not expressions. A dot or a bracket in one means itself.

## Numbers

Four number schemas, one per type: `Int32`, `Int64`, `Decimal` and `Double`.

```csharp
// One rule, so a party of twelve is one failure rather than two.
public static readonly Schema<int, int> PartySize =
    Schema.Number.Int32.Between(1, 6);

public static readonly Schema<long, long> ExperienceAwarded =
    Schema.Number.Int64.Positive();

// Exclusive at both ends, which is what GreaterThan and LessThan mean.
public static readonly Schema<decimal, decimal> GoldReward =
    Schema.Number.Decimal.GreaterThan(0m).LessThan(10_000m);

public static readonly Schema<double, double> SpellRangeMetres =
    Schema.Number.Double.Positive();
```

Inclusivity is in the name, and it matters.

| Rule                      | Bound         |
| ------------------------- | ------------- |
| `GreaterThan`, `LessThan` | Excluded      |
| `AtLeast`, `AtMost`       | Included      |
| `Between`                 | Both included |
| `Positive`, `Negative`    | Excludes zero |

Prefer one rule over two where one says the same thing. `Between(1, 6)` reports a party of twelve as one failure; `AtLeast(1).AtMost(6)` reports the same thing and is just longer to read.

## Identifiers

```csharp
public static readonly Schema<Guid, Guid> QuestId = Schema.Uuid.NotEmpty();

public static readonly Schema<Guid, Guid> PatronId = Schema.Uuid.IsVersion4();
```

`Schema.Uuid` accepts a `Guid`. `NotEmpty()` rejects `Guid.Empty`, which is what an uninitialised field deserializes to and is almost never a value you meant to receive.

It is named for the standard rather than the role, because its rules are about the UUID layout. An identifier that is not a UUID starts at `Schema.For<T>()`.

### Checking the version

`IsVersion4()` requires the value to have been generated at random, which is what `Guid.NewGuid()` produces. Use it where the identifier must carry nothing a reader can mine — no creation time, and no ordering someone could walk.

`IsVersion7()` requires the other way round: version 7 leads with a millisecond timestamp, so a run of them sorts by creation order. That is what makes it a good database key and a bad choice where the creation time is a secret.

Both read the version digits and nothing else. A value with them set to 4 passes even if the rest was not random — nothing in a UUID records how it was really made. Both also reject `Guid.Empty`, whose version digits are zero, so adding `NotEmpty()` alongside one of them says nothing new.

{% hint style="info" %}
`IsVersion7()` is on .NET 9 and later only, which is where `Guid.CreateVersion7()` arrived. A consumer on an earlier framework cannot produce one, so the package does not offer to check for one.

No other version has a rule, for the same reason: version 4 and version 7 are the only ones .NET creates.
{% endhint %}

## Dates and times

Two schemas, and picking between them is picking what the value means.

```csharp
// A moment, so a time zone is part of the value.
public static readonly Schema<DateTimeOffset, DateTimeOffset> Deadline =
    Schema.Timestamp.After(DateTimeOffset.UnixEpoch);

// A day, so a time of day would be noise. Not available on netstandard2.0.
public static readonly Schema<DateOnly, DateOnly> Founded =
    Schema.Date.OnOrAfter(new DateOnly(1066, 10, 14));

// Inclusivity is in the name. Before and After exclude the bound; OnOrBefore
// and OnOrAfter include it, which is what a closing date means.
public static readonly Schema<DateOnly, DateOnly> ClosesOn =
    Schema.Date.OnOrBefore(new DateOnly(2026, 12, 31));
```

* `Schema.Timestamp` is a `DateTimeOffset` — a moment, so a time zone is part of the value.
* `Schema.Date` is a `DateOnly` — a day, so a time of day would be noise.

`Schema.Date` is not available on `netstandard2.0`, because `DateOnly` is not.

Inclusivity is in the name here too. `Before` and `After` exclude the bound; `OnOrBefore` and `OnOrAfter` include it, which is what a closing date means.

## Booleans

```csharp
public static readonly Schema<bool, bool> AcceptedTerms =
    Schema.Bool.IsTrue();

// The rarer half, and worth a second look. A flag that has to be clear often
// reads better as the opposite flag that has to be set.
public static readonly Schema<bool, bool> NotSuspended =
    Schema.Bool.IsFalse();
```

`IsFalse` is worth a second look when you write it. A flag that has to be clear usually reads better as the opposite flag that has to be set.

## Enums

```csharp
// Rejects a value outside the declared members, which a cast can produce.
public static readonly Schema<QuestRank, QuestRank> Rank =
    Schema.Enum<QuestRank>();
```

`Schema.Enum<T>()` rejects a value outside the declared members. That is not a theoretical case — a cast produces one, and so does a deserializer handed a number.

## Anything else

`Schema.For<T>()` is the identity schema. It accepts any value of that type and gives `Check` and `Transform` somewhere to hang off.

```csharp
// For<T> is the identity schema: it accepts anything of that type and gives
// Check and Transform somewhere to hang off. Every primitive above is one.
public static readonly Schema<TimeSpan, TimeSpan> Duration =
    Schema.For<TimeSpan>()
          .Check(
               span => span > TimeSpan.Zero,
               ViolationCode.OutOfRange,
               "{Path} has to be longer than nothing, got {Received}.");
```

Every primitive above is a `Schema.For<T>()` with rules already attached. Reach for the bare form when your type has none — or when you want a rule over a whole subject, which is how [cross-field rules](/reference/packages/schemas/field-sets#a-rule-that-spans-two-fields) work.


# Composition

Add a rule, change the type, combine two schemas, and control the message and the code a failure carries.

Every schema is built the same way: start at a primitive, and add. This page covers what you can add.

## Check

`Check` adds a rule. The value survives a failure, so every later rule on the chain still runs and one parse reports all of them.

```csharp
public static readonly Schema<string, string> Title =
    Schema.Text.Trim()
          .NotEmpty()
          .Check(
               title => !title.Contains("dragon", StringComparison.OrdinalIgnoreCase),
               ViolationCode.NotAllowed,
               "{Path} may not name a dragon, got {Received}.");
```

A rule takes three things: the predicate, a code a caller can branch on, and a message a human reads. The message is a template — `{Path}`, `{Received}`, `{Predicate}` and `{Code}` are filled in for you.

### Naming the condition in the message

Writing the rule out twice — once as the predicate, once as prose — is how those two drift apart. `{Predicate}` renders the rule's own source text, so you write the condition once.

```csharp
// {Predicate} renders the rule's own source text, so the condition is
// written once. A failure here reads: "Expected reward to satisfy
// reward => reward % 10 == 0."
public static readonly Schema<int, int> Reward =
    Schema.Number.Int32.Positive()
          .Check(
               reward => reward % 10 == 0,
               ViolationCode.Mismatched,
               "Expected {Path} to satisfy {Predicate}.");
```

You pass nothing to get this. The compiler captures the text for you.

Where the lambda reads badly in the middle of a sentence, pass your own wording as a fourth argument.

```csharp
// The fourth argument replaces that source text where the lambda reads
// badly mid-sentence: "Expected reward to satisfy a multiple of ten."
public static readonly Schema<int, int> RoundReward =
    Schema.Number.Int32.Positive()
          .Check(
               reward => reward % 10 == 0,
               ViolationCode.Mismatched,
               "Expected {Path} to satisfy {Predicate}.",
               "a multiple of ten");
```

{% hint style="info" %}
`{Expected}` is the one token `Check` cannot fill. It renders a bound, and `Check` has nowhere for you to put one, so it reaches your caller as those exact characters. The rules that ship with the package — `AtLeast`, `MaxLength` and the rest — supply their own bound and do fill it. In a message you write, either interpolate the bound yourself or reach for `{Predicate}`.
{% endhint %}

## Transform

`Transform` changes the type the schema produces. From the transform onward, the chain is over the new type.

```csharp
public static readonly Schema<string, QuestTitle> Titled =
    Schema.Text.Trim().NotEmpty().Transform(text => new QuestTitle(text));
```

### A transform that can fail

The second overload returns a `Result`. An `Err` becomes a violation.

```csharp
public static readonly Schema<string, QuestRank> Rank =
    Schema.Text.Trim()
          .Transform(
               text => Enum.TryParse(text, true, out QuestRank rank)
                   ? Result.Ok<QuestRank, Error>(rank)
                   : Result.Err<QuestRank, Error>(
                       ViolationCodeCatalog.Errors.Malformed(
                           $"'{text}' is not a rank.")));
```

**This is the one seam in the "report everything" promise.** A refinement fails and the value survives, so the rest of that chain runs. A transform fails and there is no value to carry, so its chain stops there. Its siblings in a field set are unaffected and still report.

**A conversion that returns `null` is a violation, not an exception.** If the function you passed to the non-`Result` overload returns `null`, the parse reports a `Malformed` violation at that path and carries on gathering. It does not throw. Reach for the `Result` overload anyway when a conversion can fail — it lets you say *why*.

## Not

`Not` inverts a schema you already have.

```csharp
// A schema worth naming, so Not has something to invert.
public static readonly Schema<string, string> ReservedPrefixes =
    Schema.Text.StartsWith("guild:");

public static readonly Schema<string, string> PublicTitle =
    Schema.Text.Trim()
          .NotEmpty()
          .Not(ReservedPrefixes, "{Path} may not use a reserved prefix.");
```

Negation has no message of its own to borrow, so one is required.

Reach for `Not` when the thing being rejected is already a schema worth naming. For a one-off condition, `Check` with the negated predicate reads better and costs less.

## When and unless

Both take the whole value, so they read as a condition on the subject rather than on one rule.

```csharp
public static readonly Schema<string, string> SigilOfALongName =
    Schema.Text.MinLength(8).When(text => text.StartsWith("guild:"));

public static readonly Schema<string, string> NoShoutingUnlessUrgent =
    Schema.Text.Matches(NoCapitals).Unless(text => text.EndsWith("!"));
```

The rules run only when the predicate holds. `Unless` is the same thing with the predicate inverted.

## All

Every branch runs, and every failure is reported.

```csharp
public static readonly Schema<string, string> Passphrase = Schema.All(
    Schema.Text.MinLength(12),
    Schema.Text.Matches(HasADigit),
    Schema.Text.Matches(HasASymbol));
```

A passphrase that is too short, has no digit and has no symbol comes back with three violations, not one.

## Any

The first branch that accepts wins.

```csharp
// An email address or a phone number, either being fine.
public static readonly Schema<string, string> Contact = Schema.Any(
    Schema.Text.Matches(LooksLikeAnEmail),
    Schema.Text.Matches(LooksLikeAPhone));
```

When no branch accepts, you get one violation at the `Schema.Any` schema's own path, with each branch's failures nested beneath it.

## Messages

`WithMessage` replaces the message of every violation the chain produced, not only the last one.

```csharp
// Four rules, one message.
public static readonly Schema<string, string> Slug =
    Schema.Text.Trim()
          .NotEmpty()
          .MaxLength(40)
          .Matches(LowerCaseAndHyphens)
          .WithMessage("{Path} has to be lower case words joined by hyphens.");
```

Reach for it when the rules are an implementation detail and you only need to say what shape you expected. Skip it when the individual messages are what makes the failure useful.

`{Expected}` and `{Predicate}` reach your caller as those exact characters here. One message now stands for four rules, so there is no single bound or predicate left to name. Say it in the text instead.

## Codes

`WithCode` sets a domain code, so a caller can branch on the failure without matching text.

```csharp
public static readonly Schema<string, string> ReservedTitle =
    Schema.Text.Not(ReservedPrefixes, "Reserved prefix.")
          .WithCode(new ErrorCode("quest.title_reserved"));
```

The built-in `ViolationCode` values cover the generic cases — `Incomplete`, `Malformed`, `NotAllowed`, `OutOfRange`, `Mismatched`, `Duplicate`, `Conflicting` and `Truncated`. Reach for one of those when a domain code would only restate the check.

## Names

A violation's path is derived from the expression you passed, which is usually the property name and is occasionally not what you want a caller to see.

```csharp
// A violation reports "patron", not "patronEmail".
return Schema.Required(subject.PatronEmail, Schema.Text.Email())
             .Named("patron");
```

**Set the name on the field, not on the schema.** A schema is shared, so a name baked into one renames every field of its shape and nothing reports it. A field is built per parse and cannot leak.

`Schema.Named` is the other half, for a schema that is not reached through a field — a branch of `Schema.Any`, or one handed straight to `Parse`.

```csharp
// Naming the branches, so a failure says which one was tried.
public static readonly Schema<string, string> Contactable = Schema.Any(
    Schema.Text.Email().Named("email"),
    Schema.Text.Matches(LooksLikeAPhone).Named("phone"));
```

## Sensitive values

`{Received}` renders the value that was rejected. That is what makes most messages useful, and it is exactly wrong for a password, a token or a tax file number — those would land in your logs and in your API response.

`Sensitive()` opts that path out.

```csharp
// {Received} renders *** for this schema and everything beneath it, so the
// rejected value stays out of logs and out of the response. Opt-in, because
// seeing what was rejected is what makes most messages useful.
public static readonly Schema<string, string> Secret =
    Schema.Text.NotEmpty().MinLength(12).Sensitive();
```

`{Received}` then renders `***` for this schema and everything beneath it.

Three things about it are worth knowing.

* **Mark the outermost schema and stop.** Everything nested inside it is redacted too, including a nested schema that reported before the outer one ran. Marking an inner schema as well changes nothing.
* **`{Expected}` and `{Predicate}` are not redacted.** One renders a bound your schema's author wrote and the other their rule's source text. Neither is anything that arrived from outside.
* **The raw value cannot be read back.** A `Violation` exposes its path, its code and its rendered message, and nothing else. There is no way to recover the value the redaction exists to withhold.


# Structures

Parse lists and dictionaries, read the path a violation was found at, and cap what a hostile payload can cost you.

A list or a dictionary is parsed with a schema per part. The item schema can be any schema — including one you wrote.

## Lists

```csharp
// At least one objective, at most ten, and each one trimmed and bounded.
public static readonly Schema<IReadOnlyList<string>, IReadOnlyList<string>>
    Objectives = Schema.List(Schema.Text.Trim().NotEmpty().MaxLength(120))
                       .MinCount(1)
                       .MaxCount(10);
```

Every item is parsed, so a bad item at index 3 does not hide a bad one at index 7. Both are reported.

`MaxCount` is the exception. It counts *before* parsing anything, which is what makes it a guard on untrusted input rather than a report afterwards. An eleventh objective is rejected on its own, with nothing said about the ten.

The item schema is just a schema, so nothing stops it being one of yours.

```csharp
public static readonly Schema<IReadOnlyList<LeaderDto>, IReadOnlyList<Leader>>
    Leaders = Schema.List(LeaderSchema.Instance).MinCount(1);
```

## Dictionaries

```csharp
// A schema for the keys and a schema for the values.
public static readonly
    Schema<IReadOnlyDictionary<string, int>, IReadOnlyDictionary<string, int>>
    Bounties = Schema.Dictionary(
                          Schema.Text.Trim().Matches(BountyName),
                          Schema.Number.Int32.Positive())
                     .MaxCount(50);
```

Keys are parsed too, so a malformed key is a violation rather than a silent entry nobody ever looks up. `MaxCount` counts first here as well — a fifty-first entry is rejected before any key is read.

## Paths

A violation inside a structure carries where it was found. The path reads `[1]` for a list position and `leader.email` through a nested schema; in a field set both are prefixed by the field, giving `objectives[1]`.

```csharp
// A violation inside a structure carries where it was found, so the path
// reads "[1]" for a list and "leader.email" through a nested schema. In a
// field set both are prefixed by the field: "objectives[1]".
SchemaViolation violation =
    Objectives.Parse(["Rescue the cleric", "  "]).UnwrapErr();

IReadOnlyList<string> paths =
    violation.Violations.Select(failure => failure.Path.ToString())
             .ToList();
```

### Reading a path in code

The rendered path is written for a human. Branch on the segments instead.

```csharp
// The rendered path is written for a human. Branch on the segments
// instead: a list position, a dictionary key and a failed Schema.Any
// branch all render inside brackets, and only the segment says which
// one you are looking at.
SchemaViolation violation =
    Objectives.Parse(["Rescue the cleric", "  "]).UnwrapErr();

PathSegment last = violation.Violations[0].Path.Segments[^1];

string located = last.Kind switch
{
    PathSegmentKind.Index => $"entry {last.Text}",
    PathSegmentKind.Key => $"key {last.Text}",
    PathSegmentKind.Branch => $"alternative {last.Text}",
    _ => last.Text,
};
```

A list position, a dictionary key and a failed `Schema.Any` branch all render inside brackets. Only the segment's `Kind` tells you which one you are looking at.

## The report is capped

One list or dictionary reports at most 64 problems and then stops.

```csharp
// One list or dictionary reports at most 64 problems and then stops, so a
// hostile payload cannot make the report as expensive as it likes. When it
// stops, it says so with a truncated violation rather than trailing off.
string[] blanks = Enumerable.Repeat("  ", 500).ToArray();

SchemaViolation violation =
    Schema.List(Schema.Text.NotEmpty()).Parse(blanks).UnwrapErr();

bool thereAreMore = violation.Violations.Any(
    failure =>
        failure.Code == ViolationCodeCatalog.Codes.Truncated);
```

That cap is a guard, not a limitation of the design. Without it, a payload of ten thousand blank strings costs ten thousand rendered messages to reject — and whoever sent it chose the number.

When the cap is reached, the report says so with a `Truncated` violation rather than trailing off. Check for that code before you tell a caller the list they sent has exactly 64 problems.


# Field sets

Turn several fields into one object, gate a parse without producing a value, and write a rule that spans two fields.

A field set is how several schemas become one object. You list the fields; the generator writes the code that assembles them.

Derive from `SchemaConfig<TIn, TOut>` and mark the class `partial`. There is no attribute. Three rules apply, and each one is the reason a first attempt does not compile:

* The class, and every type containing it, has to be `partial` — [`WMSC0001`](/reference/source-generation/diagnostics#wmsc0001).
* It needs a constructor you can call with no arguments — [`WMSC0002`](/reference/source-generation/diagnostics#wmsc0002).
* Do not declare a member called `Instance`, `Schema` or `FieldSet` — [`WMSC0003`](/reference/source-generation/diagnostics#wmsc0003).

## The four kinds of field

### Required

```csharp
Field<string> name =
    Schema.Required(subject.Name, Schema.Text.Trim().NotEmpty());
```

Absent, `null`, or failing the schema is a violation at that field's path. The value reaches your constructor as a plain `string`, never a `string?`.

An optional third argument overrides the message for the value being *absent*. The schema's own rules keep the messages they came with.

```csharp
Field<string> title =
    Schema.Required(subject.Title, Guild.Title, "Every party needs {Path}.");
```

### Optional

```csharp
Field<Option<int>> size =
    Schema.Optional(subject.Size, Schema.Number.Int32.AtLeast(1).AtMost(6));
```

Absent is accepted. The value arrives as `Option<int>`, so `null` never reaches a rule and never reaches the constructed object.

A value that *is* present still has to pass the schema. Optional means "may be absent", not "may be wrong".

### Forbidden

```csharp
Field<Checked> legacy =
    Schema.Forbidden(subject.LegacyId, "Do not send {Path}.");
```

Reject a value at a path where you allow none.

This is the first of the three fields that yield **`Checked`**. `Checked` means "this rule passed, and it has nothing to hand you" — the field gates the parse without contributing to the object. Fields like that go to `Refine` and take no slot in the `Into` lambda.

### Extend

```csharp
Field<Checked> chronology = Schema.Extend(subject, Chronology);
```

Runs a schema over the whole subject. Another `Checked` field, so it goes to `Refine` too.

### Checked

The other two `Checked` fields are built that way from the start. This one is made from a field that does parse a value, when you want the rules but not the value.

```csharp
Field<Checked> confirmation =
    Schema.Required(subject.ConfirmEmail, Schema.Text.Email())
          .AsChecked();
```

Reach for it when the caller has to send a field correctly but your type has no place for it — part of a wire contract another system reads, or an address you check and never store.

The value goes. Everything else stays: the rules still run, and a failure is still reported at that field's own path, so a caller is told which field was wrong.

Hand the result to `Refine`. Both kinds of field go there the same way — `Required` when the caller has to send it, `Optional` when they may.

```csharp
public partial class RecruitSchema : SchemaConfig<RecruitDto, Recruit>
{
    protected override Result<Recruit, SchemaViolation> Configure(
        RecruitDto subject) =>
        Schema.Fields(
                   Schema.Required(subject.Name, Schema.Text.Trim().NotEmpty()),
                   Schema.Required(subject.Email, Guild.Email))

              // Either kind reaches Refine the same way. Required still means the
              // caller has to send it and Optional still means they may — AsChecked
              // drops the value and nothing else.
              .Refine(
                   Schema.Required(subject.ConfirmEmail, Guild.Email).AsChecked(),
                   Schema.Optional(subject.Referral, Schema.Text.Trim().NotEmpty())
                         .AsChecked())
              .Into((name, email) => new Recruit(name, email));
}
```

`Schema.Forbidden` is not the same thing. It says the field must be **absent**, and these fields are allowed. `Schema.Extend` is not either — it reports at the subject's path rather than the field's, so a caller reading the violations by field name finds nothing under the name they sent.

## A rule that spans two fields

`Schema.For<T>()` over the subject is the right home for a rule about more than one field. Its violations land at the subject's own path rather than under a field name, which is honest — the failure belongs to neither field alone.

```csharp
// A rule about two fields at once, so it takes the whole subject.
public static readonly Schema<PartyDto, PartyDto> Chronology =
    Schema.For<PartyDto>()
          .Check(
               party => party.Disbanded is null
                     || party.Formed is null
                     || party.Disbanded > party.Formed,
               ViolationCode.Conflicting,
               "A party cannot disband before it forms.");
```

Hand it to the field set with `Schema.Extend`.

## Putting it together

```csharp
public partial class PartySchema : SchemaConfig<PartyDto, Party>
{
    protected override Result<Party, SchemaViolation> Configure(PartyDto subject) =>
        Schema.Fields(
                   Schema.Required(subject.Name, Schema.Text.Trim().NotEmpty()),

                   // A nested schema is just a schema. Its violations arrive under
                   // "leader", so a reader is told which one failed.
                   Schema.Required(subject.Leader, LeaderSchema.Instance),
                   Schema.Optional(subject.Size, Schema.Number.Int32.AtLeast(1)))

              // Refine takes fields that gate the parse without producing a value,
              // so the Into lambda keeps one parameter per field above and no
              // discards.
              .Refine(
                   Schema.Forbidden(subject.LegacyId, "Do not send {Path}."),
                   Schema.Extend(subject, FieldSetsPage.Chronology))
              .Into((name, leader, size) => new Party(name, leader, size));
}
```

`Refine` takes the fields that gate the parse without producing a value, so the `Into` lambda keeps one parameter per field in `Schema.Fields` — in order, and with no discards.

**Pass only value-free fields to `Refine`.** It takes the non-generic `Field` base, which drops the value side, so it will accept a field that parses something and then throw that something away. [`WMSC0005`](/reference/source-generation/diagnostics#wmsc0005) warns when you do.

When you mean it, say so with `AsChecked` rather than listing the field in `Schema.Fields` and discarding it with a `_` in the lambda. Those discards are positional: add, remove or reorder a field and the parameters you did want quietly bind to different fields whenever the types line up. `AsChecked` also keeps `WMSC0005` working on the fields you did not mean to discard, which turning the warning off would not.

A nested schema is just a schema. Its violations arrive under the field's name, so a reader is told which one failed.

## Gating without building

Some schemas exist only to say yes. Finish those with `Checked()` instead of `Into`.

```csharp
// A schema that only gates finishes with Checked. There is nothing to construct,
// so there is no lambda and nothing to name.
public partial class ConsentSchema : SchemaConfig<ConsentDto, Checked>
{
    protected override Result<Checked, SchemaViolation> Configure(
        ConsentDto subject) =>
        Schema.Fields(
                   Schema.Required(subject.Terms, Schema.Text.NotEmpty()),
                   Schema.Required(subject.Privacy, Schema.Text.NotEmpty()))
              .Checked();
}
```

There is nothing to construct, so there is no lambda and nothing to name.

## What the generator writes

A shared `Instance`, and the `Fields`, `Refine`, `Into` and `Checked` members you call.

`Fields` is written at exactly your field count. That is why a wrong-sized `Into` lambda is a compile error rather than a surprise in production — [`WMSC0004`](/reference/source-generation/diagnostics#wmsc0004).

## Paths come from your source

A field's path is read from the expression you passed, using `CallerArgumentExpression`. `subject.Title` gives `title`, which is the case the whole design is built around.

Anything else keeps its punctuation. A method call, an indexer or a null-forgiving operator gives a path that reaches your logs and your API responses looking like source code. [`WMSC0008`](/reference/source-generation/diagnostics#wmsc0008) warns when that happens; add `.Named("...")` to fix it.


# Asynchrony

Rules that have to ask something — a database, a service — and the one place they may not go.

Most rules answer from the value alone. Some have to ask something else: is this title already taken, does this account exist. `CheckAsync` is for those.

## CheckAsync

```csharp
// The cheap rules first, then the round trip.
public static Schema<string, string> UniqueTitle(IQuestBoard board) =>
    Schema.Text.Trim()
          .NotEmpty()
          .MaxLength(80)
          .CheckAsync(
               board.TitleIsFree,
               ViolationCode.Duplicate,
               "{Path} is already on the board, got {Received}.");
```

The rule is handed the value and the parse's own cancellation token. It runs only when everything before it accepted, so it never sees a value the chain could not produce — your round trip is not spent on input that was already going to fail.

Otherwise it is `Check`. Same three arguments, same behaviour on failure: the value survives and the rest of the chain still runs.

## ParseAsync

```csharp
Result<string, SchemaViolation> result =
    await UniqueTitle(board).ParseAsync(title, cancellationToken);
```

`ParseAsync` returns a `ValueTask<Result<TOut, SchemaViolation>>`. Pass a cancellation token; the rule you wrote is given the same one.

A schema with no asynchronous rules can still be parsed with `ParseAsync`. It just completes without ever yielding.

## An async rule cannot live in a field set

This is the rule to remember on this page.

`SchemaConfig.Configure` returns a value rather than a task, so a field set only ever runs the synchronous path — even when the caller used `ParseAsync`. An asynchronous rule reached from there throws `InvalidOperationException`. Nothing in the type system says so, because `CheckAsync` returns the same schema type a synchronous rule does.

[`WMSC0006`](/reference/source-generation/diagnostics#wmsc0006) reports it at build time, which is where you would rather find out.

### Compose it around the outside instead

```csharp
// SchemaConfig.Configure returns a value rather than a task, so a field set
// only ever runs the synchronous path. An asynchronous rule reached from there
// throws, and WMSC0006 reports it at build time rather than in production.
//
// Compose the rule around the generated schema instead. The field set stays
// synchronous and the round trip happens once, after every cheap rule has
// already had its say.
public static ValueTask<Result<Quest, SchemaViolation>> ParseAgainstTheBoard(
    QuestDto posting,
    IQuestBoard board,
    CancellationToken cancellationToken) =>
    QuestSchema.Instance
               .CheckAsync(
                    (quest, token) => board.TitleIsFree(quest.Title, token),
                    ViolationCode.Duplicate,
                    "That quest is already on the board.")
               .ParseAsync(posting, cancellationToken);
```

The field set stays synchronous. The round trip happens once, after every cheap rule has already had its say — so a payload with three malformed fields never reaches your database at all.


# LINQ

C# query syntax over Option and Result, with no change in behaviour.

`Waystone.Monads.Linq` — C# query syntax over `Option` and `Result`.

## What it adds

`Select`, `SelectMany` and `Where`, under those names, so a `from … select` query compiles over an `Option<T>` or a `Result<TOk, TErr>`.

```csharp
using Waystone.Monads.Linq;

Option<Quote> quote =
    from customer in FindCustomer(id)
    from address in customer.PostalAddress
    from rate in RateFor(address)
    select Price(customer, rate);
```

Every step there returns an `Option`. If any one of them is `None`, the query stops and the result is `None` — you never write the check.

That is the same thing `AndThen` does, spelled differently:

```csharp
Option<Quote> quote = FindCustomer(id)
    .AndThen(customer => customer.PostalAddress
        .AndThen(address => RateFor(address)
            .Map(rate => Price(customer, rate))));
```

Pick whichever reads better to you. Three or more steps that each need the earlier values is where query syntax wins, because the nesting flattens out.

## When to reach for it

Reach for it when a chain of `AndThen` calls has grown enough that the names of the intermediate values stop being obvious. A `from … select` query names each step.

Skip it for a single step. `option.Map(x => x + 1)` is already shorter than the query that does the same thing.

## Install it

```
dotnet add package Waystone.Monads.Linq
```

Then add one line to the file that needs it:

```csharp
using Waystone.Monads.Linq;
```

That is the whole opt-in. It works alongside `using System.Linq;` — the names do not collide, because these are extensions on `Option<T>` and `Result<TOk, TErr>` rather than on `IEnumerable<T>`.

The package is separate so the core library stays free of the LINQ names. Nothing in `Waystone.Monads` gains or loses a member when you install it.

## What each clause maps to

| You write                 | It calls     | Which is              |
| ------------------------- | ------------ | --------------------- |
| `select`                  | `Select`     | `Map`                 |
| a second and later `from` | `SelectMany` | `AndThen`, then `Map` |
| `where`                   | `Where`      | `Filter`              |

Every member forwards to the core member and adds no behaviour. Two spellings, one implementation. Use `Map` in a method-syntax chain and `select` in a query — do not mix them in the same expression.

## Result has no `where` clause

This is the one place a query over a `Result` is poorer than a query over an `Option`, and it is a decision rather than an omission.

```csharp
// Does not compile.
Result<int, string> positive =
    from n in GetResult()
    where n > 0
    select n;
```

Discarding an Ok value would have to invent the error that replaces it, and a signature taking an error factory is not the one query syntax binds a `where` clause to. So there is no `Where` on `Result` at all.

Two ways round it:

* Filter before you enter the query.
* Use `Filter` on an `Option`, then `OkOr` to get back to a `Result` with an error you chose.

`Option` has `Where`, and it behaves as you would expect — a value that fails the predicate becomes `None`.

## Every step must share one error type

A query over `Result` threads one `TErr` through the whole chain, because that is what `AndThen` does. A step that fails with a different error type has to be mapped onto the shared one first, with `MapErr`, before it can join the query.

There is no LINQ name for projecting the error half. `MapErr` is the only spelling.

## There are no async shapes

Query expressions cannot await, so there is no `SelectAsync` and no `SelectManyAsync`. Async chaining is already covered by `MapAsync` and `AndThenAsync` in the core package — see [Async](/guides/async).

## This is not `AsEnumerable`

Both let an `Option` meet LINQ. They do opposite things.

|                        | Stays a monad? | Then what                                                                                   |
| ---------------------- | -------------- | ------------------------------------------------------------------------------------------- |
| `Waystone.Monads.Linq` | **Yes**        | `Select` on an `Option<T>` gives you an `Option<TOut>`                                      |
| `AsEnumerable()`       | **No**         | Gives you an `IEnumerable<T>` of zero or one items, and you are in `System.Linq` from there |

`AsEnumerable` is the way *out*, and it loses things on the way. On a `Result` it drops the error — an `Err` becomes an empty sequence, with nothing left to say why. Use it when you genuinely want a sequence, usually to concatenate several options together.

This package is the way to *stay in*. Nothing is materialised, nothing is enumerated, and the error survives.

## It costs no more than the method it forwards to

The three-argument `SelectMany` (the one a multi-clause `from` binds to) threads both of your delegates through the core state-passing overloads using `static` lambdas, so it captures nothing. A query does not allocate a closure that the equivalent `AndThen` chain would avoid.

## One naming wrinkle

If you call these as methods with named arguments, note that the factory parameters follow this library's chain-step naming (`optionFactory` and `resultFactory`) while `resultSelector` keeps the name LINQ gives it. Positional calls and query syntax are unaffected.

## What it does not do

* It adds no async shapes. C# query syntax has no `await`, so there is nothing to bind to.
* It gives `Result` no `where` clause, because filtering out an `Ok` has no error to fall back on.
* It changes no behaviour. Every clause forwards to a method that already existed.


# Logging

See every exception the library swallows, as a log entry with the call site attached.

`Waystone.Monads.Extensions.Logging` — sends the exceptions the library catches to your `ILogger`.

## What it adds

`UseLoggerFactory` and `UseLoggerFactoryFrom` on `MonadOptions`. Once one of them is called, every exception `Option.Try`, `Result.Try` and their async siblings swallow becomes a log entry with the call site attached.

## When to reach for it

Reach for it when the library is catching exceptions you would otherwise never see. A `Try` that turns a throw into a `None` is doing its job, and it is also the one place a real bug can hide silently.

It is the only signal that needs a package. Counters and diagnostic events work with no install at all — see [Observability](/guides/observability) for those.

## Install it

```
dotnet add package Waystone.Monads.Extensions.Logging
```

## Point it at your logger

Do this once, during start-up. Pick the line that matches how your application is built.

**You have a host and a service provider:**

```csharp
var app = builder.Build();

MonadOptions.Configure(options => options.UseLoggerFactoryFrom(app.Services));
```

**You have no container** — a console app, a test, a worker you built by hand:

```csharp
using ILoggerFactory factory = LoggerFactory.Create(
    builder => builder.AddConsole());

MonadOptions.Configure(options => options.UseLoggerFactory(factory));
```

{% hint style="info" %}
`LoggerFactory.Create` is not in `Microsoft.Extensions.Logging.Abstractions`, which is all this package brings with it. Add `Microsoft.Extensions.Logging` to build a factory yourself, plus a provider package such as `Microsoft.Extensions.Logging.Console`. An application with a host already has both.
{% endhint %}

**You already hold a logger:**

```csharp
MonadOptions.Configure(options => options.UseLogger(logger));
```

`UseLoggerFactoryFrom` asks the provider for an `ILoggerFactory` through `IServiceProvider.GetService`. It takes no dependency-injection package, so any container that can hand you a provider works — not just Microsoft's. If no factory is registered it throws and tells you to call `UseLoggerFactory` instead.

### What you get in each entry

The exception itself, plus three properties describing the call site the compiler recorded for you:

| Property             | What it holds                              |
| -------------------- | ------------------------------------------ |
| `MemberName`         | The member that called `Try`               |
| `ArgumentExpression` | The source text of the delegate you passed |
| `LineNumber`         | The line the `Try` call sits on            |

The exception goes in the logger's exception parameter, not into properties of its own. Your OpenTelemetry logging bridge reads `exception.type`, `exception.message` and `exception.stacktrace` off it, so you get those for free and you get them once.

{% hint style="info" %}
**Property names are PascalCase on purpose.** Serilog only accepts property names matching `[A-Za-z0-9_]+`. Write `{code.function.name}` in a message template and Serilog prints it as text instead of binding it. So the log properties use PascalCase, and the dotted OpenTelemetry spellings stay on the metric tags, where nothing parses a template.
{% endhint %}

### Choose the level

The default is `Debug`. A `Try` that hands back a `None` or an `Err` did what you asked it to, so warning about it fills your logs with noise.

Pass a different level if you disagree:

```csharp
MonadOptions.Configure(
    options => options.UseLoggerFactoryFrom(app.Services, LogLevel.Warning));
```

{% hint style="info" %}
OpenTelemetry's semantic conventions suggest `WARN` for an exception the application expects to handle. We default to `Debug` instead. Pass `LogLevel.Warning` to follow the convention.
{% endhint %}

### Filter the library's own output

`UseLoggerFactory` and `UseLoggerFactoryFrom` create a logger in the `Waystone.Monads` category, so you can turn the library up or down without touching anything else:

```json
{ "Logging": { "LogLevel": { "Waystone.Monads": "Warning" } } }
```

`UseLogger` does not do this — the logger you pass keeps whatever category it already had.

### Change the logger for one block

Both the logger and the level live on the `MonadOptions` scope, so [`BeginScope`](/guides/configuration#scoped-configuration) redirects them for one asynchronous flow and leaves the rest of your process alone:

```csharp
using (MonadOptions.BeginScope(
    options => options.UseLogger(captured, LogLevel.Warning)))
{
    // Everything in here logs at Warning, to `captured`.
}
```

This is what makes the logging usable in tests that run in parallel.

## Replacing UseExceptionLogger

`MonadOptions.UseExceptionLogger` used to be the only way to see these exceptions. It was obsolete from 6.7.0 and **7.0.0 removes it**.

```diff
-MonadOptions.Configure(options => options.UseExceptionLogger((ex, caller) =>
-{
-    Console.WriteLine($"{ex} at {caller.MemberName}:{caller.LineNumber}");
-}));
+MonadOptions.Configure(options => options.UseLoggerFactoryFrom(app.Services));
```

{% hint style="warning" %}
**Do not configure both.** They both still fire in 6.x, so every handled exception is reported twice until you delete the old call.
{% endhint %}

The old hook held one delegate. A second observer replaced the first without saying so, which meant it could never support more than one integration at a time. The diagnostic event has no such limit.

## What it does not do

* It does not log anything you handle yourself. Only the exceptions the library catches reach it.
* It does not replace `Observability`. Counters and the diagnostic events are in the core package and need no install.
* It does not accept more than one logger factory. The last call wins, and a scope overrides it for the block.


# Integrations

Packages that connect Waystone.Monads to a library you already use. None is required, and none changes how Waystone.Monads behaves.

Each of these bridges `Waystone.Monads` to a library from somewhere else. They put their types in that library's namespaces, so you reach them from a `using` you already have. All ship from the same repository as `Waystone.Monads`, on the same version number, and all are purely additive.

Packages that add to `Waystone.Monads` itself are under [Add-ons](/reference/packages).

## Pick a package

| If you need to                                | Install                                          | Page                                                                 |
| --------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------- |
| Assert on an `Option` or a `Result` in a test | `Waystone.Monads.Shouldly`                       | [Shouldly](/reference/integrations/shouldly)                         |
| Configure the library from a container        | `Waystone.Monads.Extensions.DependencyInjection` | [Dependency injection](/reference/integrations/dependency-injection) |
| Do that, on `Microsoft.Extensions.Hosting`    | `Waystone.Monads.Extensions.Hosting`             | [Hosting](/reference/integrations/hosting)                           |
| Get a `Result` back from a validator          | `Waystone.Monads.FluentValidation`               | [FluentValidation](/reference/integrations/fluent-validation)        |
| Serialize either type with `System.Text.Json` | `Waystone.Monads.SystemTextJson`                 | [System.Text.Json](/reference/integrations/system-text-json)         |
| Serialize either type with Json.NET           | `Waystone.Monads.NewtonsoftJson`                 | [Newtonsoft.Json](/reference/integrations/newtonsoft-json)           |

## Two of these are a pair

The two JSON packages write the same format on purpose. Pick the serializer your application already uses — a payload one of them writes is a payload the other reads, and a test in the repository asserts that in both directions.

One exception: if you publish with `PublishAot`, use [System.Text.Json](/reference/integrations/system-text-json). Json.NET has no NativeAOT story of its own.

## Two more are a pair

[Hosting](/reference/integrations/hosting) depends on [Dependency injection](/reference/integrations/dependency-injection), so installing Hosting gives you both. On a host, install Hosting. Everywhere else — a console application, a test, a container you built by hand — install the dependency injection package alone.

## None of them changes the library

No package here changes the behaviour of anything in `Waystone.Monads`.

* Two add vocabulary. Remove [Shouldly](/reference/integrations/shouldly) or [FluentValidation](/reference/integrations/fluent-validation) and the code that used it stops compiling, and nothing else moves.
* Two change only *who writes* your configuration, not what the settings mean. You can write the same settings by hand with `MonadOptions.Configure`.
* Two teach a serializer a format it did not know. Your own code does not change at all.


# Shouldly

Shouldly assertions that take an Option or a Result, so a failing test names the state it found.

`Waystone.Monads.Shouldly` — Shouldly assertions for `Option` and `Result`.

## What it adds

Assert on a piece of a monad and the failure message is about that piece, not about the monad. This is the shape most test suites have:

```csharp
result.IsOk.ShouldBeTrue();
```

When it fails, Shouldly tells you that `True` was expected and `False` was found. It cannot tell you what the error was, because by the time the assertion runs the `Result` is gone — `IsOk` handed it a `bool`.

```csharp
result.ShouldBeOk();
```

This one fails with:

```
result
    should be Ok
    but was
Err("failed")
```

The `Unwrap` shape is worse than the `IsOk` shape. `result.Unwrap().ShouldBe(42)` throws from `Unwrap` before the assertion runs at all, so the test fails on a panic and the message is about unwrapping rather than about what you expected.

## When to reach for it

Reach for it in any test project that asserts on an `Option` or a `Result`. That is the only place it belongs — it is a test-only package and it has no use in production code.

Skip it if your suite already reads well. Nothing here changes what passes or fails; it changes what a failure tells you.

## Install it

```
dotnet add package Waystone.Monads.Shouldly
```

The package targets `netstandard2.0`, depends on Shouldly 4.3.0, and brings `Waystone.Monads` with it.

**You need no new `using`.** The assertions are declared in the `Shouldly` namespace, so a test file that already has `using Shouldly;` picks them up as soon as the package is referenced.

{% hint style="info" %}
**Why not the `Waystone.Monads.Shouldly` namespace?** Because a namespace of that name would shadow the global `Shouldly` for every file inside `namespace Waystone.Monads`, and break every plain `using Shouldly;` in that tree. Living in `Shouldly` is also what makes the package invisible to a reader: there is nothing new to import.
{% endhint %}

## The assertions

Ten assertions, each on three receiver shapes — the monad, a `Task` of it, and a `ValueTask` of it.

### Option

| Assertion                     | Asserts                  | Returns   |
| ----------------------------- | ------------------------ | --------- |
| `ShouldBeSome()`              | Holds a value            | The value |
| `ShouldBeNone()`              | Holds no value           | Nothing   |
| `ShouldBeSomeValue(expected)` | Holds exactly `expected` | The value |

### Result

| Assertion                    | Asserts                           | Returns      |
| ---------------------------- | --------------------------------- | ------------ |
| `ShouldBeOk()`               | Succeeded                         | The Ok value |
| `ShouldBeErr()`              | Failed                            | The error    |
| `ShouldBeOkValue(expected)`  | Succeeded with exactly `expected` | The Ok value |
| `ShouldBeErrValue(expected)` | Failed with exactly `expected`    | The error    |

### Every one returns what it found

So you keep asserting with Shouldly's own vocabulary, on a value you now know is there:

```csharp
Order order = result.ShouldBeOk();
order.Total.ShouldBe(42);
```

Or in one line:

```csharp
result.ShouldBeOk().Total.ShouldBe(42);
```

`ShouldBeNone` is the exception. There is nothing to hand back, so it returns `void`.

## Asserting on a task

Every assertion has an `Async` sibling declared on `Task<...>` and `ValueTask<...>` receivers, so you do not have to parenthesise the `await`:

```diff
-(await LoadAsync()).ShouldBeSome();
+await LoadAsync().ShouldBeSomeAsync();
```

{% hint style="danger" %}
**Await it.** An `Async` assertion you forget to await passes without asserting anything, and the test goes green. This is why they carry an `Async` suffix instead of being overloads of the same name — the compiler warns you about an unawaited call, and it can only do that if the call is distinguishable.

`WMS2002` finds these rewrites for you. See [Assertion rules](/reference/analyzers/assertion-rules).
{% endhint %}

## Custom messages

Every assertion takes an optional `customMessage`, which is added to the failure rather than replacing it:

```csharp
result.ShouldBeOk("the seed data should have loaded");
```

```
result
    should be Ok
    but was
Err("failed")

Additional Info:
    the seed data should have loaded
```

The second optional parameter, `actualExpression`, is filled in by the compiler — it is what puts `result` at the top of that message. Do not pass it. If you need to pass a custom message positionally you will hit it, so pass the message by name.

## The analyzers ship in the same package

Installing this package also installs `WMS2001` and `WMS2002`, which find the assertions this page replaces and rewrite them for you. Both are suggestions, both are on by default, and both are batch-fixable — **Fix all occurrences in Project** converts a suite in one pass.

They are documented with the rest of the rules, on [Assertion rules](/reference/analyzers/assertion-rules).

## What it does not do

* It does not ship for any assertion library but Shouldly. There is no xUnit, NUnit or FluentAssertions equivalent.
* It does not change `Option` or `Result`. Remove the package and your production code still compiles.
* It does not assert on the *contents* of a value for you. `ShouldBeSome()` hands the value back so your next assertion can.


# Dependency injection

Lets a container write the library's configuration, so start-up code no longer calls MonadOptions.Configure by hand.

`Waystone.Monads.Extensions.DependencyInjection` — lets a container write the library's configuration.

## What it adds

`AddWaystoneMonads` on `IServiceCollection` and `UseWaystoneMonads` on `IServiceProvider`. Between them they move the configuration call out of a hand-written static and into the container you already have.

## When to reach for it

Reach for it when your application already has a container and you would rather configure the library there than in a hand-written static call.

If your application is built on `Microsoft.Extensions.Hosting`, install [Hosting](/reference/integrations/hosting) instead. It depends on this package, so you get everything here as well, and it makes the second call for you.

## Install it

```
dotnet add package Waystone.Monads.Extensions.DependencyInjection
```

```csharp
builder.Services.AddWaystoneMonads(
    options => options.UseFallbackErrorCode("Contoso"));

var app = builder.Build();

app.Services.UseWaystoneMonads();
```

This package does not change what the library does. `MonadOptions` stays ambient — `Option` and `Result` still read it statically, no monad gains a constructor dependency, and nothing is threaded through your call sites. What changes is only who writes those options.

Everything it ships sits in the `Microsoft.Extensions.DependencyInjection` namespace, which a host application already has in scope. You do not add a `using` for any of it, including for `ReadFromConfiguration`.

## Two calls, and why

`AddWaystoneMonads` registers. `UseWaystoneMonads` installs. They are separate because the configuration needs services the container has not built yet — an `ILoggerFactory` does not exist while the collection is still being populated.

This is Serilog's bootstrap-logger split, with the expensive half left out. Serilog buffers events written before the bind, because a log event emitted early is lost forever. Nothing is lost here. Options read between the two calls are answered from the defaults, which are valid settings rather than a broken state.

## Forgetting the install

**This is the failure mode, and it is silent.** The library keeps working on its defaults, so nothing throws and nothing looks wrong.

So the library instruments it. A read taken after `AddWaystoneMonads` and before `UseWaystoneMonads` writes a `Waystone.Monads.ConfigurationNotApplied` event to the `Waystone.Monads` `DiagnosticListener`:

```csharp
// In a test suite, subscribe and throw to make the omission fatal.
listener.Subscribe(
    observer,
    name => name == MonadDiagnostics.ConfigurationNotAppliedEventName);
```

The signal is held rather than spent while nothing is subscribed, so a subscriber attached at any point before the install still receives it. See [Watching for configuration that was never installed](/guides/observability#watching-for-configuration-that-was-never-installed).

[Waystone.Monads.Extensions.Hosting](/reference/integrations/hosting) removes the second call, and with it the chance of forgetting it.

## Calling it twice

`AddWaystoneMonads` accumulates rather than conflicts. Each `configure` delegate is kept, and they run in registration order at install time, so a later call overrides an earlier one on the settings it touches. Everything else the method does is idempotent.

That makes it safe for a library to call during its own registration without knowing whether the application already has.

There are three overloads. `AddWaystoneMonads()` with no delegate asks for the defaults and nothing else. `AddWaystoneMonads(options => …)` configures from literals. `AddWaystoneMonads((provider, options) => …)` also hands you the built container — see [Wiring a companion package](#wiring-a-companion-package). All three share one registration order.

## The builder it returns

`AddWaystoneMonads` returns a `MonadServicesBuilder`, not the service collection. Its `Services` property is the same collection you passed in, so carry on from there:

```csharp
builder.Services.AddWaystoneMonads()
       .Services.AddSingleton<IClock, SystemClock>();
```

The builder exists so a call that only makes sense once registration has happened can require one. `Waystone.Monads.Extensions.Hosting` hangs [`EnableInstallOnStart()`](/reference/integrations/hosting#on-the-older-ihostbuilder) off it, so asking for the install without first asking for the registration does not compile.

## What the container supplies

At install time three things are applied, in this order, each overwriting the last:

1. The options already in effect, so an earlier `MonadOptions.Configure` call is carried forward rather than discarded.
2. `ErrorCodeFactory`, if the container holds one.
3. Every delegate passed to `AddWaystoneMonads`, in registration order, whichever overload each came from.

A delegate therefore has the last word.

**`ErrorCodeFactory` is the only service resolved for you.** Everything else that comes out of the container is wired by a delegate you pass.

## Wiring a companion package

One `AddWaystoneMonads` overload hands your delegate the built provider, so a setting can come from a registered service rather than from a literal. Logging is the usual case:

```csharp
builder.Services.AddWaystoneMonads((provider, options) =>
    options.UseFallbackErrorCode("Contoso")
           .UseLoggerFactoryFrom(provider));
```

`UseLoggerFactoryFrom` ships from [Waystone.Monads.Extensions.Logging](/reference/packages/logging). **The package you installed is the one you call.** This package does not reference it, so installing this one does not drag that one into your project, and installing that one does not silently change what this one does. The same shape works for any future companion package, and the install path never grows a branch per package.

`UseLoggerFactoryFrom` throws if the container has no `ILoggerFactory` — a worker that never called `AddLogging`, for instance. That is the point of asking for it explicitly: the mistake stops start-up rather than producing silence. Pass a factory to `UseLoggerFactory` directly if you have one in hand.

{% hint style="info" %}
**Earlier `7.0.0` pre-releases resolved `ILoggerFactory` at install themselves**, so logging appeared without being asked for and this package took a hard dependency on the logging one. Both are gone. Take the provider-aware overload above and call `UseLoggerFactoryFrom` to get the old behaviour back.
{% endhint %}

**Resolve singletons only.** The options are one process-wide snapshot, so a scoped service captured in a delegate outlives the scope it came from — see the warning below.

**`ErrorCodeFactory` has no interface.** It is a public, non-sealed class with `virtual` members, so you replace it by subclassing:

```csharp
builder.Services.AddSingleton<ErrorCodeFactory, ContosoErrorCodeFactory>();
```

Register it before `AddWaystoneMonads` or after — it wins either way, because the default is registered with `TryAddSingleton`.

{% hint style="danger" %}
**Do not register a scoped service that the options will hold.** The options are one process-wide snapshot, published once, so anything resolved into them outlives the scope it came from. Register an `ErrorCodeFactory` as scoped and the install either fails scope validation or quietly captures the root instance and hands it to every request for the life of the process.

Per-request configuration is a different problem, and this package does not solve it. Use [`MonadOptions.BeginScope`](/guides/configuration#scoped-configuration).
{% endhint %}

## Reading from configuration

Binding is opt-in, the way Serilog's `ReadFrom.Configuration()` is. `AddWaystoneMonads` never reaches for an `IConfiguration` on its own — you call `ReadFromConfiguration` from the delegate you pass it:

```csharp
builder.Services.AddWaystoneMonads(
    options => options.ReadFromConfiguration(builder.Configuration));
```

```json
{
  "WaystoneMonads": {
    "FallbackErrorCode": "Contoso",
    "FallbackErrorMessage": "Something went wrong.",
    "CatchesCancellation": false
  }
}
```

| Key                    | Sets                       |
| ---------------------- | -------------------------- |
| `FallbackErrorCode`    | `UseFallbackErrorCode`     |
| `FallbackErrorMessage` | `UseFallbackErrorMessage`  |
| `CatchesCancellation`  | `UseCancellationAsFailure` |

Every key is optional, and an absent key leaves its setting alone, so a section with one key changes one setting. Pass a second argument to read a section other than `WaystoneMonads`.

`CatchesCancellation` is honoured in both directions. Setting it to `false` puts the setting back even where code earlier in the chain called `UseCancellationAsFailure()`.

**A key that is present but unusable throws.** That is the point of opting in: an empty `FallbackErrorCode`, or a `CatchesCancellation` that is neither `true` nor `false`, stops start-up where the mistake is written rather than degrading to a default nobody chose.

Binding goes through the builder's `Use…` methods rather than the reflection binder, because the settings have no public setters to bind to.

## Without a Microsoft container

Both services are resolved through `IServiceProvider.GetService` rather than any container-specific API, so `UseWaystoneMonads` works on a provider produced by any conforming container. `AddWaystoneMonads` needs an `IServiceCollection`; a container that populates itself from one (which most do) is enough.

## What it does not do

* It does not change what the library does. `MonadOptions` stays ambient, no monad gains a constructor dependency, and nothing is threaded through your call sites.
* It does not apply the configuration for you. `UseWaystoneMonads` is a second call you have to make — unless you install [Hosting](/reference/integrations/hosting).
* It does not require a Microsoft container. Both services resolve through `IServiceProvider.GetService`.


# Hosting

Installs the container-registered configuration from the host's own start-up sequence, so there is no second call to forget.

`Waystone.Monads.Extensions.Hosting` — applies the container-registered configuration at host start.

## What it adds

`AddWaystoneMonads` on `IHostApplicationBuilder`, and a hosted service that runs `UseWaystoneMonads` as the host starts. That removes the second call.

## When to reach for it

Reach for it if your application is built on `Microsoft.Extensions.Hosting`. It is the shape most applications want, and it is the package to install rather than [Dependency injection](/reference/integrations/dependency-injection) — it depends on that one, so you get both.

Skip it for a console application, a test, or a container you built by hand. There is no host to hook, so install the dependency injection package alone.

## Install it

```
dotnet add package Waystone.Monads.Extensions.Hosting
```

```csharp
builder.AddWaystoneMonads(options => options.UseFallbackErrorCode("Contoso"));

var app = builder.Build();
app.Run();

// No second call.
```

That `AddWaystoneMonads` is an extension on `IHostApplicationBuilder`, which both `WebApplicationBuilder` and the builder from `Host.CreateApplicationBuilder` implement. It has the same three overloads as the one on `IServiceCollection`, including the one that hands your delegate the host's built provider:

```csharp
builder.AddWaystoneMonads((provider, options) =>
    options.UseFallbackErrorCode("Contoso")
           .UseLoggerFactoryFrom(provider));
```

That is how you point the logging package at the host's `ILoggerFactory` — see [Wiring a companion package](/reference/integrations/dependency-injection#wiring-a-companion-package).

This package depends on [Waystone.Monads.Extensions.DependencyInjection](/reference/integrations/dependency-injection), so installing it gives you both. Read that page for what the configuration delegate can do, how the container supplies an `ErrorCodeFactory`, how to wire a companion package from the container, and how to bind settings from `IConfiguration`. Everything here is about *when* those settings are applied.

## The call it removes

The dependency injection package splits registration from installation, because configuration registered on an `IServiceCollection` needs services the container has not built yet. That leaves an application holding a second call it has to remember:

```csharp
var app = builder.Build();

app.Services.UseWaystoneMonads();   // easy to forget
```

Forgetting it is silent — the library keeps working on its defaults. This package removes the call rather than relying on anybody to remember it.

## On the older IHostBuilder

`IHostBuilder` does not implement `IHostApplicationBuilder`, so reach the same pair through `ConfigureServices`:

```csharp
new HostBuilder().ConfigureServices(
    services => services
               .AddWaystoneMonads(options => options.UseFallbackErrorCode("Contoso"))
               .EnableInstallOnStart());
```

`EnableInstallOnStart` hangs off the `MonadServicesBuilder` that `AddWaystoneMonads` returns, so asking for the install without first asking for the registration does not compile. It registers the installer and nothing else, so `AddWaystoneMonads` is still where configuration goes.

Calling it twice installs once — the registration is deduplicated on the implementation type.

## Registration order does not matter

The install runs in `IHostedLifecycleService.StartingAsync`, which the host calls on every hosted service before it calls `StartAsync` on any of them. So a background service that reads `MonadOptions` in its own `StartAsync` sees the installed configuration, whether it was registered before `EnableInstallOnStart` or after.

That is the whole reason this is a lifecycle service rather than a plain `IHostedService`. A plain one would install in `StartAsync`, in registration order, and a service registered ahead of it would read the defaults.

{% hint style="warning" %}
**Work done before the host starts is still too early.** A read taken while the service collection is being populated, or between `Build()` and `Run()`, runs ahead of every hosted service. It is answered from the defaults and reported through the `Waystone.Monads.ConfigurationNotApplied` diagnostic event, exactly as it is without this package. See [Watching for configuration that was never installed](/guides/observability#watching-for-configuration-that-was-never-installed).

Configuration is applied at host start, not at container build.
{% endhint %}

## Without a host

Nothing here applies. Call `UseWaystoneMonads()` on the provider yourself — [Waystone.Monads.Extensions.DependencyInjection](/reference/integrations/dependency-injection) is all a console application, a test, or a container built by hand needs.

## What it does not do

* It does not apply configuration at container build. It applies it at host start, so work done before the host starts still reads the defaults.
* It does not add any setting of its own. Everything the delegate can do belongs to [Dependency injection](/reference/integrations/dependency-injection).
* It does not help outside a host. See `Without a host`, above.


# FluentValidation

Run a FluentValidation validator and get a Result back, with the failures attached to the error.

`Waystone.Monads.FluentValidation` — a validator that returns a `Result`.

## What it adds

Two extension methods, `Validate` and `ValidateAsync`, on any value. Each takes an `IValidator<T>` you already wrote and hands back a `Result<TValue, Error>`.

```csharp
using FluentValidation;
using FluentValidation.Extensions;

record UserInput(int Range, string Search);

class UserInputValidator : AbstractValidator<UserInput>
{
    public UserInputValidator()
    {
        RuleFor(x => x.Range).GreaterThan(0);
        RuleFor(x => x.Search).NotEmpty();
    }
}

UserInput input = new(1, "bob");

Result<UserInput, Error> result = input.Validate(new UserInputValidator());
```

A value that passes comes back as an `Ok` holding the value you gave it. A value that fails comes back as an `Err` holding a [`ValidationError`](#validationerror).

The async form is the same shape, and takes a cancellation token:

```csharp
Result<UserInput, Error> result =
    await input.ValidateAsync(new UserInputValidator(), cancellationToken);
```

## When to reach for it

Reach for it where a validation failure is an ordinary outcome your caller handles, and you are already writing FluentValidation validators. `Validate` gives you a `Result` that chains with everything else in the library.

Skip it if you throw on invalid input by design, or if you do not use FluentValidation. Nothing else here depends on it.

## Install it

```
dotnet add package Waystone.Monads.FluentValidation
```

You also need `FluentValidation` itself, which you almost certainly already have — you have to write the validator.

The package supports `FluentValidation >= 11.1.0 && < 13.0.0`. Bring your own version inside that range.

{% hint style="info" %}
FluentValidation 12 targets `net8.0` only. If you build for .NET Framework or `netstandard2.0`, you cannot resolve it. That is FluentValidation's constraint, not this package's — stay on 11.x there.
{% endhint %}

## Where the types live

The package shadows FluentValidation's own namespaces. Its types sit beside `IValidator` and `ValidationFailure` rather than under a parallel `Waystone` tree, so the validator file you already wrote usually needs no new `using` at all.

| Member                      | Namespace                     |
| --------------------------- | ----------------------------- |
| `ValidationError`           | `FluentValidation`            |
| `Validate`, `ValidateAsync` | `FluentValidation.Extensions` |
| `UseValidationErrorCode`    | `FluentValidation.Configs`    |

The package and assembly are still called `Waystone.Monads.FluentValidation`. Only the namespaces shadow.

Before `7.0.0` these lived under `Waystone.Monads.FluentValidation.*`. Every type and member name is unchanged — only the `using` directives move. See [Every v7 break](/upgrading/v7/breaking-changes#waystone-monads-fluentvalidation).

## It errs with `Error`, so it chains

This is the point of the package. `Validate` errs with `Error`, the same type the rest of `Waystone.Monads` uses, so a validation step drops into a chain without a conversion at the seam.

```csharp
Result<UserInput, Error> result = input.Validate(new UserInputValidator())
                                       .AndThen(Normalise);
```

The async form composes the same way:

```csharp
Result<UserInput, Error> result =
    await input.ValidateAsync(new UserInputValidator(), cancellationToken)
               .AndThenAsync(Save);
```

`ValidateAsync` returns a `ValueTask`, which is what lets a chain ending in it be passed as a step to `AndThenAsync`. See [Async](/guides/async).

## `ValidationError`

`ValidationError` is a `sealed record` that derives from `Error`. It is an error, not something you convert into one.

| Member           | What it gives you                                                 |
| ---------------- | ----------------------------------------------------------------- |
| `Code`           | The configured validation error code. Default `validation.failed` |
| `Message`        | Every failure message joined with `"; "`                          |
| `Failures`       | The `ValidationFailure` list the validator reported, never empty  |
| `ToDictionary()` | Those messages grouped by property name                           |

You reach the detail by pattern matching, at the one place you need it:

```csharp
if (error is ValidationError validationError)
{
    return ValidationProblem(validationError.ToDictionary());
}
```

`ToDictionary()` builds a fresh dictionary each call, so hold the result if you need it twice.

### You cannot build an empty one

The constructor is `internal`, and only the failure branch inside `Validate` and `ValidateAsync` reaches it. So a `ValidationError` always carries at least one failure, and there is no "no failures" case for you to handle.

### Two of them compare on code and message

`Failures` takes no part in equality. `Message` is rendered from it, so comparing both would only add reference equality over a list — two errors describing the same failures would come out unequal.

A `ValidationError` never equals a plain `Error`, even with the same code and message. Records compare their type as well as their values.

## Configure the error code

Configure this through `MonadOptions`, alongside the core settings. There is no separate options class.

```csharp
using FluentValidation.Configs;
using Waystone.Monads.Configs;

MonadOptions.Configure(options => options.UseValidationErrorCode("input.invalid"));
```

The default is `validation.failed`. The code cannot be null or whitespace — passing either throws an `ArgumentException`.

`UseValidationErrorCode` returns `MonadValidationOptionsBuilder`, not `MonadOptionsBuilder`, so set the core options first if you need both:

```csharp
MonadOptions.Configure(options =>
{
    options.UseFallbackErrorCode("Contoso");
    options.UseValidationErrorCode("input.invalid");
});
```

### Scopes work, and the code is read once

Validation options honour `MonadOptions.BeginScope`. One scope covers this package and the core together.

```csharp
using (MonadOptions.BeginScope(options => options.UseValidationErrorCode("debug.validation")))
{
    // errors created in here carry "debug.validation"
}
```

The code is read **when the validation runs**, not when you later read the error. An error created inside a scope keeps that scope's code after the scope closes. See [Configuration](/guides/configuration) for the full scope semantics.

## Exceptions still throw

Only validation failures become an `Err`.

* An exception thrown by your validator propagates to the caller.
* `Validate` throws if the validator declares asynchronous rules. Call `ValidateAsync` for those.
* A cancelled token surfaces as an `OperationCanceledException`, not as an `Err`.

## It changed a lot in 7.0.0

If you are coming from `6.x`, this package no longer has a `ValidationErr` type and the extension methods return something different. See [Every v7 break](/upgrading/v7/breaking-changes#waystone-monads-fluentvalidation).

## What it does not do

* It does not turn exceptions into an `Err`. Only validation failures become one.
* It does not run asynchronous rules from `Validate`. That call throws; use `ValidateAsync`.
* It does not register your validators. That is FluentValidation's own container wiring, unchanged.


# System.Text.Json

Serialize Option and Result with System.Text.Json, in a format a consumer already agreed to.

`Waystone.Monads.SystemTextJson` — converters for `Option` and `Result`.

## What it adds

Converters for `Option<T>` and `Result<TOk, TErr>`, and one call that registers them:

```csharp
using System.Text.Json;

JsonSerializerOptions options = new();
options.AddMonadConverters();

string json = JsonSerializer.Serialize(model, options);
```

Call it while you are still building the options. `System.Text.Json` freezes a `JsonSerializerOptions` the first time it serializes with it, and adding a converter after that throws.

Without this package, an `Option<T>` on a DTO serializes as its own internals, `{"IsSome":false,"IsNone":true}`, and does not read back.

## When to reach for it

Reach for it when an `Option` or a `Result` is a member of a type you serialize, and your application already uses `System.Text.Json`. Without it, both types serialize as whatever their properties happen to expose, which is not a format anything can read back.

If your application uses Json.NET instead, install [Newtonsoft.Json](/reference/integrations/newtonsoft-json) — the two write the same bytes, so the choice is decided by the serializer you already have, not by preference.

## Install it

```
dotnet add package Waystone.Monads.SystemTextJson
```

The package supports `System.Text.Json >= 8.0.5 && < 11.0.0`. Bring your own version inside that range. Every version in the range ships a `netstandard2.0` and a `net462` asset, so you can use it on .NET Framework too.

## Where the types live

The package shadows `System.Text.Json`'s own namespaces, so its types sit where you already look for them.

| Member                                                         | Namespace                        |
| -------------------------------------------------------------- | -------------------------------- |
| `AddMonadConverters`                                           | `System.Text.Json`               |
| `OptionJsonConverter<T>`, `OptionJsonConverterFactory`         | `System.Text.Json.Serialization` |
| `ResultJsonConverter<TOk, TErr>`, `ResultJsonConverterFactory` | `System.Text.Json.Serialization` |

The converters follow `JsonConverter<T>` down into `System.Text.Json.Serialization`. The extension method sits beside the `JsonSerializerOptions` it extends.

The package and assembly are still called `Waystone.Monads.SystemTextJson`. Only the namespaces shadow.

## `Option<T>` is the value, or null

This is the format Rust's serde uses. A `Some` contributes what the value alone would have written. A `None` writes `null`.

```csharp
public sealed class Person
{
    public Option<string> Nickname { get; set; } = Option.None<string>();
}
```

```jsonc
{ "Nickname": "Ally" }   // Option.Some("Ally")
{ "Nickname": null }     // Option.None<string>()
```

So swapping a `string` property for an `Option<string>` does not change the JSON a consumer receives. That is the point of the format.

### A None writes the property, it does not remove it

A converter cannot delete its own property from the object around it. A `None` writes `"Nickname": null`.

**`JsonIgnoreCondition.WhenWritingNull` does not change that.** It tests the CLR value for null, and `Option.None<T>()` is an object like any other. It is never null, so the property is always written. `[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]` fails the same way.

A type-info modifier does work. It decides per property whether to write it:

```csharp
options.TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
    Modifiers = { SkipNoneProperties },
};

static void SkipNoneProperties(JsonTypeInfo typeInfo)
{
    foreach (JsonPropertyInfo property in typeInfo.Properties)
    {
        property.ShouldSerialize = static (_, value) =>
            value is null
         || value.GetType() is not { IsGenericType: true } type
         || type.GetGenericTypeDefinition() != typeof(None<>);
    }
}
```

The package does not ship that modifier. Whether to omit a property is a decision about your wire contract, not about `Option<T>`, and most people who want it want it for some models and not others.

### An absent property does not read back as a None

If the property is missing from the payload, `System.Text.Json` never calls the converter for that member. The member keeps its CLR default, which for `Option<T>` is `null` — not `None<T>()`.

Initialise the member, as `Person.Nickname` does above. Otherwise the model holds a null where it promised an option.

### A nested option collapses

`Option<Option<T>>` does not survive a round trip. `Some(None)` and `None` both write `null`, and both read back as `None`:

```csharp
Option<Option<int>> before = Option.Some(Option.None<int>());
string json = JsonSerializer.Serialize(before, options);   // "null"
Option<Option<int>> after = JsonSerializer.Deserialize<Option<Option<int>>>(json, options)!;
// after is None<Option<int>>(), not Some(None<int>())
```

The converter accepts this rather than throwing. Throwing on a shape the type system allows is worse than losing a distinction you should not be relying on. The [`WM2009` analyzer rule](/reference/analyzers) already reports the declaration, which is the better place to catch it.

## `Result<TOk, TErr>` names its case

A result has no idiomatic JSON shape to borrow. Both cases carry ordinary values of different types, so the case has to be named on the wire.

```jsonc
{ "$type": "ok",  "value": 42 }
{ "$type": "err", "value": { "Code": "validation.failed", "Message": "..." } }
```

Property order does not matter. `{"value":42,"$type":"ok"}` reads the same.

### Why the payload is nested

`$type` is also `System.Text.Json`'s own polymorphism discriminator. If your `TOk` or `TErr` is a polymorphic base carrying `[JsonDerivedType]`, it writes a `$type` of its own.

Nesting puts that one *inside* `value`, a level below the result's:

```jsonc
{ "$type": "ok", "value": { "$type": "cat", "Name": "Tom" } }
```

Flattening the payload beside the discriminator would have made the two siblings. The collision would then surface only for people whose payload happens to be polymorphic. Nesting rules it out.

### The four names are fixed

`$type`, `value`, `ok` and `err` never change. `JsonSerializerOptions.PropertyNamingPolicy` does not rename them, so a camel-casing service and a snake-casing one still exchange the same payload.

### Reading rejects what a result cannot hold

Deserializing throws `JsonException` when:

* the payload is not an object
* `$type` is missing, or is not a string
* `$type` names neither case
* `value` is missing
* `value` reads as null

A result has no null case. Accepting one would push the failure somewhere later and harder to trace.

A null `value` is not rejected on sight, though. It is read as the case's own type first. So a payload whose own converter reads `null` still round-trips — most usefully `Result<Option<T>, TErr>`, where `Ok(None)` writes `"value": null` and reads back as `Ok(None)`.

## Trimming and NativeAOT

Both factories close their converter reflectively, once per monad type, and the serializer caches the result. Under NativeAOT that **throws** when a type argument is a value type:

```
NotSupportedException: 'OptionJsonConverter`1[System.Int32]' is missing native
code or metadata.
```

A generic instantiation over a value type needs its own compiled code. The compiler cannot see through `MakeGenericType` to know it will be asked for one. Reference types all share a single compiled converter, so they are unaffected.

This is measured under `PublishAot` on .NET 10, not inferred:

| Registered through              | Type argument  | Under NativeAOT |
| ------------------------------- | -------------- | --------------- |
| `AddMonadConverters()`          | reference type | works           |
| `AddMonadConverters()`          | value type     | throws          |
| `options.Converters.Add(new …)` | either         | works           |

For `Result<TOk, TErr>` it is enough for one of the two arguments to be a value type.

Register value-type monads explicitly instead. The concrete converters are public, with public parameterless constructors, precisely so this path exists. It uses no reflection at all:

```csharp
options.Converters.Add(new OptionJsonConverter<int>());
options.Converters.Add(new ResultJsonConverter<int, string>());
```

A model made of `Option<string>` and `Result<Uri, Error>` needs nothing extra.

`Option<T>` and `Result<TOk, TErr>` members do not get the source-generation fast path from a `JsonSerializerContext`. A factory-produced converter works from one, but you get correctness, not the speed.

## It matches the Newtonsoft.Json package byte for byte

[`Waystone.Monads.NewtonsoftJson`](/reference/integrations/newtonsoft-json) writes the same JSON. A test in the repository serializes with one package, deserializes with the other, both directions, and asserts the two write identical bytes.

So you can switch serializers, or run both in one system, without a migration on the wire.

## What it does not do

* It does not remove a property for a `None`. The property is written with a `null` value, and an absent property is a read error rather than a `None`.
* It does not give `Option<T>` or `Result<TOk, TErr>` the source-generation fast path from a `JsonSerializerContext`. You get correctness, not speed.
* It does not change either type. Remove the package and only serialization breaks.


# Newtonsoft.Json

Serialize Option and Result with Newtonsoft.Json, in the same format the System.Text.Json package writes.

`Waystone.Monads.NewtonsoftJson` — converters for `Option` and `Result`.

## What it adds

Converters for `Option<T>` and `Result<TOk, TErr>`, and one call that registers them:

```csharp
using Newtonsoft.Json;

JsonSerializerSettings settings = new JsonSerializerSettings().AddMonadConverters();

string json = JsonConvert.SerializeObject(model, settings);
```

`AddMonadConverters` returns the settings you gave it, so you can chain from it. It appends both converters to `Converters`. Json.NET takes the first converter that accepts a type, so a converter you registered for an option or a result beforehand keeps priority.

Without this package, an `Option<T>` on a DTO serializes as its own internals, `{"IsSome":false,"IsNone":true}`, and does not read back.

## When to reach for it

Reach for it when an `Option` or a `Result` is a member of a type you serialize, and your application already uses Json.NET. Without it, both types serialize as whatever their properties happen to expose, which is not a format anything can read back.

If your application uses `System.Text.Json` instead, install [System.Text.Json](/reference/integrations/system-text-json) — the two write the same bytes, so the choice is decided by the serializer you already have, not by preference.

## Install it

```
dotnet add package Waystone.Monads.NewtonsoftJson
```

The package supports `Newtonsoft.Json >= 13.0.1 && < 14.0.0`. Bring your own version inside that range. Every version in the range ships a `netstandard2.0`, a `net45` and a `net20` asset, so you can use it on .NET Framework too.

## Where the types live

The package shadows `Newtonsoft.Json`'s own namespace, so its types sit where you already look for them.

| Member                | Namespace         |
| --------------------- | ----------------- |
| `AddMonadConverters`  | `Newtonsoft.Json` |
| `OptionJsonConverter` | `Newtonsoft.Json` |
| `ResultJsonConverter` | `Newtonsoft.Json` |

`JsonConverter` and `JsonSerializerSettings` both live in the root `Newtonsoft.Json` namespace, so everything here lands there too.

The package and assembly are still called `Waystone.Monads.NewtonsoftJson`. Only the namespace shadows.

## `Option<T>` is the value, or null

This is the format Rust's serde uses. A `Some` contributes what the value alone would have written. A `None` writes `null`.

```csharp
public sealed class Person
{
    public Option<string> Nickname { get; set; } = Option.None<string>();
}
```

```jsonc
{ "Nickname": "Ally" }   // Option.Some("Ally")
{ "Nickname": null }     // Option.None<string>()
```

So swapping a `string` property for an `Option<string>` does not change the JSON a consumer receives. That is the point of the format.

### A None writes the property, it does not remove it

A converter cannot delete its own property from the object around it. A `None` writes `"Nickname": null`.

**`NullValueHandling.Ignore` does not change that.** It tests the CLR value for null, and `Option.None<T>()` is an object like any other. It is never null, so the property is always written. `[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]` fails the same way.

A contract resolver does work. It decides per property whether to write it:

```csharp
settings.ContractResolver = new SkipNoneContractResolver();

public sealed class SkipNoneContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(
        MemberInfo member,
        MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);

        property.ShouldSerialize = instance =>
            property.ValueProvider?.GetValue(instance)?.GetType() is not
                { IsGenericType: true } type
         || type.GetGenericTypeDefinition() != typeof(None<>);

        return property;
    }
}
```

A `ShouldSerialize{PropertyName}` method on the model does the same job for one property.

The package does not ship that resolver. Whether to omit a property is a decision about your wire contract, not about `Option<T>`, and most people who want it want it for some models and not others.

### An absent property does not read back as a None

If the property is missing from the payload, Json.NET never calls the converter for that member. The member keeps whatever the model gave it, which without an initialiser is `null` — not `None<T>()`.

Initialise the member, as `Person.Nickname` does above. Otherwise the model holds a null where it promised an option.

### A nested option collapses

`Option<Option<T>>` does not survive a round trip. `Some(None)` and `None` both write `null`, and both read back as `None`:

```csharp
Option<Option<int>> before = Option.Some(Option.None<int>());
string json = JsonConvert.SerializeObject(before, settings);   // "null"
Option<Option<int>> after = JsonConvert.DeserializeObject<Option<Option<int>>>(json, settings)!;
// after is None<Option<int>>(), not Some(None<int>())
```

The converter accepts this rather than throwing. Throwing on a shape the type system allows is worse than losing a distinction you should not be relying on. The [`WM2009` analyzer rule](/reference/analyzers) already reports the declaration, which is the better place to catch it.

## `Result<TOk, TErr>` names its case

A result has no idiomatic JSON shape to borrow. Both cases carry ordinary values of different types, so the case has to be named on the wire.

```jsonc
{ "$type": "ok",  "value": 42 }
{ "$type": "err", "value": { "Code": "validation.failed", "Message": "..." } }
```

Property order does not matter. `{"value":42,"$type":"ok"}` reads the same.

### Why the payload is nested

`$type` is a busy name. Json.NET writes one of its own when `TypeNameHandling` is on, and it is also `System.Text.Json`'s polymorphism discriminator.

Nesting puts any such `$type` *inside* `value`, a level below the result's:

```jsonc
{ "$type": "ok", "value": { "$type": "MyApp.Cat, MyApp", "Name": "Tom" } }
```

Flattening the payload beside the discriminator would have made the two siblings. The collision would then surface only for people who happen to turn `TypeNameHandling` on. Nesting rules it out.

### The four names are fixed

`$type`, `value`, `ok` and `err` never change. A `CamelCasePropertyNamesContractResolver` does not rename them, so a camel-casing service and a snake-casing one still exchange the same payload.

### Reading rejects what a result cannot hold

Deserializing throws `JsonSerializationException` when:

* the payload is not an object
* `$type` is missing, or is not a string
* `$type` names neither case
* `value` is missing
* `value` deserializes to null

A result has no null case. Accepting one would push the failure somewhere later and harder to trace.

A null `value` is not rejected on sight, though. It is deserialized as the case's own type first. So a payload whose own converter reads `null` still round-trips — most usefully `Result<Option<T>, TErr>`, where `Ok(None)` writes `"value": null` and reads back as `Ok(None)`.

## Reflection and NativeAOT

Json.NET picks a converter from the **runtime** type of the value it is writing. For a monad that is always `Some<T>`, `None<T>`, `Ok<TOk, TErr>` or `Err<TOk, TErr>` — never the option or result itself. Both converters therefore accept all three shapes.

Each converter closes an internal adapter over the type arguments once per closed type and caches it. Only the first monad of a given type costs any reflection. Nothing reflects per call.

{% hint style="warning" %}
**Publishing with `PublishAot`? Use** [**`Waystone.Monads.SystemTextJson`**](/reference/integrations/system-text-json) **instead.**

That first construction is the one that fails under NativeAOT for a value-type argument. There is no escape hatch here, because Json.NET cannot register a converter for a single closed generic type. Json.NET has no first-class NativeAOT support of its own either.
{% endhint %}

## It matches the System.Text.Json package byte for byte

[`Waystone.Monads.SystemTextJson`](/reference/integrations/system-text-json) writes the same JSON. A test in the repository serializes with one package, deserializes with the other, both directions, and asserts the two write identical bytes.

So you can switch serializers, or run both in one system, without a migration on the wire.

## What it does not do

* It does not remove a property for a `None`. The property is written with a `null` value, and an absent property is a read error rather than a `None`.
* It does not support NativeAOT. Json.NET cannot register a converter for a single closed generic type, so there is no escape hatch — use [System.Text.Json](/reference/integrations/system-text-json) if you publish with `PublishAot`.
* It does not change either type. Remove the package and only serialization breaks.


# Source generation

Mark an enum with \[ErrorCodeCatalog] and get its error codes as compile-time constants, generated at build time.

`Waystone.Monads` ships a source generator inside the package. It ships from 6.2.0. You add no reference and configure nothing. It stays silent until you mark an enum with `[ErrorCodeCatalog]`.

## Which page do you want

| You want to                                        | Read                                                                      |
| -------------------------------------------------- | ------------------------------------------------------------------------- |
| Know why this exists and turn it on                | This page                                                                 |
| Know exactly what it emits                         | [Error code catalogs](/reference/source-generation/error-code-catalogs)   |
| Change the shape of the generated strings          | [Code format language](/reference/source-generation/code-format)          |
| Review your codes, or fail a build when they drift | [Reviewing generated codes](/reference/source-generation/reviewing-codes) |
| Understand a `WMG` build error                     | [Generator diagnostics](/reference/source-generation/diagnostics)         |

Hand-written `ErrorCode` values are a different thing, and they are on [Errors](/guides/errors). This group covers the generated ones only.

## Why it exists

Up to 6.x, `ErrorCode.FromEnum(InputErrors.Missing)` worked out the string `"InputErrors.Missing"` at run time, by reflecting over the enum. That meant the string your callers saw was not written anywhere you could point at. You could not use it in a `switch`, you could not put it in an attribute, and you could not find every place that read it.

Mark the enum with `[ErrorCodeCatalog]` and `Waystone.Monads` generates those strings as constants when you build.

{% hint style="warning" %}
`ErrorCode.FromEnum` was obsolete from 6.2.0 and 7.0.0 removes it, so this group describes the only supported way to get an error code from an enum. See [Deprecations](/upgrading/deprecations) for the migration.
{% endhint %}

## Marking an enum

```csharp
using Waystone.Monads.Results.Errors;

namespace Ordering;

[ErrorCodeCatalog]
public enum OrderErrorCode
{
    NotFound,
    AlreadyShipped,
}
```

That gives you a new class next to the enum, `OrderErrorCodeCatalog`, in the same namespace and with the same accessibility as the enum.

The name is the enum's own name with `Catalog` on the end, and nothing is taken off it. `OrderFailure` gives you `OrderFailureCatalog`; `OrderErrorCode` gives you `OrderErrorCodeCatalog`. If that reads twice, rename the enum — two enums whose names differ always get two catalogs whose names differ.

Next: [what the catalog contains](/reference/source-generation/error-code-catalogs).


# Error code catalogs

What \[ErrorCodeCatalog] emits: three nested classes, three extensions, and the fallback for a value that is not a declared member.

## What you get

Three nested classes, one per shape:

```csharp
// The code as a compile-time constant.
OrderErrorCodeCatalog.Names.NotFound   // "OrderErrorCode.NotFound"

// The code as an ErrorCode.
OrderErrorCodeCatalog.Codes.NotFound   // ErrorCode { Value = "OrderErrorCode.NotFound" }

// An Error carrying that code.
OrderErrorCodeCatalog.Errors.NotFound("no order with that id")
```

`Names` gives you a `const string`, so you can use it anywhere C# wants a constant — a `case` label, an attribute argument, a switch on a code that arrived over the wire.

And three extension methods, for when you are holding a value rather than naming a member:

```csharp
OrderErrorCode errorCode = Classify(order);

string asName = errorCode.ToErrorCodeName();
ErrorCode asErrorCode = errorCode.ToErrorCode();
Error asError = errorCode.ToError("no order with that id");
```

The nesting is what keeps your member names usable as-is. A member called `NotFoundCode` becomes `Names.NotFoundCode`, and nothing collides.

`Errors.NotFound(message)` and `ToError(message)` build the `Error` for you, so they inherit how `Error` treats a message: it is trimmed, and a blank one is replaced by your [configured fallback](/guides/configuration#error-code-and-message-fallbacks) rather than rejected. Neither throws on a blank message, so pass a real one.

## A value that is not a declared member

Casting an arbitrary integer to an enum is legal C#, so `(OrderErrorCode)99` is a value you can be handed. The three extensions apply the same scheme to it:

```csharp
((OrderErrorCode)99).ToErrorCodeName(); // "OrderErrorCode.99"
```

## Reusing a code across two enums

Two attributed enums with the same name in different namespaces generate the same code for every member name they share. `Ordering.OrderErrorCode.NotFound` and `Shipping.OrderErrorCode.NotFound` both generate `"OrderErrorCode.NotFound"`, and nothing reading the code can tell the two errors apart. So can two differently named enums that share a format: `"order.{member:kebab}"` on both `OrderErrorCode` and `ShipmentError` makes `NotFound` collide.

`WM2018` reports that. It is a suggestion, not a warning — see [Idioms](/reference/analyzers/idioms#wm2018).


# Code format language

Shape the generated strings with {enum} and {member}, an optional casing, and one default for the whole assembly.

## Choosing the code format

`OrderErrorCode.NotFound` is the default, not the only option. Set `Format` on the attribute and the generated codes follow it:

```csharp
[ErrorCodeCatalog(Format = "order.{member:kebab}")]
public enum OrderErrorCode
{
    NotFound,
    AlreadyShipped,
}
```

```csharp
OrderErrorCodeCatalog.Names.NotFound       // "order.not-found"
OrderErrorCodeCatalog.Names.AlreadyShipped // "order.already-shipped"
```

Everything the format does happens at build time, so what you get out is still a `const string`.

## The format language

Two placeholders, and everything else is literal text:

| Placeholder | Substitutes                       |
| ----------- | --------------------------------- |
| `{enum}`    | The enum's name, `OrderErrorCode` |
| `{member}`  | The member's name, `NotFound`     |

Either one takes an optional casing after a colon — `{member:kebab}`:

| Casing   | `NotFound`  | `HTTPNotFound`   | `Error404`  |
| -------- | ----------- | ---------------- | ----------- |
| *(none)* | `NotFound`  | `HTTPNotFound`   | `Error404`  |
| `kebab`  | `not-found` | `http-not-found` | `error-404` |
| `snake`  | `not_found` | `http_not_found` | `error_404` |
| `lower`  | `notfound`  | `httpnotfound`   | `error404`  |
| `upper`  | `NOTFOUND`  | `HTTPNOTFOUND`   | `ERROR404`  |

`kebab` and `snake` split the identifier into words; `lower` and `upper` only change case. A word boundary falls where a lowercase or a digit meets an uppercase, before the last uppercase of a run that runs into a lowercase, and between letters and digits. An underscore already in the name counts as a boundary rather than doubling up, so `Already_Shipped` gives `already-shipped`.

Write a literal brace by doubling it: `{{` and `}}`.

The default is `{enum}.{member}`, which is what an enum that sets nothing gets.

## One format for the whole assembly

Put the attribute on the assembly and every attributed enum in the project uses it:

```csharp
[assembly: ErrorCodeFormat("{enum:kebab}/{member:kebab}")]
```

An enum's own `Format` wins over the assembly's, so the assembly attribute sets the house style and an individual enum departs from it where it needs to.

## The format is your contract, not your factory

The generated strings come from the format and nothing else. In particular:

{% hint style="warning" %}
**A generated member never consults your `ErrorCodeFactory`.** The generator runs at build time and cannot run a factory you install at run time. A factory installed through `MonadOptions.UseErrorCodeFactory` still shapes the codes it derives from an exception; it has no say over an attributed enum.

That holds for every generated member, including the fallback for an undeclared value. If you were using a custom factory in 6.x to shape the codes an enum produces, say the same thing with `Format` instead — you get the same strings as constants, and one answer rather than two.
{% endhint %}

Shaping enum codes is the part of `ErrorCodeFactory` the format replaces, and 7.0.0 removes that part. `ErrorCodeFactory.FromEnum` and `ErrorCode.FromEnum` reported `CS0618` from 6.2.0 and are gone now — the override produces `CS0115`. See [Deprecations](/upgrading/deprecations). `FromException` is not affected.


# Reviewing generated codes

Commit your error codes as a list, and make a divergence fail the build rather than a rename change a wire contract silently.

## Reviewing your codes as a list

An error code is a wire contract, and the thing that makes one hard to hold onto is that a rename changes it silently. You can make the whole set reviewable by committing it.

Add an `ErrorCodes.txt` to the project and list it as an `AdditionalFiles` item:

```xml
<ItemGroup>
    <AdditionalFiles Include="ErrorCodes.txt"/>
</ItemGroup>
```

That is the whole opt-in — the same shape as `PublicAPI.Shipped.txt` for the public API analyzers. A project without the file never sees either rule.

The file is one code per line. Blank lines are ignored and a line starting with `#` is a comment:

```
# Every error code this project publishes. Reviewed on change.
order.already-shipped
order.not-found
```

Two rules then keep it honest:

| ID       | What it reports                                        |
| -------- | ------------------------------------------------------ |
| `WM2019` | An enum member generates a code the file does not list |
| `WM2020` | The file lists a code no catalog generates             |

`WM2019` has a code fix, **Update ErrorCodes.txt**, which rewrites the whole file from the compilation: it adds every missing code and removes every stale entry in one pass, sorted, keeping your leading comment block. So a rename shows up as one removed line and one added line, and you read it in the diff before you commit it.

{% hint style="info" %}
`WM2020` has no code fix of its own. It is reported against `ErrorCodes.txt` rather than against any of your source, and Roslyn does not offer fixes for a diagnostic reported at the end of a compilation. In practice this does not come up: the `WM2019` fix removes stale entries too. A project whose *only* divergence is a stale entry deletes the line the message names.
{% endhint %}

## Making a divergence fail the build

Both rules ship as suggestions, so by default they show in the IDE and not in CI. If you have committed the file you probably want the opposite. `WM2019` responds to an ordinary `.editorconfig`:

```ini
[*.cs]
dotnet_diagnostic.WM2019.severity = warning
```

{% hint style="warning" %}
**`WM2020` does not.** It is reported against `ErrorCodes.txt`, which has no syntax tree, and Roslyn resolves `dotnet_diagnostic` severities per syntax tree — so a path-matched section is never consulted for it, including `[*]`. Raising it takes a global analyzer config:

```ini
is_global = true
dotnet_diagnostic.WM2019.severity = warning
dotnet_diagnostic.WM2020.severity = warning
```

Put that in a `.globalconfig` next to the project. A global config covers both rules, so it is the simpler thing to write even though only one of them needs it.
{% endhint %}

## Renaming is a breaking change

The code is built from two names: the enum's and the member's. Rename either (or edit the format) and every consumer reading the code sees a different string, with nothing in the compiler to tell you.

```csharp
[ErrorCodeCatalog]
public enum OrderErrorCode   // rename this -> every code changes
{
    NotFound,                // rename this -> "OrderErrorCode.NotFound" changes
}
```

This is not new — `ErrorCode.FromEnum` worked the same way, and `ErrorCode`'s own guidance is that a code should not change between occurrences of the same error. Treat an attributed enum as a published contract, the way you would treat a URL.


# Generator diagnostics

The WMGxxxx and WMSCxxxx codes. Each one marks something a generator cannot emit, or emitted differently from how you meant it.

These come from a source generator rather than from the analyzer. Two generators ship, and each has its own prefix.

| Prefix | Generator           | Page section                    |
| ------ | ------------------- | ------------------------------- |
| `WMG`  | Error code catalogs | [WMG](#wmg-error-code-catalogs) |
| `WMSC` | Schemas             | [WMSC](#wmsc-schemas)           |

{% hint style="info" %}
`WMSC` is not `WMS`. The `WMS` codes are analyzer rules from `Waystone.Monads.Shouldly`, listed on [Assertion rules](/reference/analyzers/assertion-rules). The prefixes are four characters and three characters, and suppressing the wrong one silences a rule you wanted.
{% endhint %}

## WMG: error code catalogs

Six diagnostics, all errors. Each marks a case where the generator would otherwise produce code that does not compile, or a code you did not mean to publish.

| ID                    | What it reports                                          |
| --------------------- | -------------------------------------------------------- |
| [`WMG0001`](#wmg0001) | `[ErrorCodeCatalog]` on a `[Flags]` enum                 |
| [`WMG0002`](#wmg0002) | Two members of the enum sharing a value                  |
| [`WMG0003`](#wmg0003) | A member named `Names`, `Codes` or `Errors`              |
| [`WMG0004`](#wmg0004) | `ErrorCode` or `Error` not resolvable in the compilation |
| [`WMG0005`](#wmg0005) | A `Format` the generator cannot parse                    |
| [`WMG0006`](#wmg0006) | A `Format` that leaves out `{member}`                    |

### WMG0001

**A flags enum has no single code per value.** `OrderErrorCode.NotFound | OrderErrorCode.AlreadyShipped` is one value whose `ToString()` is `"NotFound, AlreadyShipped"`, and there is no sensible code for it. Use a plain enum for errors, and model a combination as its own member if you need one.

### WMG0002

**Two members with the same value are one value.** Given `NotFound = 1` and `Missing = 1`, the two are indistinguishable at run time, so neither has a code of its own. Give them different values, or delete the alias.

### WMG0003

**A member named after a generated class would produce invalid C#.** The generator emits nested classes called `Names`, `Codes` and `Errors`. A member with one of those names would produce a member sharing its enclosing type's name, which is `CS0542`. Rename the member.

Every other name is fine, including `ToError`, `ToErrorCode` and `ToErrorCodeName` — the extensions live on the outer class and your members live in the nested ones, so they never meet.

### WMG0004

**The generator cannot find `ErrorCode` or `Error`.** You will only see this if the generator is running on a project that does not reference `Waystone.Monads`, which normally means a hand-wired analyzer reference. Reference the package.

### WMG0005

**The format does not parse.** An unclosed placeholder, a stray `}`, a name that is not `{enum}` or `{member}`, or a casing that is not `kebab`, `snake`, `lower` or `upper`. The message names the position and what it expected. This reports on the attribute that set the format, whether that is the enum's or the assembly's.

### WMG0006

**A format without `{member}` gives every member the same code.** `"{enum:kebab}"` generates one string for the whole enum, so the codes stop telling the members apart. Include `{member}`.

## WMSC: schemas

Nine diagnostics from the schemas package. Five are errors — the schema cannot be generated, or the code cannot work. Three warn about code that compiles and runs and is probably not what you meant. One suggests a better spelling for code with nothing wrong with it, and only an IDE ever shows it.

See [Schemas](/reference/packages/schemas) for the package itself.

| ID                      | Severity   | What it reports                                          |
| ----------------------- | ---------- | -------------------------------------------------------- |
| [`WMSC0001`](#wmsc0001) | Error      | A schema, or a type containing it, is not `partial`      |
| [`WMSC0002`](#wmsc0002) | Error      | No accessible parameterless constructor                  |
| [`WMSC0003`](#wmsc0003) | Error      | A member named `Instance`, `Schema` or `FieldSet`        |
| [`WMSC0004`](#wmsc0004) | Error      | The `Into` lambda's arity does not match the field count |
| [`WMSC0005`](#wmsc0005) | Warning    | `Refine` is handed a field that produces a value         |
| [`WMSC0006`](#wmsc0006) | Error      | An asynchronous rule reached from a field set            |
| [`WMSC0007`](#wmsc0007) | Warning    | A field-set call the generator did not recognise         |
| [`WMSC0008`](#wmsc0008) | Warning    | A field path taken from an expression, not a name        |
| [`WMSC0009`](#wmsc0009) | Suggestion | `Schema.For<T>()` where a named schema exists            |

### WMSC0001

**A generator adds members through a second declaration of the same class**, which the compiler accepts only where every type in the nesting chain is `partial`.

`class QuestSchema : SchemaConfig<QuestDto, Quest>` gets nothing generated. Add `partial`.

The diagnostic reports against the type that is missing the modifier, which is not always the schema. A schema nested inside `public class Endpoints` needs `Endpoints` to be `partial` too, and that is the one you have to edit.

Nothing else reports this. A schema that is not `partial` is perfectly legal C#; it just silently receives no `Instance`.

### WMSC0002

**The generated `Instance` is a static property initialised with `new`.**

`SchemaConfig` supplies a protected parameterless constructor, so a derived schema inherits one — until it declares a constructor of its own, at which point the implicit one disappears with no diagnostic of its own.

Given `public QuestSchema(IQuestBoard board)` and nothing else, `Instance` cannot be constructed. Either add a parameterless constructor, or take what the schema needs from the input it parses rather than from a constructor. A schema that genuinely needs a dependency is usually a schema that wants [`CheckAsync`](/reference/packages/schemas/asynchrony) composed around it instead.

### WMSC0003

**The generator reopens your class and writes three names into it** — `Instance`, a nested `Schema`, and a `FieldSet` struct per field count. A hand-written member of any of those names is a duplicate definition.

Rename yours, or delete it and use the generated one.

Type parameters do not separate them. A nested type collides with an existing member of the same name whatever its arity, so `class FieldSet<T>` collides with the generated `FieldSet<T1, T2>`.

The compiler reports this collision too, but against the generated file — which is not a file anyone can edit.

`Schema` and `FieldSet` are only checked where the generator is actually writing those members. A schema that never calls `Schema.Fields` may keep a member of either name.

### WMSC0004

**The generated `Into` takes one parameter per field**, so a lambda of any other arity cannot bind to it.

Three fields in `Schema.Fields` and `.Into((a, b) => …)` is a mismatch. Give the lambda one parameter per field, in the order the fields are listed.

The compiler rejects this too, but as a delegate conversion failure against a generated file you cannot open, naming neither the field count nor what decided it.

The diagnostic reports at the `Into` call, not at the field list. The field list is what you meant; the lambda is what disagrees with it.

### WMSC0005

**`Refine` takes the non-generic `Field` base, which drops the value side.** It accepts any field and keeps only its violations.

That is the right shape for `Schema.Forbidden` and `Schema.Extend`, which yield `Checked` and have no value to contribute. It is a silent mistake for a field that parses something somebody expected to find on the result.

`Refine(Schema.Required(subject.Email, Guild.Email))` checks the email and throws it away. List it in `Schema.Fields` instead, so it reaches the `Into` lambda.

**Unless you meant it.** Gating on a value without keeping it is legitimate — a confirmation field that has to be a well-formed email but is never stored is the obvious case. That is why this warns rather than fails.

Say so with [`AsChecked`](/reference/packages/schemas/field-sets#checked), which hands `Refine` the same field with its value dropped and its path kept. The warning stops, because the field now yields `Checked` like the other two. Suppressing the warning instead would also hide the next field you discard by accident.

### WMSC0006

**`SchemaConfig.Configure` returns a value rather than a task**, so a field set evaluates synchronously even when the caller uses `ParseAsync`. An asynchronous rule reached that way throws `InvalidOperationException`.

Nothing in the type system says so, because `CheckAsync` returns the same schema type a synchronous rule does.

Two ways out:

* Use `Check` if the rule can answer from the value alone.
* Compose the schema outside the field set and parse it with `ParseAsync`. See [Asynchrony](/reference/packages/schemas/asynchrony#compose-it-around-the-outside-instead).

This is an error even though the code generates and compiles. There is no reading of it that works: the rule either throws or is skipped, and it never does its job.

The diagnostic reports at the `CheckAsync` call, which is the one place you can act on. The schema holding it is fine, and so is every other rule in the chain.

### WMSC0007

**Write the receiver as `Schema`.** An alias, a renamed import, or a call with no receiver at all leaves the generator nothing to match, so it generates nothing. Qualify `Schema` with its containing type if you need to.

**A known false positive.** The generator matches the receiver by name, so this fires on *any* unbound call to a member named `Fields` — including one that has nothing to do with a field set and failed to bind for its own reasons. The compiler is already reporting that call, so this rule warns rather than adding a second build failure to a build that has one.

### WMSC0008

**A field's path comes from `CallerArgumentExpression`**, which hands the runtime the argument's source text and nothing else.

A member access reduces to the member's name, and that is the case the whole design is built around: `subject.Title` gives `title`. Anything else keeps its punctuation. A method call, an indexer, a literal or a null-forgiving operator gives a path like `subject.Total.ToString()`, and that text then reaches your logs and your API responses alongside the violation.

Add `.Named("total")` to report it under a name a caller can act on.

**A known false positive.** The derived path is only *usually* wrong. If you are not showing violations to anybody outside your own process, you may not care what the expression reduces to. That is why this warns rather than fails.

### WMSC0009

**A named schema and `Schema.For<T>()` are the same object.** Every named one is a `Schema.For<T>()` behind the property, and the result is cached per type, so neither spelling checks anything the other does not.

Only one of them is where the rules for that type are documented. `Schema.For<int>()` tells a reader nothing; `Schema.Number.Int32` leads them to `AtLeast`, `AtMost` and the rest.

Nine types have a named spelling:

| Type                               | Write                              |
| ---------------------------------- | ---------------------------------- |
| `string`                           | `Schema.Text`                      |
| `bool`                             | `Schema.Bool`                      |
| `Guid`                             | `Schema.Uuid`                      |
| `DateTimeOffset`                   | `Schema.Timestamp`                 |
| `DateOnly`                         | `Schema.Date`                      |
| `int`, `long`, `decimal`, `double` | `Schema.Number.Int32` and siblings |

**This suggests rather than warns**, so a build never mentions it and an IDE offers it as a refactoring. Nothing about `Schema.For<string>()` is wrong; it is only harder to find the rules for.

Nothing is reported for a type with no named spelling. `Schema.For<T>()` over a domain type is the documented way to start your own rules, and an enum has `Schema.Enum<T>()`, which checks membership rather than aliasing `For`.

This one comes from an analyzer rather than from the generator, because a schema is usually declared in a shared static field and a generator only ever sees a `Configure` body. It ships in the same package.


# Analyzers

The Roslyn rules that ship inside Waystone.Monads, what each tier means, and how to turn a rule up or off.

## What this page is for

`Waystone.Monads` ships a Roslyn analyzer inside the package. Install or upgrade to 7.0.0 and you get these rules. You add no reference and configure nothing.

Every rule has an ID like `WM1002`. The first digit tells you how much it matters:

| Tier      | Severity   | What they report                                             | Page                                                    |
| --------- | ---------- | ------------------------------------------------------------ | ------------------------------------------------------- |
| `WM1xxx`  | Warning    | Code that throws or quietly does the wrong thing at run time | [Runtime bugs](/reference/analyzers/runtime-bugs)       |
| `WM2xxx`  | Suggestion | Working code that reads better another way                   | [Idioms](/reference/analyzers/idioms)                   |
| `WM3xxx`  | Off        | Migration aids you turn on while you adopt the library       | [Migration aids](/reference/analyzers/migration-aids)   |
| `WMSxxxx` | Suggestion | Test assertions that read better another way                 | [Assertion rules](/reference/analyzers/assertion-rules) |

Warnings show up in your build. Suggestions show up in your IDE only, so they never break a build that passes today. The `WM3xxx` rules stay off until you enable them.

The `WMS` rules ship in the `Waystone.Monads.Shouldly` package, which you install in test projects only. They are not in the core package.

A separate set of diagnostics uses a `WMG` prefix. Those come from the source generator rather than the analyzer, they are all errors, and they only fire on an enum you marked with `[ErrorCodeCatalog]`. They are on [Source generation](/reference/source-generation) instead.

{% hint style="info" %}
These pages list the rules as at the version they were written for. For the set that ships in the version you installed, read [`Rules.cs`](https://github.com/draekien-industries/waystone-dotnet/blob/main/src/Waystone.Monads.Analyzers/Rules.cs) in the repository — every descriptor is declared there in one file.
{% endhint %}

{% hint style="warning" %}
Do you build with `TreatWarningsAsErrors`? Then a `WM1xxx` rule that fires breaks your build after you upgrade. We chose that on purpose. Every one of these rules marks code that throws or returns the wrong value at run time. Read the rule before you suppress it.
{% endhint %}

## Every rule

| ID                                                        | What it reports                                                                      | Default     |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------ | ----------- |
| [`WM1001`](/reference/analyzers/runtime-bugs#wm1001)      | `Option.Some` given a value that is provably null, which always throws               | Warning     |
| [`WM1002`](/reference/analyzers/runtime-bugs#wm1002)      | `null` written where an `Option` or `Result` belongs                                 | Warning     |
| [`WM1003`](/reference/analyzers/runtime-bugs#wm1003)      | `default` on an `Option` or `Result`, which is null rather than the empty case       | Warning     |
| [`WM1005`](/reference/analyzers/runtime-bugs#wm1005)      | `Option.Some` given a value the compiler treats as maybe-null                        | Warning     |
| [`WM1006`](/reference/analyzers/runtime-bugs#wm1006)      | A discarded `Result`, so the failure vanishes                                        | Warning     |
| [`WM1008`](/reference/analyzers/runtime-bugs#wm1008)      | An `Option` or `Result` declared nullable, which adds a third state                  | Warning     |
| [`WM1011`](/reference/analyzers/runtime-bugs#wm1011)      | An async delegate passed to a synchronous method, so the task is never awaited       | Warning     |
| [`WM2001`](/reference/analyzers/idioms#wm2001)            | `Unwrap` and `UnwrapErr`, which throw when there is no value                         | Suggestion  |
| [`WM2002`](/reference/analyzers/idioms#wm2002)            | `Expect`, which throws when its invariant does not hold                              | Suggestion  |
| [`WM2003`](/reference/analyzers/idioms#wm2003)            | A `throw` inside a member that returns `Result`                                      | Suggestion  |
| [`WM2004`](/reference/analyzers/idioms#wm2004)            | An `IsSome` check with an `Unwrap` inside it                                         | Suggestion  |
| [`WM2005`](/reference/analyzers/idioms#wm2005)            | `Map` followed by `Flatten`, which is `AndThen`                                      | Suggestion  |
| [`WM2006`](/reference/analyzers/idioms#wm2006)            | A state check combined with an unwrap of the same value                              | Suggestion  |
| [`WM2007`](/reference/analyzers/idioms#wm2007)            | `UnwrapOr` given the default of the type                                             | Suggestion  |
| [`WM2008`](/reference/analyzers/idioms#wm2008)            | An `Option` or `Result` compared to `null`                                           | Suggestion  |
| [`WM2009`](/reference/analyzers/idioms#wm2009)            | `Option<Option<T>>`, which has three states where two mean anything                  | Suggestion  |
| [`WM2010`](/reference/analyzers/idioms#wm2010)            | **Retired in 7.0.0.** `Result<T, T>`                                                 | Not shipped |
| [`WM2011`](/reference/analyzers/idioms#wm2011)            | A declaration that names `Some`, `None`, `Ok` or `Err` instead of the base type      | Suggestion  |
| [`WM2012`](/reference/analyzers/idioms#wm2012)            | A nullable member sitting alongside members that use `Option`                        | Suggestion  |
| [`WM2013`](/reference/analyzers/idioms#wm2013)            | A discarded `Option`                                                                 | Suggestion  |
| [`WM2015`](/reference/analyzers/idioms#wm2015)            | `UnwrapOrDefault` or `MapOrDefault` producing a value type                           | Suggestion  |
| [`WM2016`](/reference/analyzers/idioms#wm2016)            | An eager argument that is not free to evaluate                                       | Suggestion  |
| [`WM2017`](/reference/analyzers/idioms#wm2017)            | A delegate that captures, where binding the data with `With` would avoid the closure | Suggestion  |
| [`WM2018`](/reference/analyzers/idioms#wm2018)            | Two `[ErrorCodeCatalog]` enums that generate the same error code                     | Suggestion  |
| [`WM2019`](/reference/analyzers/idioms#wm2019)            | A generated error code that `ErrorCodes.txt` does not list                           | Suggestion  |
| [`WM2020`](/reference/analyzers/idioms#wm2020)            | An `ErrorCodes.txt` entry no catalog generates                                       | Suggestion  |
| [`WM2021`](/reference/analyzers/idioms#wm2021)            | A state check read through a property pattern                                        | Suggestion  |
| [`WM2022`](/reference/analyzers/idioms#wm2022)            | A `Task`-returning method group passed to `AndThenAsync` or `OrElseAsync`            | Suggestion  |
| [`WM2023`](/reference/analyzers/idioms#wm2023)            | An `Option` bound as state by `With` instead of zipped                               | Suggestion  |
| [`WM2024`](/reference/analyzers/idioms#wm2024)            | A delegate with nothing to defer passed to a lazy member                             | Suggestion  |
| [`WM3001`](/reference/analyzers/migration-aids#wm3001)    | A member that returns a nullable type, where `Option<T>` would fit                   | Off         |
| [`WM3002`](/reference/analyzers/migration-aids#wm3002)    | A `throw`, where returning `Result<TOk, Error>` would fit                            | Off         |
| [`WMS2001`](/reference/analyzers/assertion-rules#wms2001) | An assertion on `IsSome`, `IsOk` or `Unwrap` instead of on the monad                 | Suggestion  |
| [`WMS2002`](/reference/analyzers/assertion-rules#wms2002) | An `await` wrapped in parentheses so a synchronous assertion can run                 | Suggestion  |

The id space has gaps. `WM1004`, `WM1007`, `WM1009`, `WM1010` and `WM2014` shipped in 5.x and were removed in 6.0.0. `WM2010` was removed in 7.0.0. A removed id is never reused.

## Changing a rule

You configure every rule through `.editorconfig`, the standard way. Raise one:

```ini
[*.cs]
dotnet_diagnostic.WM2001.severity = warning
```

Silence one:

```ini
[*.cs]
dotnet_diagnostic.WM1005.severity = none
```

Silence one at a single line:

```csharp
#pragma warning disable WM1001
Option<int> option = Option.Some(0);
#pragma warning restore WM1001
```

Put an `.editorconfig` in a subdirectory to scope a rule to part of your solution. Test projects are the common case. `WM2001` earns its keep less in a test, where an unwrap that throws fails the test anyway.

You cannot drop the analyzer and keep the library, because both ship in one package. `.editorconfig` is how you turn rules off.

To raise a whole tier rather than a rule at a time, see [Severity presets](/reference/analyzers/severity-presets). One MSBuild property covers the set.

The `WMS` rules configure the same way, through `dotnet_diagnostic.WMS2001.severity` and the like. You can drop those entirely by removing the `Waystone.Monads.Shouldly` package reference, since the analyzer ships with it — which you cannot do for the `WM` rules, because they ship inside the library.


# Runtime bugs

The WM1xxx rules. Each one marks code that compiles and then throws or quietly does the wrong thing at run time.

These rules are warnings. Each one marks code that compiles and then misbehaves.

| ID                  | What it reports                                                                |
| ------------------- | ------------------------------------------------------------------------------ |
| [`WM1001`](#wm1001) | `Option.Some` given a value that is provably null, which always throws         |
| [`WM1002`](#wm1002) | `null` written where an `Option` or `Result` belongs                           |
| [`WM1003`](#wm1003) | `default` on an `Option` or `Result`, which is null rather than the empty case |
| [`WM1005`](#wm1005) | `Option.Some` given a value the compiler treats as maybe-null                  |
| [`WM1006`](#wm1006) | A discarded `Result`, so the failure vanishes                                  |
| [`WM1008`](#wm1008) | An `Option` or `Result` declared nullable, which adds a third state            |
| [`WM1011`](#wm1011) | An async delegate passed to a synchronous method, so the task is never awaited |

The gaps are real. `WM1004`, `WM1007`, `WM1009` and `WM1010` shipped in 5.x and were removed in 6.0.0. A removed id is never reused, so a suppression you wrote for one of them is dead but harmless.

## WM1001

**`Some` cannot hold null.** `Option.Some(x)` throws `ArgumentNullException` when `x` is null, so `Option.Some(default(string)!)` always throws.

```diff
-Option<string> option = Option.Some(default(string)!);
+Option<string> option = Option.None<string>();
```

The rule fires only when it can prove the value is null without running your program — a `null` literal, or `default(T)` where `T` is a reference type. Use `Option.FromNullable` when the value merely might be null; `WM1005` covers that case.

A `Some` may hold `0`, `false` and any other value-type default from 6.0.0 onwards. Null is the only value it rejects.

**Quick fix:** use `Option.None<T>()`.

## WM1002

**Null where an `Option` or `Result` belongs.** Both types are records, so the compiler lets you write `null` anywhere one is expected. Your next member access then throws `NullReferenceException`.

```diff
-Option<int> option = null;
+Option<int> option = Option.None<int>();
```

The rule covers assignment, `return` and arguments you pass to a method. It also catches a null you wrote as `null!`:

```diff
-Accept(null!);
+Accept(Option.None<int>());
```

Annotating the target `Option<int>?` stops this rule, because the annotation says you meant to allow null. It starts `WM1008` instead, which asks you to drop the annotation. An `Option` already has an empty case, so a nullable one gives you two ways to say the same thing. This holds wherever you write the annotation — a parameter, a return type, or a tuple or array element.

**Quick fix for `Option<T>`:** use `Option.None<T>()`. `Result<TOk, TErr>` gets no quick fix, because nothing in your code says whether you meant `Ok` or `Err`.

## WM1003

**The default of an `Option` or `Result` is null.** `default(Option<int>)` gives you no empty option. It gives you `null`, for the reason `WM1002` explains.

```diff
-Result<int, string> result = default;
+Result<int, string> result = Result.Err<int, string>("not set");
```

**Quick fix for `Option<T>`:** use `Option.None<T>()`.

## WM1005

**You passed a possibly null value to `Some`.** `Some` rejects null. Pass a value the compiler treats as maybe-null and the call throws whenever that value is null.

```diff
-Option<string> option = Option.Some(value);
+Option<string> option = Option.FromNullable(value);
```

This rule reads the compiler's nullable flow state, so it says nothing in a project that has nullable reference types turned off. That project faces the bug most. Turn nullable on.

**Quick fix:** use `Option.FromNullable`.

## WM1006

**You discarded a `Result`.** A `Result` used as a statement throws nothing and reports nothing, so the failure vanishes:

```csharp
Save();   // returns Result<int, Error>, and you just lost the Err case
```

Handle it, or return it to a caller who will:

```csharp
return Save().Match(value => value, error => 0);
```

The rule follows an `await`, including one you wrote with `.ConfigureAwait(false)`, so it catches a discarded `Task<Result<TOk, TErr>>` as well.

Rust marks `Result` as `#[must_use]` for the same reason. C# has no attribute that forces you to consume a return value, so this rule stands in for one.

**No quick fix.** Only you can decide what the failure should do.

## WM1008

**An `Option` or `Result` is declared nullable.** Both are records, so the compiler accepts `Option<int>?`. That gives you three states where two mean anything: a value, an empty option, and null. `None` is already the empty case you are reaching for.

```diff
-Option<int>? option = Find(id);
+Option<int> option = Find(id);
```

The rule reads the annotation itself rather than the compiler's nullable state, so it fires whether or not you build with nullable reference types on.

It also finds the annotation when the type is nested inside another one. A tuple element, an array element and a type argument all count, and the element can sit in any position of the tuple:

```diff
-(Option<int>? a, int b) Make() => (null, 1);
+(Option<int> a, int b) Make() => (Option.None<int>(), 1);
```

`Result` behaves the same way. There the rule points you to `Err` rather than `None`.

**Quick fix:** drop the `?`.

## WM1011

**Your delegate returns a task and the method does not await it.** A synchronous method calls your delegate and stores whatever comes back. Give it one that returns a task and the monad holds the task, not the result.

```diff
-var result = Option.Try(() => FetchCountAsync());
+var result = await Option.TryAsync(() => FetchCountAsync());

-Option<Task<int>> doubled = option.Map(x => DoubleAsync(x));
+Option<int> doubled = await option.MapAsync(x => DoubleAsync(x));
```

Both calls compile. Neither awaits anything, so the work has not finished when the monad is handed back, and anything it throws goes unobserved. For `Try` the damage is worse: **your exception handling is gone entirely** — a throw escapes to your caller instead of becoming a `None` or an `Err`, and your configured exception logger never sees it.

Use the `Async` sibling of whatever you called. It awaits the delegate and holds the result.

## What it does and does not report

The rule asks **where the task ends up**, not whether a delegate produced one.

| Call                                                  | Produces                    | Reported |
| ----------------------------------------------------- | --------------------------- | -------- |
| `Option.Try(() => FetchAsync())`                      | `Option<Task<int>>`         | Yes      |
| `option.Map(x => FetchAsync(x))`                      | `Option<Task<int>>`         | Yes      |
| `result.MapErr(e => FormatAsync(e))`                  | `Result<int, Task<string>>` | Yes      |
| `option.Match(x => FetchAsync(x), () => ZeroAsync())` | `Task<int>`                 | No       |
| `option.MapOr(zeroTask, x => FetchAsync(x))`          | `Task<int>`                 | No       |
| `Option.Some(FetchAsync())`                           | `Option<Task<int>>`         | No       |

`Match` and `MapOr` hand the task straight back, so you can await it and nothing is lost. `Option.Some(FetchAsync())` takes a value rather than a delegate, so you built that task on purpose.

Only a task trapped **inside** an `Option` or a `Result` is a defect — you cannot await it without unwrapping first, and most callers never do.

This is a warning rather than a suggestion because it fires on code that runs and does the wrong thing. It is also the only protection you have against the v6 removal of the async `Try` overloads, which rebinds those call sites silently. See [Silent change 1](/upgrading/older/v5-to-v6#silent-change-1-try-with-an-async-factory).

**No quick fix, deliberately.** Renaming to the `Async` sibling leaves you with an unawaited task, and no fixer can decide where your `await` belongs.


# Idioms

The WM2xxx rules. Your code works; each rule points at a clearer way to write it.

These rules are suggestions. Your code works. The rule points at a clearer way to write it. You see them in your IDE, and they stay out of your build.

| ID                  | What it reports                                                                                                                          | Quick fix                                  |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| [`WM2001`](#wm2001) | `Unwrap` and `UnwrapErr`, which throw when there is no value                                                                             | `UnwrapOrDefault()`                        |
| [`WM2002`](#wm2002) | `Expect`, which throws when its invariant does not hold                                                                                  | `UnwrapOrDefault()`                        |
| [`WM2003`](#wm2003) | A `throw` inside a member that returns `Result`, so the failure escapes the channel your signature promises                              | None                                       |
| [`WM2004`](#wm2004) | An `IsSome` check with an `Unwrap` inside it, which asks the same question twice                                                         | None                                       |
| [`WM2005`](#wm2005) | `Map` followed by `Flatten`, which is `AndThen`                                                                                          | `AndThen`                                  |
| [`WM2006`](#wm2006) | A state check combined with an unwrap of the same value, which is `IsSomeAnd`, `IsNoneOr`, `IsOkAnd` or `IsErrAnd`                       | None                                       |
| [`WM2007`](#wm2007) | `UnwrapOr` given the default of the type, which is `UnwrapOrDefault`                                                                     | `UnwrapOrDefault()`                        |
| [`WM2008`](#wm2008) | An `Option` or `Result` compared to `null`, which reads like an absence check but is not one                                             | The matching state check                   |
| [`WM2009`](#wm2009) | `Option<Option<T>>`, which has three states where only two mean anything                                                                 | None                                       |
| [`WM2010`](#wm2010) | **Retired in 7.0.0.** `Result<T, T>`, whose two implicit conversions were ambiguous                                                      | None                                       |
| [`WM2011`](#wm2011) | A declaration that names `Some`, `None`, `Ok` or `Err` instead of `Option` or `Result`, so it can hold only one of the two states        | The base type                              |
| [`WM2012`](#wm2012) | A nullable member sitting alongside members that use `Option`, so one type has two ways of saying "absent"                               | None                                       |
| [`WM2013`](#wm2013) | A discarded `Option`, as `WM1006` does for `Result`                                                                                      | None                                       |
| [`WM2015`](#wm2015) | `UnwrapOrDefault` or `MapOrDefault` producing a value type, where the default is indistinguishable from a real result                    | `UnwrapOrNull()` or `MapOrNull()`          |
| [`WM2016`](#wm2016) | An argument to `Or`, `And`, `UnwrapOr`, `MapOr` or `OkOr` that is not free to evaluate, so it runs even when it is discarded             | The `Else` sibling                         |
| [`WM2017`](#wm2017) | A delegate that captures a local or a parameter, where binding the data with `With` would avoid the closure                              | `With`, then the same method on the binder |
| [`WM2018`](#wm2018) | Two `[ErrorCodeCatalog]` enums that generate the same error code                                                                         | None                                       |
| [`WM2019`](#wm2019) | A generated error code that `ErrorCodes.txt` does not list                                                                               | Update `ErrorCodes.txt`                    |
| [`WM2020`](#wm2020) | An `ErrorCodes.txt` entry no catalog generates                                                                                           | None                                       |
| [`WM2021`](#wm2021) | `IsSome`, `IsNone`, `IsOk` or `IsErr` read through a property pattern, which hides the check from the rules that read it                 | None                                       |
| [`WM2022`](#wm2022) | A `Task`-returning method group passed to `AndThenAsync` or `OrElseAsync`, whose step returns a `ValueTask`                              | Wrap it in an async lambda                 |
| [`WM2023`](#wm2023) | An `Option` bound as state by `With`, leaving the delegate to unwrap it and the absent case to be forgotten                              | None                                       |
| [`WM2024`](#wm2024) | A delegate whose body is already built, passed to `AndThen`, `OrElse`, `UnwrapOrElse`, `MapOrElse` or `OkOrElse`, so nothing is deferred | The eager sibling                          |

There is no `WM2014`. It shipped in 5.4.0 as a `FlatMap` rename aid and was removed in 6.0.0. `WM2010` is listed above because build output from 6.x still links to it; nothing in 7.0.0 reports it.

`WM2008` owns every null comparison and null pattern, so `WM1002` leaves those alone. You get one diagnostic per site, not two.

`WM2007` and `WM2015` point opposite ways on purpose, and both are suggestions so you can decide. `WM2007` says `UnwrapOr(0)` is `UnwrapOrDefault()`, which is shorter and says what it means. `WM2015` then says that on a value type `UnwrapOrDefault()` hands back `0`, which you cannot tell apart from a real zero, and offers `UnwrapOrNull()`. Take the second one where the difference matters to you. Applying the `WM2007` quick fix on a value type reports `WM2015` on the result, for the same reason. The same pair applies to the quick fixes `WM2001` and `WM2002` offer.

`WM2003` ignores the same throws as [`WM3002`](/reference/analyzers/migration-aids#wm3002).

We split `WM2001` and `WM2002` on purpose. `Expect` states an invariant and names it in the message, which is fair where the invariant is real. So you can keep `WM2002` on and turn `WM2001` off, or the reverse.

## WM2001

**`Unwrap` throws when there is nothing to unwrap.** It converts an absence you had already captured in the type back into an unhandled exception.

```diff
-int count = option.Unwrap();
+int count = option.UnwrapOr(0);
```

`UnwrapOrElse` defers the fallback until it is needed, and `Match` handles both branches explicitly. Reach for `UnwrapOrElse` only where producing the fallback costs something — hand it a value you already have and [`WM2024`](#wm2024) reports it. On a value type the quick fix reports `WM2015`, for the reason given above.

**Quick fix:** `UnwrapOrDefault()`.

## WM2002

**`Expect` throws too, and names the invariant on the way out.** That is defensible where the invariant is genuine, which is why this is a separate rule from `WM2001` — you can keep one on and turn the other off.

```diff
-int count = result.Expect("the length was validated upstream");
+int count = result.UnwrapOr(0);
```

**Quick fix:** `UnwrapOrDefault()`.

## WM2003

**A `throw` inside a member returning `Result` goes around its own signature.** The signature promises failures arrive as values, so a caller who handled `Err` still has to wrap the call in `try` to be safe.

```diff
-if (!found) throw new InvalidOperationException("no such user");
+if (!found) return new Error("NoSuchUser", "no such user");
```

Ignores the same throws `WM3002` does, listed under Migration aids.

## WM2004

**An `IsSome` check with an `Unwrap` inside it asks the same question twice.** Nothing enforces that the two answers agree; whoever reads the code has to notice.

```diff
-if (option.IsSome) Console.WriteLine(option.Unwrap());
+option.Inspect(Console.WriteLine);
```

Use `Match` where both branches do work, `Inspect` where only the present one does.

## WM2005

**`Map` followed by `Flatten` is `AndThen`.** Mapping with a function that itself returns an `Option` builds an `Option<Option<T>>`, and the flatten then takes apart what the map just built.

```diff
-option.Map(FindParent).Flatten()
+option.AndThen(FindParent)
```

**Quick fix:** `AndThen`.

## WM2006

**A state check combined with an unwrap of the same value already has a name.** `IsSomeAnd`, `IsNoneOr`, `IsOkAnd` and `IsErrAnd` take the predicate and supply the value to it.

```diff
-if (option.IsSome && option.Unwrap() > 10)
+if (option.IsSomeAnd(x => x > 10))
```

Distinct from `WM2004`: that one is a guard around a block, this one is a single boolean expression.

## WM2007

**`UnwrapOr` given the default of the type is `UnwrapOrDefault`.** The same result without naming the type's default yourself.

```diff
-int count = option.UnwrapOr(0);
+int count = option.UnwrapOrDefault();
```

On a value type, applying this reports `WM2015` on the result. That is deliberate and explained above.

**Quick fix:** `UnwrapOrDefault()`.

## WM2008

**Comparing an `Option` or `Result` to `null` reads as an absence check and is not one.** Neither type is ever null in correct use, so the comparison is always false and the case you meant to test goes untested.

```diff
-if (option == null)
+if (option.IsNone)
```

Covers `is null` and `is not null` patterns as well as `==` and `!=`. `WM2008` owns every null comparison, which is why `WM1002` leaves them alone — one diagnostic per site, not two.

**Quick fix:** the matching state check.

## WM2009

**`Option<Option<T>>` has three states and two meanings.** An absent outer and an absent inner are different values that almost no caller acts on differently.

```diff
-Option<Option<User>> user = FindUser(id).Map(LoadProfile);
+Option<Profile> user = FindUser(id).AndThen(LoadProfile);
```

It usually means a `Map` wanted to be an `AndThen`, which is `WM2005`.

## WM2010

**Retired in 7.0.0. This rule no longer ships.** The anchor stays because build output from earlier versions links here.

`Result` used to declare one implicit conversion from `TOk` and another from `TErr`. Where those were the same type the compiler could not choose between them, so every implicit conversion became a compile error — and `Ok` and `Err` were indistinguishable to a reader. 7.0.0 removes both conversions, so there is nothing left for the rule to report. A `Result<T, T>` is still worth avoiding for the second reason.

```diff
-Result<string, string> Parse(string input);
+Result<string, Error> Parse(string input);
```

## WM2011

**`Some`, `None`, `Ok` and `Err` are the cases, not the type.** A field declared `Some<int>` can never be `None`, which is the entire point of `Option`.

```diff
-Some<int> total;
+Option<int> total;
```

**Quick fix:** the base type.

## WM2012

**A nullable member beside `Option` members gives one type two ways to say "absent".** Callers then have to remember which convention applies to which member.

```diff
-string? DisplayName { get; }
+Option<string> DisplayName { get; }
```

Fires only on a type that already uses `Option` or `Result` somewhere. `WM3001` is the version for a codebase that has not adopted the library at all.

## WM2013

**A discarded `Option` is a question nobody read the answer to.** Less harmful than discarding a `Result`, which `WM1006` reports as a bug, but usually still a mistake.

```diff
-option.Filter(IsActive);
+Option<User> active = option.Filter(IsActive);
```

## WM2015

**On a value type, `UnwrapOrDefault` hands back `0` for the absent case.** `T?` on a type parameter constrained only to `notnull` is an annotation, not a `Nullable<T>`, so nothing distinguishes "there was no value" from a real zero. The message names the value you get back, so it reads "hands back 0, the default of 'int'", and it renders an enum's zero member by name.

```diff
-int? count = option.UnwrapOrDefault();
+int? count = option.UnwrapOrNull();
```

Perfectly legitimate where you do want the default, which is why this informs rather than warns. `MapOrDefault` and `MapOrNull` work the same way.

**Quick fix:** `UnwrapOrNull()` or `MapOrNull()`.

## WM2016

**An eager argument runs even when it is thrown away.** `And`, `Or`, `UnwrapOr`, `MapOr` and `OkOr` evaluate their argument before they check whether the receiver needs it. An expensive call or one with a side effect runs unconditionally.

```diff
-option.UnwrapOr(LoadFallbackFromDisk())
+option.UnwrapOrElse(() => LoadFallbackFromDisk())
```

The lazy siblings (`AndThen`, `OrElse`, `UnwrapOrElse`, `MapOrElse` and `OkOrElse`) take a delegate and only call it when the other branch is taken.

The rule reports what it cannot prove is **free**, not what it can prove is expensive, because only the first is decidable. It stays silent on a constant, a bare local, parameter, field or property read, and `default`. It also stays silent on an expression built entirely out of those, so it leaves `fallback + 1`, `defaults[0]` and `flag ? a : b` alone.

It fires on a call, a `new` and an `await`. It also fires on a user-defined operator or implicit conversion. Both are ordinary method calls, however short they look — `option.UnwrapOr(count)` reports when `count` is an `int` and the option holds a type you can implicitly convert an `int` to.

The message tells you which of two reasons applies:

| The message says                    | What to do                                                                 |
| ----------------------------------- | -------------------------------------------------------------------------- |
| `and computing it may be expensive` | Weigh it. The rule cannot tell a cheap call from a costly one              |
| `and evaluating it changes state`   | Act on it. An increment or an assignment runs on every call, needed or not |

Moving a state-changing argument to the `Else` sibling changes what your code does, not just what it costs.

{% hint style="info" %}
**A known false positive.** The rule cannot tell a cheap call from an expensive one, so it fires on `option.UnwrapOr(GetZero())` as readily as on a database round-trip. When it does, you pay a delegate allocation to avoid nothing. That is why it is a suggestion. Ignore it where the call is trivial.
{% endhint %}

A bare property read is skipped whatever the receiver, including one whose getter computes. We cannot tell an auto-property from a computed one when it comes from another assembly, and a rule that behaved differently depending on which assembly declared a property would be worse than one that skips them all.

**Quick fix:** wrap the argument in a lambda and call the `Else` sibling.

## WM2017

**Your delegate captures, and `With` would let it stop.** A lambda that reads a local or a parameter from the enclosing method allocates a display class every time the call site runs.

```csharp
Option<int> share = reward.Map(gold => gold / partySize);
```

Bind the data to the receiver instead. `With` hands it to the delegate as an argument, so the delegate closes over nothing and the compiler caches it.

```csharp
Option<int> share = reward
    .With(partySize)
    .Map(static (gold, party) => gold / party);
```

The closure costs 88 bytes at every call — 24 for the display class, 64 for the delegate. `Match` is the most expensive of them to call with a closure. Its two branches share one display class but need a delegate each, so the call costs 152 bytes rather than 88.

The rule reads its list of methods off the binder that `With` returns, rather than matching names. Three things follow:

* It covers nearly every delegate-taking method on both types. See [Where you can use it](/reference/state-overloads#where-you-can-use-it).
* It reaches the `…Async` methods.
* A method the binder does not carry — `ZipWith` and `Reduce` — never gets pointed at a rewrite that does not exist.

Up to 7.1.0 the rule pointed at the [state overload](/reference/state-overloads#passing-the-data-as-the-first-argument) instead. We still support those overloads. The rule stopped naming them for two reasons: `With` reads in call order, and it covers async delegates, which no state overload does.

It stays quiet when:

* **The lambda captures only `this`** — a bare field read, or a call to another method on the same type. That allocates a delegate rather than a display class, a much smaller cost, and reporting it would fire on most ordinary code.
* **The lambda captures nothing.** The compiler already caches it in a static field, so binding would buy you nothing.
* **You are already passing state**, either through `With` or through a state overload.

**Quick fix:** bind the state with `With`. The fix does five things at once, because the rewrite does not compile without all of them:

* Inserts the `With` call on the receiver.
* Adds the parameter to every delegate in the call and marks each one `static`.
* Rewrites every use of the captured names inside the delegate bodies.
* Packs two or more captures into a tuple.
* Adds the extensions `using` when the file does not already have it.

It names the new parameter around whatever is in scope — `state`, then `state1` — rather than reusing the captured name, which would shadow your local.

It declines three cases rather than guess, so you will sometimes see the diagnostic with no lightbulb behind it. Rewrite these by hand:

* **A method group argument.** It cannot grow the parameter the binder's delegate needs.
* **A capture one of the lambdas already declares**, as a parameter or as a local. The rewrite would shadow it, and from C# 8 that is legal, so it would be silent.
* **A capture that cannot name a tuple member** — one called `Rest`, or `ItemN` anywhere but position N. Naming the members anything other than your variables would put invented names in your source.

## WM2018

**Two enums generate the same error code.** An `[ErrorCodeCatalog]` enum builds each code from a format, and by default that format is the enum's name and the member's name, so two enums sharing a name in different namespaces generate the same code for every member name they share.

```csharp
namespace Ordering;

[ErrorCodeCatalog]
public enum OrderErrorCode { NotFound }   // "OrderErrorCode.NotFound"

namespace Shipping;

[ErrorCodeCatalog]
public enum OrderErrorCode { NotFound }   // "OrderErrorCode.NotFound" -- the same code
```

Whoever reads the code cannot tell which error happened. Namespaces separate the two enums in your source and do not separate the codes.

The rule reports on the second declaration in alphabetical order, and names both members and the shared code in the message. It reports once per colliding member, not once per pair of enums, so an enum with three shared members gives you three diagnostics.

The rule keys on the generated code, not on the enum's name, so a `Format` moves what it sees. Two differently named enums that share `"order.{member:kebab}"` collide and are reported; two enums sharing a name with different formats do not collide and are not.

**No quick fix.** The fix is a rename or a different format, and which of the two enums should keep the code is not something the analyzer can work out.

See [Error code catalogs](/reference/source-generation/error-code-catalogs) for what the attribute generates.

## WM2019

**A generated error code is missing from the registry.** A project that commits an `ErrorCodes.txt` has opted into reviewing its error codes as a list, so a code that is not on the list is a wire contract nobody read a diff for.

```csharp
[ErrorCodeCatalog(Format = "order.{member:kebab}")]
public enum OrderErrorCode
{
    NotFound,        // "order.not-found" -- listed
    AlreadyShipped,  // "order.already-shipped" -- not listed, reported here
}
```

Reported on the member, because that is the thing you can act on.

**Quick fix: Update `ErrorCodes.txt`.** It rewrites the whole file from the compilation — every missing code added, every stale entry removed, sorted, your leading comment block kept. One invocation is enough however many diagnostics there are.

A project with no `ErrorCodes.txt` never sees this rule. See [Reviewing generated codes](/reference/source-generation/reviewing-codes#reviewing-your-codes-as-a-list).

## WM2020

**The registry lists a code nothing generates.** The other direction: an entry left behind by a rename or a deletion, claiming a code the project no longer produces.

```
order.already-shipped
order.cancelled          <- nothing generates this any more
order.not-found
```

Reported against `ErrorCodes.txt` itself, at the line, because nothing in your source corresponds to it.

**No quick fix.** Roslyn does not offer fixes for a diagnostic reported at the end of a compilation, which this has to be — whether an entry is stale cannot be known until every enum in the project has been seen. In practice the `WM2019` fix removes stale entries too, so the two travel together; delete the named line by hand if it is your only divergence.

{% hint style="warning" %}
This rule's severity cannot be set from a path-matched `.editorconfig` section, not even `[*]`. It needs a global analyzer config — see [Reviewing generated codes](/reference/source-generation/reviewing-codes#making-a-divergence-fail-the-build).
{% endhint %}

## WM2021

**A property pattern is a state check the other rules cannot see.** `option is { IsSome: true }` asks exactly what `option.IsSome` asks. It reads as though pattern matching is doing something for you here, and it is not — the monad exposes no value to destructure, so the pattern only reaches the same boolean by a longer route.

```diff
-if (option is { IsSome: true }) { return option.Unwrap(); }
+return option.UnwrapOr(0);
```

The rule fires wherever a property subpattern reads `IsSome`, `IsNone`, `IsOk` or `IsErr` on an `Option` or a `Result`: an `is` expression, a negated one, a `switch` arm, a `when` clause, and a subpattern nested inside another type's pattern.

```diff
-return option switch { { IsSome: true } => 1, _ => 0 };
+return option.MapOr(0, _ => 1);
```

The value you test for makes no difference. `{ IsSome: false }` asks the same question as `{ IsSome: true }` and hides it the same way.

{% hint style="info" %}
**Why this is its own rule.** `WM2004` and `WM2006` both match a property *read*. A subpattern is not one, so writing the check as a pattern used to silence them without changing what the code does. That is what this rule reports — not the pattern's style, but the fact that it opts your call site out of the rules that would otherwise read it.
{% endhint %}

Fixing this rule leaves you with `option.IsSome`, which `WM2004` may then report if an unwrap follows it. That chain is deliberate. Each rule states one true thing, and the second only becomes visible once the first is resolved — the same way `WM2007` and `WM2015` pair up.

**No quick fix.** The rewrite depends on where the pattern sits. An `is` expression becomes a property read, a negated one becomes a negated read, and a `switch` arm needs restructuring into a `Match`. A fix that handled only the first would leave the two shapes most worth correcting untouched, while implying the rule had been dealt with.

## WM2022

**`CS0411` here is not a generics problem.** `AndThenAsync` and `OrElseAsync` take a step that returns `ValueTask<Option<T>>` or `ValueTask<Result<TOk, TErr>>`. Hand one a method group that returns `Task<...>` and the conversion fails, so the compiler reports a type inference failure — a message that names neither `ValueTask` nor the parameter, and sends you looking at your type arguments. This rule says the part `CS0411` leaves out.

```diff
-Task<Option<Order>> LoadOrder(int id) => ...;
+ValueTask<Option<Order>> LoadOrder(int id) => ...;

 // option is an Option<int>
 ValueTask<Option<Order>> order = option.AndThenAsync(LoadOrder);
```

There are two corrections, and which you want depends on who owns the step.

**Change the step to return `ValueTask`** where the step is yours. Prefer this. Every async member of this library returns a `ValueTask`, so a step that does the same drops straight into another chain as a step of its own.

**Wrap it** where the step is third-party and you cannot change its signature:

```csharp
option.AndThenAsync(async id => await LoadOrder(id));
```

**The quick fix offers the wrap only**, though the message names both corrections. Retyping the step is safe only where it is already `async` — a `Task.FromResult` body does not convert — and it changes a signature every other caller of that member sees. A fix reading one call site cannot judge either, so the message states both and leaves the choice to you.

## What it does and does not report

The rule fires on `AndThenAsync` and `OrElseAsync` only, on both `Option` and `Result`, and on both the monad and the awaited-receiver extensions.

It fires only where the call **failed to bind**. That is the whole point: a call that compiles needs no advice, and a rule that could not read a failed call would have nothing to report. So `WM2022` never breaks a build that passes today — the build is already broken when it fires, which is also why it is a suggestion rather than a warning.

It reports **method groups**, not lambdas. An async lambda already infers a `ValueTask` from its body, so there is nothing to correct.

The quick fix declines on an **overloaded** method group. The lambda's parameter name comes from the method's own, and there is no reason to prefer one overload's spelling of it over another's.

## WM2023

**`With` binds anything, including another option.** Its type parameter is unconstrained, so the compiler takes an `Option<T>` as readily as an `int`, and nothing in the signature says the pairing is a mistake.

```csharp
Option<int> haul = reward
    .With(bonus)
    .Map(static (gold, extra) => gold + extra.UnwrapOr(0));
```

The binder hands the state to the delegate untouched. So the delegate runs whenever the receiver is `Some`, and the second option's absence is left for you to handle — above, a missing bonus quietly becomes zero.

`Zip` and `ZipWith` are the members for this. Both give `None` when either side is absent, so the case cannot be forgotten.

```csharp
Option<int> haul = reward.ZipWith(
    bonus,
    static (gold, extra) => gold + extra);
```

**Those two do not do the same thing, and that is the point.** The first treats a missing bonus as zero; the second reports it. If the fallback is what you meant, say so with `Reduce`, which keeps a single `Some` when the other side is absent.

**There is no quick fix.** The rewrite lifts the second option out of the delegate's body and into the call, so it has to rewrite the body rather than the call alone — and which of `ZipWith` or `Reduce` you meant is not in the source to read.

### Why Result is not reported

`resultA.With(resultB)` is left alone, deliberately.

`Result` has no `Zip` or `ZipWith`. Neither does Rust's, and the standard idiom there closes over the second result:

```rust
a.and_then(|x| b.map(|y| (x, y)))
```

In C# that capture allocates a display class, which is what [`WM2017`](#wm2017) exists to report. So binding the second result is the capture-free spelling on that side rather than a mistake. Reporting it would put the two rules in a loop, each one naming the other's fix.

### It sits opposite WM2017

[`WM2017`](#wm2017) pushes you toward `With`. This rule pushes one case back off it. The overlap is deliberate: a monad is the one kind of state that buys you nothing, because the delegate still has to unwrap it.

## WM2024

**The `Else` members take a delegate so an expensive fallback runs only on the branch that needs it.** Hand one a value you already have and nothing is deferred — it was built before the delegate was.

```csharp
int gold = reward.UnwrapOrElse(() => 100);
```

The call allocates a delegate for no gain, and it tells whoever reads it that the fallback is costly when it is not. The eager sibling takes the value.

```csharp
int gold = reward.UnwrapOr(100);
```

**Quick fix:** the eager sibling.

### What counts as free

The rule fires only where the delegate's body is provably free of work:

* a literal
* a constant
* `nameof`
* `default`
* a bare local or parameter read

Everything else is left alone — a method call, an object creation, an indexer, or an expression built out of parts.

**A property read is left alone too**, and this is the one place the rule is deliberately narrower than [`WM2016`](#wm2016). That rule counts a property read as free, so it stays quiet on `UnwrapOr(x.Prop)`, which costs you a suggestion you never see. Counting it free here would tell you to run a getter that may compute, on every call, whether the fallback is needed or not. A suggestion you miss and a getter that runs when it should not are not the same size of mistake.

### It is the inverse of WM2016

[`WM2016`](#wm2016) says *prefer the lazy variant when the argument is not free*. This one says *prefer the eager variant when the delegate is*.

They can never both fire on one call site. `WM2016` reads the non-delegate argument of an eager member; this reads the delegate argument of a lazy one, and the two sets of member names do not overlap.

### Synchronous members only

`AndThenAsync`, `OrElseAsync` and their siblings are not reported. A free body is barely reachable there — the value would have to be a task you already hold — and the rewrite would swap an async lambda for a task rather than unwrap an expression.

### Where the fix declines

Two shapes are reported and left alone, rather than rewritten into something that does not compile.

**A state overload**, where the delegate is not the first argument. The eager sibling has nowhere to put the bound state. The message still names the member to move to, and `UnwrapOr(0)` is what you want there.

**A block body**, where the expression sits inside a `return`. Lifting it out would drop any statement standing beside it.


# Migration aids

The WM3xxx rules. They ship off, and you turn them on while you convert a codebase to the library.

These two rules ship **off**. They report on code that has not adopted the library yet, so in most codebases they fire everywhere. Turn them on while you convert, then turn them off again.

| ID                  | What it reports                                                                                          |
| ------------------- | -------------------------------------------------------------------------------------------------------- |
| [`WM3001`](#wm3001) | A member that returns a nullable type, where `Option<T>` would make the absent case impossible to ignore |
| [`WM3002`](#wm3002) | A `throw`, where returning `Result<TOk, Error>` would state the failure in the signature                 |

Enable one in your `.editorconfig`:

```ini
[*.cs]
dotnet_diagnostic.WM3001.severity = suggestion
dotnet_diagnostic.WM3002.severity = suggestion
```

`WM3002` ignores the throws a `Result` would not improve: `ArgumentException` and its subtypes, `NotImplementedException`, `NotSupportedException`, `ObjectDisposedException`, a bare `throw;` rethrow, and any throw inside a lambda you pass to `Option.Try` or `Result.Try`.

## WM3001

**A nullable return leaves the absent case easy to ignore.** `Option<T>` makes the caller acknowledge it. Off by default, because in a codebase that has not adopted the library this fires on nearly every member.

```diff
-User? FindUser(int id);
+Option<User> FindUser(int id);
```

`WM2012` is the narrower, on-by-default version, for a type that already uses `Option` elsewhere.

## WM3002

**A `throw` states a failure nowhere in the signature.** Returning `Result<TOk, Error>` puts it there, where a caller cannot miss it. Off by default, because it fires on every throw in a codebase that has not adopted `Result`.

```diff
-if (!found) throw new InvalidOperationException("no such user");
+if (!found) return new Error("NoSuchUser", "no such user");
```

It skips the throws listed above, which a `Result` would not improve. `WM2003` is the on-by-default version, for a member that already returns `Result`.


# Assertion rules

The WMSxxxx rules. They ship in Waystone.Monads.Shouldly and fire on test assertions only.

These rules ship in `Waystone.Monads.Shouldly`, not in the core package. Add that package to a test project and you get them:

```
dotnet add package Waystone.Monads.Shouldly
```

Their ids start with `WMS`, a second namespace beside `WM`. `WM` ids are validated against the core analyzer assembly, so a rule shipped from another package cannot take one. The `2` means what it means everywhere else on this page: working code that reads better another way, reported as a suggestion, on by default. There is no `WMS1` tier and there should not be one — every rule here fires on a test that already passes.

| ID                    | What it reports                                                      | Quick fix                    |
| --------------------- | -------------------------------------------------------------------- | ---------------------------- |
| [`WMS2001`](#wms2001) | An assertion on `IsSome`, `IsOk` or `Unwrap` instead of on the monad | The matching monad assertion |
| [`WMS2002`](#wms2002) | An `await` wrapped in parentheses so a synchronous assertion can run | The `Async` assertion        |

Both fixes are batch-fixable, so **Fix all occurrences in Project** clears a test suite in one pass. Run it more than once. `WMS2001` rewrites `(await task).IsSome.ShouldBeTrue()` into `(await task).ShouldBeSome()`, which is then `WMS2002`'s input, and a batch fixer lands only non-overlapping fixes per pass.

## WMS2001

**Assert on the monad, not on a piece of it.** `IsSome` and `IsOk` yield a `bool`, and `Unwrap` yields the contained value. Either way the assertion that follows never sees the monad, so a failing test cannot tell you what it found.

```diff
-option.IsSome.ShouldBeTrue();
+option.ShouldBeSome();
```

The `IsSome` version fails with "expected True, was False". The `ShouldBeSome` version names the `None`. The `Unwrap` version is worse: `Unwrap` throws before the assertion runs, so the test fails on a panic rather than on an assertion, and the message is about `Unwrap` rather than about your expectation.

```diff
-option.Unwrap().ShouldBe(42);
+option.ShouldBeSomeValue(42);
```

The rule reads both halves of the pair, so `ShouldBeFalse` picks the opposite assertion:

| You wrote                                                         | The fix writes                |
| ----------------------------------------------------------------- | ----------------------------- |
| `option.IsSome.ShouldBeTrue()` or `option.IsNone.ShouldBeFalse()` | `option.ShouldBeSome()`       |
| `option.IsNone.ShouldBeTrue()` or `option.IsSome.ShouldBeFalse()` | `option.ShouldBeNone()`       |
| `result.IsOk.ShouldBeTrue()` or `result.IsErr.ShouldBeFalse()`    | `result.ShouldBeOk()`         |
| `result.IsErr.ShouldBeTrue()` or `result.IsOk.ShouldBeFalse()`    | `result.ShouldBeErr()`        |
| `option.Unwrap().ShouldBe(x)`                                     | `option.ShouldBeSomeValue(x)` |
| `result.Unwrap().ShouldBe(x)`                                     | `result.ShouldBeOkValue(x)`   |
| `result.UnwrapErr().ShouldBe(x)`                                  | `result.ShouldBeErrValue(x)`  |

`UnwrapErr` on an `Option` is not in that table because it does not compile — an `Option` has no error half.

## What it does not report

**`ShouldBeOfType<Some<T>>()` never reports.** Those sites are usually testing the closed hierarchy itself, and nothing in the syntax separates that from an incidental type check, so rewriting them would delete the only coverage of it. This is excluded by design, not by omission.

**A comparison overload carrying its own options never reports.** That covers `ShouldBe(expected, tolerance)`, the comparer overload, and the ignore-order overload. Those arguments describe how to compare a bare value, and they have no counterpart on an assertion that takes the monad. `ShouldBeTrue` and `ShouldBeFalse` need no equivalent exclusion — a `bool` has nothing to configure.

{% hint style="info" %}
**Two diagnostics on the `Unwrap` line is one problem.** `WMS2001` overlaps `WM2001` there on purpose. The spans differ: `WM2001` reports the panicking call, this rule the whole assertion. Applying the `WMS2001` fix resolves both, because the rewrite is what removes the `Unwrap`. Suppressing either to silence the pair would leave a consumer who does not use this package with no signal at all.
{% endhint %}

The reported span is the whole assertion, and it is **not** faded in your IDE the way most `WM2` rules with a fix are. Fading it would read as "this line can go", and the fix replaces your assertion rather than removing it.

## WMS2002

**Drop the parentheses around the `await`.** Member access binds tighter than `await`, so asserting on a task's result forces you to parenthesise it. Every assertion in this package is also declared on `Task` and `ValueTask` receivers, so you do not have to.

```diff
-(await LoadAsync()).ShouldBeSome();
+await LoadAsync().ShouldBeSomeAsync();
```

The fix appends `Async` to the assertion and moves the `await` outward.

## What it does not report

**`(await task.ConfigureAwait(false)).ShouldBeSome()` never reports.** Read this one before you conclude the rule is broken — `ConfigureAwait` is what this library's own documented style produces everywhere, so this is the shape you are most likely to have written. `ConfigureAwait` returns an awaitable that is not a task, and this package declares no assertion on it, so moving the `await` outward would leave the rewrite with no receiver.

**A chained assertion never reports.** In `(await task).ShouldBeSome().Name`, something else reads the assertion's result. Moving the `await` outward would bind `.Name` to the assertion's task rather than to its value, so the rewrite would change what the test does.

The rule is scoped to an `await` of `Task<T>` or `ValueTask<T>` written directly, and to an assertion whose result nothing else reads. Both exclusions exist for the same reason: outside them, moving the `await` changes what is awaited.


# Severity presets

Turn the analyzer up in one line, without listing thirty rules in your .editorconfig.

## Pick a preset in one line

Set `WaystoneMonadsRuleset` in the project that references the package:

```xml
<PropertyGroup>
    <WaystoneMonadsRuleset>recommended</WaystoneMonadsRuleset>
</PropertyGroup>
```

Three values are valid: `recommended`, `strict`, and `none`. The default is `none`, so nothing changes until you ask.

Get the value wrong and the build fails before the compiler runs, naming the three valid values. That is deliberate — the name is matched, not turned into a file path, so a typo cannot reach the compiler as a missing analyzer config.

## What each preset sets

The library ships 29 rules. Here is what each preset does to them.

| Tier     | Rules                                                                | Shipped default | `recommended` | `strict`    |
| -------- | -------------------------------------------------------------------- | --------------- | ------------- | ----------- |
| Misuse   | `WM1001`, `WM1002`, `WM1003`, `WM1005`, `WM1006`, `WM1008`, `WM1011` | Warning         | **Error**     | **Error**   |
| Idiom    | 20 rules, `WM2001` through `WM2022`                                  | Suggestion      | Suggestion    | **Warning** |
| Adoption | `WM3001`, `WM3002`                                                   | Off             | Off           | **Warning** |

`recommended` changes seven rules and leaves twenty-two alone. Every one of the seven reports code that throws at run time, ignores a failure, or means the opposite of what it reads as. Failing the build on those is a statement the rule messages already make.

`strict` is the posture of a codebase adopting the library wholesale. It turns everything on.

{% hint style="warning" %}
**`strict` will report a lot on an existing codebase**, and the two adoption rules will account for most of it by a wide margin. They fire on every nullable return and every `throw`, converted or not — see [Migration aids](/reference/analyzers/migration-aids).

If you want the idiom rules enforced without that noise, take `recommended` and raise the idiom tier yourself. That is not what `strict` is for.
{% endhint %}

## The test package has the same switch

`Waystone.Monads.Shouldly` reads the same `WaystoneMonadsRuleset` property, so setting it once covers both.

| Rules                | Shipped default | `recommended` | `strict`    |
| -------------------- | --------------- | ------------- | ----------- |
| `WMS2001`, `WMS2002` | Suggestion      | Suggestion    | **Warning** |

`recommended` leaves the assertion rules where they are. They fire on tests that pass, and a test suite is not where you want a build-breaking opinion about assertion style.

## Overriding one rule

A preset is a floor, not a ceiling. Anything you write wins.

```ini
[*.cs]
dotnet_diagnostic.WM1006.severity = warning
```

That works because the presets ship as global analyzer configs at `global_level = -1`, which sits below every level you can author — so your own `.globalconfig` wins a conflict outright rather than producing a tie, and a path-matched `.editorconfig` section beats any global config regardless of level.

Scoping a preset to part of a solution works the same way as scoping a rule: set the property in the projects you want it in, and leave it out of the ones you do not. Test projects are the usual exception.

## Why this is not an .editorconfig fragment you copy

It cannot be one, and the reason is not style.

`WM2020` reports against `ErrorCodes.txt`, which has no syntax tree. Roslyn resolves `dotnet_diagnostic` severities per tree, so a path-matched section cannot reach that rule — not even `[*]`. An `.editorconfig` fragment would therefore ship a preset with one rule silently missing from it. A global analyzer config has no such limit.

## A preset does not flow to your dependents

The preset files ship in the package's `build/` folder rather than `buildTransitive/`, so a project that depends on yours does not inherit your choice.

That matches where the rules themselves go. A `PackageReference` excludes analyzers from transitive consumers by default, so a project that only depends on this package indirectly never gets the analyzer — and a preset that travelled further than the analyzer would set severities for rules nothing in that project reports.


# Overview

What changes between major versions, what you have to do about it, and what is on its way out.

## What version are you on now?

| You are on    | Read                                                                                  |
| ------------- | ------------------------------------------------------------------------------------- |
| 6.x           | [From v6.x](/upgrading/v7/from-v6)                                                    |
| 5.x           | [From v5.x](/upgrading/v7/from-v5) — the combined path, not the two pages in sequence |
| 4.x or older  | [Older upgrades](/upgrading/older), then come back here                               |
| 7.0.0 already | [Deprecations](/upgrading/deprecations) — what is going away next                     |

**If you are skipping v6, take the combined page.** The two sets of changes interact, and one v6 change had a warning that only existed in 5.5.x — so a reader coming from 5.4 or earlier gets it with no signal at all.

## The three entries in this group

* [**Upgrade to v7**](/upgrading/v7) — the current major. Start with [Breaking changes](/upgrading/v7/breaking-changes) if you want to know how bad it is before you pick a path.
* [**Deprecations**](/upgrading/deprecations) — API that still works today but is going away, with the version that removes it. Read this before you upgrade, not after.
* [**Older upgrades**](/upgrading/older) — every hop from v1 to v6, newest first.

## Every upgrade page has an agent prompt

Each of the seven upgrade pages opens with a collapsed block holding a prompt for that hop, ready to copy into Claude Code or a similar tool. It is collapsed by default, so a reader doing the upgrade by hand scrolls past one line.

Three rules apply to every one of them. They are in each prompt, and they are worth knowing before you run one:

* Never suppress a diagnostic, add a pragma to disable one, or add a null-forgiving `!` to make an error go away. Every diagnostic in an upgrade has a real fix.
* Do not change behaviour to make a test pass. A test that fails after an upgrade is either a silent change you missed or a real finding.
* Do not reformat, rename, or refactor anything the upgrade does not require.

## What a prompt cannot do for you

{% hint style="warning" %}
**Step 1 of the v7 prompts needs your judgement.** The prompt asks the agent to find and report every silent change, and to decide only the ones with a mechanical answer.

Two have no mechanical answer:

* **A projection that can return null.** Whether the null meant "absent" or was never supposed to happen decides whether you convert to `Option.FromNullable` or fix the caller. Only someone who knows the domain can say.
* **Coming from 5.x, an `IsNone` branch on a value-type option.** `Option.Some(0)` now gives you a `Some`. Whether that branch was standing in for "zero" needs someone who knows what the code means. See [Silent change 3](/upgrading/older/v5-to-v6#silent-change-3-some-accepts-value-type-defaults).

Expect the agent to bring these to you. If it decided them on its own, that is a finding.
{% endhint %}

## If you would rather not use an agent

Every step in a prompt maps to a section on the upgrade page it sits on, and [Breaking changes](/upgrading/v7/breaking-changes) is the same v7 information as a reference table naming the diagnostic each break produces.


# Upgrade to v7

The current major. Three changes keep compiling and change what your code does; the rest break the build.

7.0.0 is the current major. It is the largest release the library has had, and most of it breaks loudly — the compiler finds it for you.

The part that does not is what to read first.

## At a glance

|                                  |                                          |
| -------------------------------- | ---------------------------------------- |
| Changes that keep compiling      | 3 from v6, 6 if you are coming from v5   |
| Changes that break the build     | 11                                       |
| Removals with no warning release | 1, in `Waystone.Monads.FluentValidation` |
| Analyzer rules added             | `WM2022`                                 |
| Analyzer rules removed           | `WM2010`                                 |

## Pick your path

| You are on   | Read                                                                                        |
| ------------ | ------------------------------------------------------------------------------------------- |
| 6.x          | [From v6.x](/upgrading/v7/from-v6)                                                          |
| 5.x          | [From v5.x](/upgrading/v7/from-v5)                                                          |
| 4.x or older | [Older upgrades](/upgrading/older) up to 5.x first, then [From v5.x](/upgrading/v7/from-v5) |

## The supporting pages

* [**Breaking changes**](/upgrading/v7/breaking-changes) — every v7 break as a reference table, naming the compiler diagnostic each one produces, and whether a code fix exists. Read this if you want the size of the job before you pick a path.
* [**Deprecations**](/upgrading/deprecations) — what 7.0.0 marks for removal in 8.0.0.

Both v7 pages open with a collapsed agent prompt for that path.


# Breaking changes

Every break in 7.0.0, the compiler diagnostic it produces, and whether a code fix handles it.

This is the reference list. For the upgrade itself, with an agent prompt and the order to work in, go to [6.x to 7.0.0](/upgrading/v7/from-v6) or [5.x to 7.0.0](/upgrading/v7/from-v5).

## Start with the silent ones

Three changes leave your code compiling and change what it does. Nothing in your build output will mention them, so they are the only part of this upgrade you have to go looking for.

| What changed                                                                                  | What happens now                                                                                                                         | Where to look                                                                                                        |
| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| A delegate passed to `Map`, `MapAsync`, `Reduce` or `ReduceAsync` on an `Option` returns null | Throws `ArgumentNullException`, naming the delegate parameter — `map` or `reduce`. In 6.x the null was carried into the `Some`.          | Any projection returning a nullable reference, a `FirstOrDefault`, a dictionary lookup, or an explicit `return null` |
| A factory passed to `AndThen` or `AndThenAsync` returns a null `Option` or `Result`           | Throws `ArgumentNullException`, naming `optionFactory` or `resultFactory`                                                                | Factories that can return a null monad, usually from a field or a cache                                              |
| A `MonadOptionsScope` is disposed when it is not the innermost open scope                     | Nothing is restored, and the library writes a `ScopeDisposedOutOfOrder` diagnostic event. In 6.x it restored the wrong options silently. | Any scope disposed by hand rather than with `using`, or held in a field                                              |

The first two throw where 6.x carried a null onward. That is the intended fix — the null was going to surface later as a `NullReferenceException` from code that had every right to assume a `Some` held a value. To map a null onto a `None`, use `AndThen` with `Option.FromNullable`.

The third has its own section on [Configuration](/guides/configuration#what-happens-when-you-dispose-out-of-order), and the event is on [Observability](/guides/observability#watching-for-a-scope-disposed-out-of-order).

## The loud ones

These break the build. The compiler finds them for you; the table tells you what the diagnostic actually means, because several of them name something other than the real problem.

| What changed                                                    | Old → new                                                                                                                                                                             | Diagnostic                                                    | Code fix          |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ----------------- |
| Parameter names across `Option`, `Result` and their case types  | `MapOr(default: x, …)` → `MapOr(defaultValue: x, …)`, and the same for `else` → `valueFactory`, `createDefault` → `defaultFactory`, `createOther` → `optionFactory` / `resultFactory` | `CS1739`                                                      | **Yes**           |
| Implicit conversions to `Option` and `Result` removed           | `Option<int> o = 5;` → `Option.Some(5)`                                                                                                                                               | `CS0029`, `CS1503`                                            | **Yes**           |
| Per-family extension classes collapsed into one class per monad | `AndThenExtensions.AndThenAsync(o, f)` → `o.AndThenAsync(f)`                                                                                                                          | `CS0103`, or `CS0234` on a `using static`                     | No                |
| `MonadOptions` authoring moved to `MonadOptionsBuilder`         | `Use…` methods are on the builder                                                                                                                                                     | `CS1061`, `CS1503`, `CS0029`, `CS0103`                        | No                |
| `MonadOptions.UseExceptionLogger` removed                       | `UseLogger`, `UseLoggerFactory` or `UseLoggerFactoryFrom` from `Waystone.Monads.Extensions.Logging`                                                                                   | `CS1061`                                                      | No                |
| `ErrorCode.FromEnum` removed                                    | `[ErrorCodeCatalog]` and the generated `ToErrorCode()`                                                                                                                                | `CS0117`                                                      | No                |
| `Error.FromEnum` removed                                        | `[ErrorCodeCatalog]` and the generated `{Enum}Catalog.Errors.{Member}(message)`                                                                                                       | `CS0117`                                                      | No                |
| `ErrorCodeFactory.FromEnum` virtual removed                     | `[ErrorCodeCatalog]`; enum codes are settled at compile time now                                                                                                                      | `CS0115`                                                      | No                |
| `Result.Err<TOk>(Enum, string)` overload removed                | `Result.Err(code.ToError(message))`                                                                                                                                                   | `CS1501`                                                      | No                |
| An async chaining step's delegate returns `Task`                | Return `ValueTask`, or wrap it in an async lambda                                                                                                                                     | `CS0411`, plus [`WM2022`](/reference/analyzers/idioms#wm2022) | **Yes**, the wrap |
| `TryAsync` and `CollectAsync` return `ValueTask`                | `Task<Option<T>> t = Option.TryAsync(f);` → add `.AsTask()`, or keep the `await`                                                                                                      | `CS0029`, or `CS1503` passing it to `Task.WhenAll`            | No                |

The four `FromEnum`-family removals and `UseExceptionLogger` were all obsolete in 6.x with a message naming the replacement. Nothing in 7.0.0 is removed without that warning release, with one exception below.

### Where a code fix exists, run it first

`Waystone.Monads` ships fixes for the three rows marked **Yes**. They handle the bulk of a real upgrade, and running them before you edit anything by hand keeps the diff small.

The two keyed to `CS1739`, `CS0029` and `CS1503` attach to the **compiler diagnostic**, not to an analyzer rule — so `dotnet format analyzers` cannot apply them. They appear on the error in your IDE, where **Fix all occurrences in Project** applies them in a batch. The `WM2022` fix is an ordinary analyzer fix and works either way. The [upgrade pages](/upgrading/v7/from-v6) say this again in context.

{% hint style="info" %}
**`.AsTask()` comes up twice.** v6 made every async *extension* return `ValueTask`; v7 does the same to `TryAsync` and `CollectAsync`, the two static members v6 left alone. If you are coming from 5.x you hit both. The v6 half is on [v5.x to v6.x](/upgrading/older/v5-to-v6#loud-change-async-extensions-all-return-valuetask), and the [5.x to 7.0.0 page](/upgrading/v7/from-v5) tells you where in the order to do it.
{% endhint %}

## The one removal with no warning release

**The implicit conversions to `Option` and `Result` were removed without being obsoleted first**, which is not how this repository normally treats public API.

The reason is mechanical rather than a decision to move fast: an obsoleted implicit conversion still takes part in overload resolution. Marking it `[Obsolete]` would have produced a warning and left the conversion working — so the silent wrong-branch behaviour the removal exists to prevent would have carried on for another major version. There was no ordering that gave both a warning and a fix.

The extension-class collapse has the same shape for a different reason: two static classes declaring the same extension member for the same receiver is `CS0121`, so the old and new spellings could not coexist for a version.

## Diagnostics that mask other diagnostics

Three things fire at the declaration phase, and a declaration error stops the compiler reporting body errors in the same project. So your first green build is not the whole job.

**`CS0234` on a `using static`.** A missing type name blocks overload resolution in every file that has the `using`, so parameter renames and conversion errors in those files go unreported until you fix the qualifier.

**`CS0115` on an `ErrorCodeFactory.FromEnum` override.** A codebase with both an override and call sites sees only the override error, fixes it, and then discovers the call sites on the next build.

**`CS0246` on a signature naming `ValidationErr`.** Only if you install Waystone.Monads.FluentValidation, covered below. A field, property or return type spelled `Result<T, ValidationErr>` is a declaration error, so the bodies that call `Validate` stay quiet until you change the signature.

Build, fix, and build again. Twice is not paranoia here.

## Waystone.Monads.FluentValidation

This companion package was rewritten in 7.0.0. If you do not install it, skip this section. If you do, it is a clean break rather than a deprecation, so nothing warned you in 6.x.

`ValidationErr` wrapped a failed `ValidationResult` and converted to an `Error` when you asked. That meant `Result<TValue, ValidationErr>` could not join a chain without a `MapErr` at the seam, and `ToError()` read your configuration at the moment you called it — so the same failure produced a different code depending on where you converted it.

`ValidationError` replaces it and **is** an `Error`.

| What changed                                          | Old → new                                                                             | Diagnostic         |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------ |
| `ValidationErr` removed                               | `ValidationError`, which derives from `Error`                                         | `CS0246`           |
| `Validate` and `ValidateAsync` err with `Error`       | `Result<T, ValidationErr>` → `Result<T, Error>`                                       | `CS0029`           |
| `ValidateAsync` returns `ValueTask`                   | Add `.AsTask()`, or keep the `await`                                                  | `CS0029`, `CS1503` |
| `ValidationErr.ToError()` removed                     | Nothing to call — you already have an `Error`                                         | `CS1061`           |
| `ValidationErr.Create()` removed                      | `Validate` or `ValidateAsync`                                                         | `CS0117`           |
| `AsValidationResult()` and `RuleSetsExecuted` removed | Use `Failures` for the failure list                                                   | `CS1061`           |
| `UseFallbackValidationErrorMessage` removed           | Nothing — the case it covered cannot happen now                                       | `CS1061`           |
| Namespaces shadow FluentValidation's own              | `Waystone.Monads.FluentValidation.Results.Extensions` → `FluentValidation.Extensions` | `CS0246`, `CS0234` |

There is no code fix for any of these.

### The namespaces shadow FluentValidation's own

The types moved out of `Waystone.Monads.FluentValidation.*` and into `FluentValidation.*`, so they sit beside `IValidator` and `ValidationFailure` in the namespace your validator file already imports.

| Old namespace                                         | New namespace                 |
| ----------------------------------------------------- | ----------------------------- |
| `Waystone.Monads.FluentValidation.Results`            | `FluentValidation`            |
| `Waystone.Monads.FluentValidation.Results.Extensions` | `FluentValidation.Extensions` |
| `Waystone.Monads.FluentValidation.Configs`            | `FluentValidation.Configs`    |

The package and assembly keep the name `Waystone.Monads.FluentValidation`, so the `PackageReference` does not change. Delete the old `using` directives; a file that already has `using FluentValidation;` needs nothing back for `ValidationError`.

No shim ships in the old namespace. A forwarding type there would be a distinct type at run time, so `error is ValidationError` would compile against it and silently stop matching — a quiet wrong answer in place of a build error.

### Why the removals had no warning release

`ValidationErr` could not be obsoleted alongside its replacement. The new `Validate` differs from the old one only in its return type, and C# cannot overload on that. The two spellings could only coexist in separate namespaces, which would give `CS0121` to anyone importing both.

### Why `UseFallbackValidationErrorMessage` is gone rather than renamed

It set the message used when a validation failure carried none of its own. A `ValidationError` now has an `internal` constructor that only the failure branch reaches, so it always carries at least one failure. There is no empty case left for a fallback to cover, and `Error` already substitutes the core fallback for a blank message.

### Recovering the failure detail

Where you used to hold a `ValidationErr`, you now pattern match:

```csharp
if (error is ValidationError validationError)
{
    return ValidationProblem(validationError.ToDictionary());
}
```

`Failures` carries the `ValidationFailure` list, and `ToDictionary()` groups the messages by property, exactly as before. Full detail is on [Waystone.Monads.FluentValidation](/reference/integrations/fluent-validation).

### The supported FluentValidation range widened

The package now declares `FluentValidation >= 11.1.0 && < 13.0.0`, where 6.x pinned a single version. 11.1.0 is the first release carrying `ValidationResult.ToDictionary()` as an instance method.

## Rule ids that no longer exist

`WM2010` is retired in 7.0.0. It reported a `Result<T, T>` whose two implicit conversions were ambiguous, and there are no implicit conversions left for it to report on.

Retired ids are never reused, so a stale `.editorconfig` entry or `#pragma` naming one does nothing at all — it does not error, and it does not warn. The full list of retired ids is on [Deprecations](/upgrading/deprecations#rule-ids-that-are-gaps).


# From v6.x

Three changes that keep compiling and change what your code does, and eight that break the build.

<details>

<summary>Upgrade with an agent — copy this prompt</summary>

Pointed at your solution, in Claude Code or a similar tool. It covers every mechanical part of this upgrade.

```
You are upgrading a C# codebase from Waystone.Monads 6.x to 7.0.0.

Work in this order. Do not skip step 1 — it is the only step the compiler cannot do for
you.

## Step 1 — find the silent behaviour changes first

These compile without error and behave differently. Search for each, decide per call
site, and report every one you changed and every one you deliberately left. Do this
before you touch anything that fails to build, because the build errors will otherwise
bury them.

1. A projection returning null now throws. On an Option, Map, MapAsync, Reduce and
   ReduceAsync used to carry a null return from your delegate into the Some. They now
   throw ArgumentNullException, naming the delegate parameter. Find delegates passed to
   those members that can return null — a nullable reference, a FirstOrDefault, a
   dictionary lookup, an explicit return null. Each is a latent exception. Fix by
   projecting into an option instead: AndThen with Option.FromNullable.

2. A factory returning a null monad now throws. AndThen and AndThenAsync used to accept
   a null Option or Result back from your factory. They now throw
   ArgumentNullException naming optionFactory or resultFactory. Return
   Option.None<T>() or an Err rather than null.

3. Disposing an options scope out of order no longer restores. If the codebase disposes
   a MonadOptionsScope by hand rather than with using, or stores one in a field, the
   out-of-order case now declines to restore and writes a
   Waystone.Monads.ScopeDisposedOutOfOrder diagnostic event instead of silently putting
   the wrong options back. Convert every one to using.


There is deliberately no item here about holding a MonadOptions instance. Options did
become an immutable snapshot in 7.0.0, but the type no longer exposes any public
instance member or accessor at all, so code that held one fails to build rather than
quietly going stale. That is compile-time work, and step 4 covers it. Do not
reintroduce it as a silent change.

## Step 2 — upgrade the package and build

Set the package version to 7.0.0. Then build and capture every
diagnostic. Do not fix anything yet. Count the diagnostics by code and report the
counts, so both of us know the size of the job.

Build twice before you trust the count. Three diagnostics in this upgrade fire at the
declaration phase and mask every body-phase error in the same project: CS0234 on a
using static of a removed extension class, CS0115 on an ErrorCodeFactory.FromEnum
override, and — only if the FluentValidation package is referenced — CS0246 on a
signature naming the removed ValidationErr type. Fix those first, then re-count.

## Step 3 — take the code fixes the package offers

Waystone.Monads ships code fixes keyed to the compiler diagnostics this upgrade
produces, not to analyzer rules — so dotnet format analyzers will not apply them. They
appear on the error itself in your IDE, and "Fix all occurrences in Project" applies
them in a batch:

- CS1739 — a renamed parameter. The fix substitutes the new name.
- CS0029 and CS1503 — a removed implicit conversion. The fix wraps the value in
  Option.Some, Result.Ok or Result.Err. Where a Result carries the same type on both
  sides it offers both and does not pick for you.
- CS0029 and CS1503 on an async chain — the fix adds .AsTask(). Prefer changing the
  declared type or awaiting the value; .AsTask() allocates.
- WM2022 — a Task-returning method group passed to AndThenAsync or OrElseAsync. The fix
  wraps it in an async lambda.

If you cannot drive an IDE, do these by hand from the diagnostic list in step 4.
Rebuild and re-count either way.

## Step 4 — work the remaining diagnostics by code

- Anything touching configuration — CS1061, CS1503, CS0029, CS0103. In 7.0.0
  MonadOptions publishes an immutable snapshot and the authoring surface moved to a new
  MonadOptionsBuilder. The Use* methods live on the builder, not on MonadOptions.
  - MonadOptions.Configure(options => options.UseFallbackErrorCode("x")); still
    compiles unchanged, because the lambda parameter's type is inferred. This is the
    dominant call shape. Leave call sites that already build alone.
  - An explicitly typed lambda parameter breaks. (MonadOptions options) => becomes
    (MonadOptionsBuilder options) =>, or drop the type annotation and let it infer.
  - A field, parameter, local or property typed MonadOptions has no replacement.
    Configuration is reachable only inside a Configure or BeginScope callback now. Move
    the Use* calls into one; do not try to pass options around.
  - In the satellite packages, MonadOptionsExtensions is renamed to
    MonadOptionsBuilderExtensions, and its extension receiver changes from MonadOptions
    to MonadOptionsBuilder. A using static or a qualified static call naming the old
    class must be updated; a reduced extension call on the callback's parameter needs no
    change.
  - Never stash the MonadOptionsBuilder the callback hands you. It is authoring state,
    not the published options — calls made on it after the callback returns are
    discarded without error.
- CS1739 — "does not have a parameter named X". A parameter was renamed. Read the new
  name from the signature and update the named argument. Do not convert the call to
  positional arguments to dodge it; the name was chosen to say what the argument is for.
- CS0029 / CS1503 — cannot convert. Either the implicit conversions to Option and Result
  were removed, so wrap the value explicitly in Option.Some, Result.Ok or Result.Err; or
  an async member now returns ValueTask<T> where it returned Task<T> — in the core
  package Option.TryAsync, Result.TryAsync and CollectAsync are the three that changed in
  7.0.0, and the FluentValidation package's ValidateAsync changed with them. For the
  ValueTask case, prefer changing the local's type or awaiting it to converting with
  .AsTask(), which allocates. A call to Task.WhenAll is the one place .AsTask() is the
  right answer.
- CS0103 / CS0234 — the name does not exist. The per-family extension classes were
  collapsed into one class per monad. A using static or a qualified static call naming
  the old class must move to OptionExtensions or ResultExtensions, or better, become a
  reduced extension call on the receiver.
- CS0117 / CS1061 / CS1501 — no such member or overload. A member obsoleted in 6.x was
  removed. The five are ErrorCode.FromEnum, Error.FromEnum, ErrorCodeFactory.FromEnum,
  MonadOptions.UseExceptionLogger, and the Result.Err<TOk>(Enum, string) overload. Read
  the obsoletion message in the 6.x package for the replacement it names.
  - UseExceptionLogger is the one worth spelling out. If its delegate only logged, replace
    it with UseLoggerFactory, UseLoggerFactoryFrom or UseLogger on the builder, from the
    Waystone.Monads.Extensions.Logging package. If it did anything else — a metric, a
    span, a bug report — that is an observer, not a logger: subscribe with
    MonadDiagnostics.ExceptionHandledEvent.Subscribe instead. Do not reach for a
    DiagnosticListener by name; the typed token exists so a wrong name cannot fail
    silently.
- CS0115 — no suitable method found to override. A virtual a consumer overrode is gone.
  ErrorCodeFactory.FromEnum is the one this hits: enum codes are settled at compile time
  now and a factory cannot change them. Delete the override and use [ErrorCodeCatalog]
  instead.
- CS0411 — type arguments cannot be inferred. An async chaining step's delegate returns
  a Task where a ValueTask is wanted. WM2022 reports the same call and says which
  parameter. Change the step to return ValueTask, or wrap it in an async lambda.
- Anything naming Waystone.Monads.FluentValidation. Check whether the solution references
  that package before reading this; if it does not, skip the whole bullet. If it does,
  the package was rewritten in 7.0.0 rather than deprecated, so nothing warned in 6.x and
  no code fix exists. Every call site needs a hand edit.
  - The namespaces shadow FluentValidation's own (CS0246, CS0234). Delete every using of
    Waystone.Monads.FluentValidation.Results.Extensions, .Results and .Configs, and use
    FluentValidation.Extensions, FluentValidation and FluentValidation.Configs. A file
    that already has using FluentValidation; needs nothing back for ValidationError. The
    package and assembly names are unchanged, so leave the PackageReference alone.
  - ValidationErr is gone (CS0246). ValidationError replaces it and derives from Error,
    so a failed validation now joins a chain without a MapErr at the seam.
  - Validate and ValidateAsync err with Error, not ValidationErr (CS0029). Change the
    declared type to Result<T, Error>, and delete the MapErr that used to convert.
  - ToError() is gone (CS1061). Delete the call — you already hold an Error.
  - ValidationErr.Create() is gone (CS0117). Only Validate and ValidateAsync build a
    ValidationError now; its constructor is internal.
  - AsValidationResult() and RuleSetsExecuted are gone (CS1061). Failures carries the
    ValidationFailure list, and ToDictionary() groups messages by property as before.
  - UseFallbackValidationErrorMessage is gone (CS1061) with no replacement. A
    ValidationError always carries at least one failure, so the empty case it covered
    cannot happen. Delete the call.
  - To read failure detail from a plain Error, pattern match: if (error is
    ValidationError validationError). Report every place you had to add that match.
  - Tell me if the resolved FluentValidation version moved. The package now allows
    >= 11.1.0 && < 13.0.0 where 6.x pinned one version, so the upgrade can pull a
    different FluentValidation than the solution was tested against. Do not pin it back
    without asking me.

## Step 5 — verify

Build clean, then run the test suite. Report: the diagnostic counts before and after,
every silent change from step 1 with the decision you made, and anything you could not
resolve.

## Step 6 — offer these, do not apply them

Once the build is clean, tell me about these optional things and let me decide. Do not
install a package or change a severity in this step.

- Waystone.Monads.Shouldly, which replaces assertions on IsSome and Unwrap in the test
  suite. Its WMS2001 and WMS2002 rules are batch-fixable.
- Waystone.Monads.Linq, which adds C# query syntax over the monads.
- The recommended severity preset, which makes the seven misuse rules build errors. A
  major upgrade is a reasonable moment to turn it on, but it is my call, not yours.
- Waystone.Monads.Extensions.Hosting, if the application is built on
  Microsoft.Extensions.Hosting. builder.AddWaystoneMonads(...) registers the
  configuration and installs it from the host's own start-up sequence, replacing the
  hand-written MonadOptions.Configure call. Say where the current Configure call lives so
  I can see what it would replace.
- Waystone.Monads.Extensions.DependencyInjection, only if the application has a container
  but is not built on the hosting abstractions. It gives services.AddWaystoneMonads(...),
  and the configuration is applied by a separate provider.UseWaystoneMonads() call. That
  second call is easy to forget; if it is missed the library writes a
  Waystone.Monads.ConfigurationNotApplied event rather than failing. Prefer the hosting
  package where both would work, because it makes that call for you.

## Rules

- Never suppress a diagnostic, add a pragma to disable one, or add a null-forgiving !
  to make an error go away. Every diagnostic here has a real fix.
- Do not change behaviour to make a test pass. If a test fails after the upgrade, that
  is either a step 1 change you missed or a real finding — report it, do not paper over
  it.
- Do not reformat, rename, or refactor anything the upgrade does not require.
```

</details>

{% hint style="warning" %}
**One step it cannot do for you**, in step 1 of the prompt. Whether a null projection meant "absent" or was never supposed to happen is a domain question, and only you can answer it. Expect the agent to bring it to you; if it decided on its own, that is a finding.
{% endhint %}

## Read this first

Three changes keep compiling and change what your code does:

1. [A projection that returns null now throws](#silent-change-1-a-null-projection-throws).
2. [`AndThen` rejects a factory that returns a null monad](#silent-change-2-andthen-rejects-a-null-monad).
3. [A scope disposed out of order restores nothing](#silent-change-3-a-scope-disposed-out-of-order-restores-nothing).

Everything else in 7.0.0 breaks loudly. The compiler will find it for you, and [Every v7 break](/upgrading/v7/breaking-changes) lists which diagnostic each one produces.

## Silent change 1: a null projection throws

`Map`, `MapAsync`, `Reduce` and `ReduceAsync` on an `Option` used to carry a null return from your delegate straight into the `Some`. They now throw `ArgumentNullException`, naming the delegate parameter.

```csharp
Option<string> name = user.Map(u => u.MiddleName); // MiddleName can be null

// 6.x: a Some holding null.
// 7.0.0: ArgumentNullException, naming 'map'.
```

This is a fix, not a tightening for its own sake. A `Some` holding null broke the one promise the type makes, and the failure surfaced later as a `NullReferenceException` in code that had every right to assume a `Some` held a value — a long way from the projection that caused it.

### The repair

Project into an option rather than into a value:

```diff
-Option<string> name = user.Map(u => u.MiddleName);
+Option<string> name = user.AndThen(u => Option.FromNullable(u.MiddleName));
```

### What to check

Any delegate passed to those four members that can return null: a nullable reference, a `FirstOrDefault`, a dictionary lookup, an explicit `return null`. Nullable reference warnings will point at most of them if you have them switched on, because the delegate's return type is constrained to a non-nullable type.

### There is no analyzer for this one

A rule would have to prove a delegate can return null across a call boundary, which is what the nullable reference annotations already do better. Turn those on if they are off.

## Silent change 2: AndThen rejects a null monad

`AndThen` and `AndThenAsync` used to accept a null `Option` or `Result` back from your factory and hand it onward. They now throw `ArgumentNullException`, naming `optionFactory` or `resultFactory`.

```csharp
// 7.0.0: ArgumentNullException if the cache returns null.
Option<Order> order = id.AndThen(i => _cache[i]);
```

### The repair

Return the empty case rather than null:

```diff
-Option<Order> order = id.AndThen(i => _cache[i]);
+Option<Order> order = id.AndThen(i => Option.FromNullable(_cache[i]));
```

`OrElse` is not affected. A factory that produces the fallback runs only when there is nothing to carry forward, so a null there has never been ambiguous.

## Silent change 3: a scope disposed out of order restores nothing

A `MonadOptionsScope` now restores only when it is the innermost scope still open. Dispose it at any other time and nothing is restored — the library writes a `Waystone.Monads.ScopeDisposedOutOfOrder` diagnostic event instead.

In 6.x the same mistake restored the *outer* scope's predecessor and silently discarded the inner scope, so the options in effect afterwards were wrong and nothing said so.

```csharp
var outer = MonadOptions.BeginScope(o => o.UseFallbackErrorCode("Outer"));
var inner = MonadOptions.BeginScope(o => o.UseFallbackErrorCode("Inner"));

outer.Dispose(); // 6.x: silently wrong. 7.0.0: nothing restored, event written.
inner.Dispose();
```

### The repair

Use `using`. It disposes in reverse order for you, and nothing else guarantees it.

```diff
-var scope = MonadOptions.BeginScope(o => o.UseFallbackErrorCode("Debug"));
-// ...
-scope.Dispose();
+using (MonadOptions.BeginScope(o => o.UseFallbackErrorCode("Debug")))
+{
+    // ...
+}
```

### What to check

Every scope held in a field or a local rather than a `using`. Also every scope disposed from a different asynchronous flow than the one that opened it — a scope lives in the flow, so another flow's `Dispose` never sees it and now reports.

Full contract on [Configuration](/guides/configuration#what-happens-when-you-dispose-out-of-order); the event and a subscriber on [Observability](/guides/observability#watching-for-a-scope-disposed-out-of-order).

## Loud change: the implicit conversions are gone

`T` no longer converts to `Option<T>`, and `TOk` and `TErr` no longer convert to `Result<TOk, TErr>`.

```diff
-Option<int> count = 5;
+Option<int> count = Option.Some(5);
```

```diff
-Result<int, string> parsed = 5;
+Result<int, string> parsed = Result.Ok<int, string>(5);
```

**A code fix handles this.** It appears on the `CS0029` or `CS1503` error, and **Fix all occurrences in Project** applies it in a batch. Where a `Result` carries the same type on both sides it offers both `Ok` and `Err` and does not choose for you — which is the whole point.

### Why there was no warning release

This is the one removal in 7.0.0 that was not obsoleted first, and the reason is mechanical. An `[Obsolete]` implicit conversion still takes part in overload resolution. Marking it would have produced a warning and left the conversion working, so the silent wrong-branch behaviour it was removed to prevent would have carried on for another major version. There was no ordering that gave both a warning and a fix.

## Loud change: configuration moved to a builder

`MonadOptions` now publishes an immutable snapshot, and the `Use…` methods moved to a new `MonadOptionsBuilder`.

**Most call sites do not change**, because the lambda parameter's type is inferred:

```csharp
// Compiles against 6.x and against 7.0.0.
MonadOptions.Configure(options => options.UseFallbackErrorCode("Unknown"));
```

Three things do break:

* An explicitly typed lambda parameter. `(MonadOptions options) =>` becomes `(MonadOptionsBuilder options) =>`, or drop the annotation.
* A field, parameter, local or property typed `MonadOptions`. There is no replacement — configuration is reachable only inside a `Configure` or `BeginScope` callback now.
* `MonadOptionsExtensions` in the satellite packages, now `MonadOptionsBuilderExtensions`. Only a `using static` or a qualified static call is affected.

Do not keep the builder past the callback. Calls on it afterwards are discarded without an error. Full model on [Configuration](/guides/configuration#how-configuration-works).

## Loud change: the extension classes collapsed

The per-family extension classes (`AndThenExtensions`, `MapExtensions`, `IsSomeAndExtensions` and the rest) are now one class per monad: `OptionExtensions` and `ResultExtensions`.

```diff
-using static Waystone.Monads.Options.Extensions.AndThenExtensions;
-
-Option<Order> order = AndThenAsync(option, Load);
+Option<Order> order = await option.AndThenAsync(Load);
```

Called as extensions, the normal way, nothing changes. Only a `using static` or a qualified static call breaks, as `CS0234` or `CS0103`.

These could not overlap for a version either: two static classes declaring the same extension member for the same receiver is `CS0121`, and `[Obsolete]` does not remove a member from overload resolution.

{% hint style="danger" %}
**Fix the `using static` lines first.** `CS0234` is a declaration-phase error, so it stops the compiler reporting anything in the body of every file that has the `using`. Parameter renames and conversion errors in those files stay invisible until the qualifier is fixed, and your first green build is not the whole job.
{% endhint %}

## Loud change: five obsolete members are gone

All five were `[Obsolete]` in 6.x with a message naming the replacement.

| Removed                           | Replacement                                                                                         | Diagnostic |
| --------------------------------- | --------------------------------------------------------------------------------------------------- | ---------- |
| `MonadOptions.UseExceptionLogger` | `UseLogger`, `UseLoggerFactory` or `UseLoggerFactoryFrom` from `Waystone.Monads.Extensions.Logging` | `CS1061`   |
| `ErrorCode.FromEnum`              | `[ErrorCodeCatalog]` and the generated `ToErrorCode()`                                              | `CS0117`   |
| `Error.FromEnum`                  | `[ErrorCodeCatalog]` and the generated `{Enum}Catalog.Errors.{Member}(message)`                     | `CS0117`   |
| `ErrorCodeFactory.FromEnum`       | `[ErrorCodeCatalog]`. Enum codes are settled at compile time now, so a factory cannot change them.  | `CS0115`   |
| `Result.Err<TOk>(Enum, string)`   | `Result.Err(code.ToError(message))`                                                                 | `CS1501`   |

{% hint style="danger" %}
**`ErrorCodeFactory.FromEnum` masks other errors too.** An override of it is `CS0115` at the declaration, so a codebase with both an override and call sites sees only the override error, fixes it, and discovers the call sites on the next build. Build twice.
{% endhint %}

See [Generated error codes](/reference/source-generation) for the `[ErrorCodeCatalog]` route.

## Loud change: parameter names

Parameters were renamed across `Option`, `Result` and their case types, for consistency: `default` became `defaultValue`, `else` became `valueFactory`, `createDefault` became `defaultFactory`, and `createOther` became `optionFactory` or `resultFactory`.

**Positional calls are unaffected.** Argument order did not change, so this only breaks a call that names its arguments, as `CS1739`.

```diff
-option.MapOr(default: 0, map: x => x + 1);
+option.MapOr(defaultValue: 0, map: x => x + 1);
```

A code fix substitutes the new name. Do not switch to positional arguments to dodge it — the name was chosen to say what the argument is for.

## Loud change: TryAsync and CollectAsync return ValueTask

v6 made every async *extension* return `ValueTask` and left the static members alone. v7 finishes the job. `Option.TryAsync`, `Result.TryAsync` and `CollectAsync` returned `Task` up to 6.7.0 and return `ValueTask` now, so the rule has no exceptions left.

**If you await the call, nothing changes.** You await a `ValueTask` the same way. It breaks only where you name the type or hand the task to something that wants a `Task`:

```diff
-Task<Option<int>> pending = Option.TryAsync(() => FetchAsync());
+ValueTask<Option<int>> pending = Option.TryAsync(() => FetchAsync());
```

```diff
-await Task.WhenAll(Result.TryAsync(A, Fail), Result.TryAsync(B, Fail));
+await Task.WhenAll(
+    Result.TryAsync(A, Fail).AsTask(),
+    Result.TryAsync(B, Fail).AsTask());
```

You get `CS0029` on the assignment and `CS1503` on the `Task.WhenAll` call. There is no code fix. Two things to carry over from the v6 change:

* **Await it once, and only once.** This matters when you store it first, as above.
* `.AsTask()` allocates. Only reach for it where a `Task` is genuinely required.

If you were following [v5.x to v6.x](/upgrading/older/v5-to-v6#loud-change-async-extensions-all-return-valuetask), it told you to leave `TryAsync` alone. That advice held for v6 and stops holding here.

## Analyzer rules that changed

### Added

| Rule                                           | Severity   | What it reports                                                                                             |
| ---------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------- |
| [`WM2022`](/reference/analyzers/idioms#wm2022) | Suggestion | A `Task`-returning method group passed to `AndThenAsync` or `OrElseAsync`, whose step returns a `ValueTask` |

Two more ship in the new test package rather than in the library: [`WMS2001` and `WMS2002`](/reference/analyzers/assertion-rules).

### Removed

| Rule     | Why                                                                                                                                     |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `WM2010` | It reported a `Result<T, T>` whose two implicit conversions were ambiguous. There are no implicit conversions left for it to report on. |

Remove any `.editorconfig` entry or `#pragma` naming `WM2010`. A retired id is never reused, so a stale entry does nothing at all — it neither errors nor warns.

## New, and worth a look once the build is clean

* [**Waystone.Monads.Shouldly**](/reference/integrations/shouldly) — assertions that take the monad, so a failing test names the `None` or `Err` it found. `WMS2001` and `WMS2002` convert an existing suite in a batch.
* [**Waystone.Monads.Linq**](/reference/packages/linq) — C# query syntax over `Option` and `Result`.
* [**Severity presets**](/reference/analyzers/severity-presets) — one MSBuild property makes the seven misuse rules build errors. A major upgrade is a reasonable moment to turn it on.

Do these after the upgrade, not during it.

## Everything on one page

| Change                                           | Breaks the build?                  | What to do                                       |
| ------------------------------------------------ | ---------------------------------- | ------------------------------------------------ |
| A projection returning null throws               | **No**                             | Project with `AndThen` and `Option.FromNullable` |
| `AndThen` rejects a null monad                   | **No**                             | Return `None` or an `Err` rather than null       |
| A scope disposed out of order restores nothing   | **No**                             | Use `using`                                      |
| Implicit conversions removed                     | **Yes**                            | Take the code fix on `CS0029` / `CS1503`         |
| Configuration moved to a builder                 | **Yes**, only for explicit types   | Let the lambda parameter infer                   |
| Extension classes collapsed                      | **Yes**                            | Drop the `using static`; call as an extension    |
| Five obsolete members removed                    | **Yes**                            | See the table above                              |
| Parameter renames                                | **Yes**, only for named arguments  | Take the code fix on `CS1739`                    |
| `TryAsync` and `CollectAsync` return `ValueTask` | **Yes**, only if you name the type | Change the declared type, or add `.AsTask()`     |
| `WM2010` retired                                 | **No**                             | Delete any `.editorconfig` entry for it          |


# From v5.x

Skipping v6 means every v6 change and every v7 change land together. This is the order to do them in.

## This page exists because skipping a major is normal

Going from 5.x straight to 7.0.0 means the v6 changes and the v7 changes arrive in the same build. That is six silent changes rather than three, and they interact — so the order you work in matters more than it does on either single-version page.

{% hint style="danger" %}
**One v6 change had a warning you will not get.** `WM1010` was the analyzer rule that warned about `Option.Some` accepting a value-type default. It shipped in 5.5.0 and v6 retired it, so it only ever protected people who happened to be on a 5.5.x release when they upgraded. If you are on 5.4 or earlier, that warning never existed for you.

[Silent change 5](#silent-change-3-optionsome-accepts-value-type-defaults) is that change. It is the one on this page with no tooling behind it at all.
{% endhint %}

<details>

<summary>Upgrade with an agent — copy this prompt</summary>

Pointed at your solution, in Claude Code or a similar tool. It does the v6 work first and reports it separately.

```
You are upgrading a C# codebase from Waystone.Monads 5.x to 7.0.0. Every v6 change
lands at the same time as every v7 change, so do the v6 work first and report it
separately.

Work in this order. Do not skip step 1 — it is the only step the compiler cannot do for
you.

## Step 1 — find the silent behaviour changes first

These compile without error and behave differently. Search for each, decide per call
site, and report every one you changed and every one you deliberately left. Do this
before you touch anything that fails to build, because the build errors will otherwise
bury them.

1. A projection returning null now throws. On an Option, Map, MapAsync, Reduce and
   ReduceAsync used to carry a null return from your delegate into the Some. They now
   throw ArgumentNullException, naming the delegate parameter. Find delegates passed to
   those members that can return null — a nullable reference, a FirstOrDefault, a
   dictionary lookup, an explicit return null. Each is a latent exception. Fix by
   projecting into an option instead: AndThen with Option.FromNullable.

2. A factory returning a null monad now throws. AndThen and AndThenAsync used to accept
   a null Option or Result back from your factory. They now throw
   ArgumentNullException naming optionFactory or resultFactory. Return
   Option.None<T>() or an Err rather than null.

3. Disposing an options scope out of order no longer restores. If the codebase disposes
   a MonadOptionsScope by hand rather than with using, or stores one in a field, the
   out-of-order case now declines to restore and writes a
   Waystone.Monads.ScopeDisposedOutOfOrder diagnostic event instead of silently putting
   the wrong options back. Convert every one to using.

4. A cancellation is no longer caught. Try and TryAsync let an
   OperationCanceledException propagate instead of converting it to a None or an Err. If
   the codebase relied on the old behaviour, restore it with
   MonadOptions.Configure(options => options.UseCancellationAsFailure()); — but prefer
   letting cancellation propagate, and say so if you change it.

5. Option.Some accepts value-type defaults. Option.Some(0)
   returns a Some where it used to throw, and Option<int> x = 0; gave you a None
   before. Report every IsNone branch on a value-type option — you cannot decide these
   without me.

There is deliberately no item here about holding a MonadOptions instance. Options did
become an immutable snapshot in 7.0.0, but the type no longer exposes any public
instance member or accessor at all, so code that held one fails to build rather than
quietly going stale. That is compile-time work, and step 4 covers it. Do not
reintroduce it as a silent change.

## Step 2 — upgrade the package and build

Set the package version to 7.0.0. Then build and capture every
diagnostic. Do not fix anything yet. Count the diagnostics by code and report the
counts, so both of us know the size of the job.

Build twice before you trust the count. Three diagnostics in this upgrade fire at the
declaration phase and mask every body-phase error in the same project: CS0234 on a
using static of a removed extension class, CS0115 on an ErrorCodeFactory.FromEnum
override, and — only if the FluentValidation package is referenced — CS0246 on a
signature naming the removed ValidationErr type. Fix those first, then re-count.

## Step 3 — take the code fixes the package offers

Waystone.Monads ships code fixes keyed to the compiler diagnostics this upgrade
produces, not to analyzer rules — so dotnet format analyzers will not apply them. They
appear on the error itself in your IDE, and "Fix all occurrences in Project" applies
them in a batch:

- CS1739 — a renamed parameter. The fix substitutes the new name.
- CS0029 and CS1503 — a removed implicit conversion. The fix wraps the value in
  Option.Some, Result.Ok or Result.Err. Where a Result carries the same type on both
  sides it offers both and does not pick for you.
- CS0029 and CS1503 on an async chain — the fix adds .AsTask(). Prefer changing the
  declared type or awaiting the value; .AsTask() allocates.
- WM2022 — a Task-returning method group passed to AndThenAsync or OrElseAsync. The fix
  wraps it in an async lambda.

If you cannot drive an IDE, do these by hand from the diagnostic list in step 4.
Rebuild and re-count either way.

## Step 4 — work the remaining diagnostics by code

- Anything touching configuration — CS1061, CS1503, CS0029, CS0103. In 7.0.0
  MonadOptions publishes an immutable snapshot and the authoring surface moved to a new
  MonadOptionsBuilder. The Use* methods live on the builder, not on MonadOptions.
  - MonadOptions.Configure(options => options.UseFallbackErrorCode("x")); still
    compiles unchanged, because the lambda parameter's type is inferred. This is the
    dominant call shape. Leave call sites that already build alone.
  - An explicitly typed lambda parameter breaks. (MonadOptions options) => becomes
    (MonadOptionsBuilder options) =>, or drop the type annotation and let it infer.
  - A field, parameter, local or property typed MonadOptions has no replacement.
    Configuration is reachable only inside a Configure or BeginScope callback now. Move
    the Use* calls into one; do not try to pass options around.
  - In the satellite packages, MonadOptionsExtensions is renamed to
    MonadOptionsBuilderExtensions, and its extension receiver changes from MonadOptions
    to MonadOptionsBuilder. A using static or a qualified static call naming the old
    class must be updated; a reduced extension call on the callback's parameter needs no
    change.
  - Never stash the MonadOptionsBuilder the callback hands you. It is authoring state,
    not the published options — calls made on it after the callback returns are
    discarded without error.
- CS1739 — "does not have a parameter named X". A parameter was renamed. Read the new
  name from the signature and update the named argument. Do not convert the call to
  positional arguments to dodge it; the name was chosen to say what the argument is for.
- CS0029 / CS1503 — cannot convert. Either the implicit conversions to Option and Result
  were removed, so wrap the value explicitly in Option.Some, Result.Ok or Result.Err; or
  an async member now returns ValueTask<T> where it returned Task<T> — in the core
  package Option.TryAsync, Result.TryAsync and CollectAsync are the three that changed in
  7.0.0, and the FluentValidation package's ValidateAsync changed with them. For the
  ValueTask case, prefer changing the local's type or awaiting it to converting with
  .AsTask(), which allocates. A call to Task.WhenAll is the one place .AsTask() is the
  right answer.
- CS0103 / CS0234 — the name does not exist. The per-family extension classes were
  collapsed into one class per monad. A using static or a qualified static call naming
  the old class must move to OptionExtensions or ResultExtensions, or better, become a
  reduced extension call on the receiver.
- CS0117 / CS1061 / CS1501 — no such member or overload. A member obsoleted in 6.x was
  removed. The five are ErrorCode.FromEnum, Error.FromEnum, ErrorCodeFactory.FromEnum,
  MonadOptions.UseExceptionLogger, and the Result.Err<TOk>(Enum, string) overload. Read
  the obsoletion message in the 6.x package for the replacement it names.
  - UseExceptionLogger is the one worth spelling out. If its delegate only logged, replace
    it with UseLoggerFactory, UseLoggerFactoryFrom or UseLogger on the builder, from the
    Waystone.Monads.Extensions.Logging package. If it did anything else — a metric, a
    span, a bug report — that is an observer, not a logger: subscribe with
    MonadDiagnostics.ExceptionHandledEvent.Subscribe instead. Do not reach for a
    DiagnosticListener by name; the typed token exists so a wrong name cannot fail
    silently.
- CS0115 — no suitable method found to override. A virtual a consumer overrode is gone.
  ErrorCodeFactory.FromEnum is the one this hits: enum codes are settled at compile time
  now and a factory cannot change them. Delete the override and use [ErrorCodeCatalog]
  instead.
- CS0411 — type arguments cannot be inferred. An async chaining step's delegate returns
  a Task where a ValueTask is wanted. WM2022 reports the same call and says which
  parameter. Change the step to return ValueTask, or wrap it in an async lambda.
- Anything naming Waystone.Monads.FluentValidation. Check whether the solution references
  that package before reading this; if it does not, skip the whole bullet. If it does,
  the package was rewritten in 7.0.0 rather than deprecated, so nothing warned in 6.x and
  no code fix exists. Every call site needs a hand edit.
  - The namespaces shadow FluentValidation's own (CS0246, CS0234). Delete every using of
    Waystone.Monads.FluentValidation.Results.Extensions, .Results and .Configs, and use
    FluentValidation.Extensions, FluentValidation and FluentValidation.Configs. A file
    that already has using FluentValidation; needs nothing back for ValidationError. The
    package and assembly names are unchanged, so leave the PackageReference alone.
  - ValidationErr is gone (CS0246). ValidationError replaces it and derives from Error,
    so a failed validation now joins a chain without a MapErr at the seam.
  - Validate and ValidateAsync err with Error, not ValidationErr (CS0029). Change the
    declared type to Result<T, Error>, and delete the MapErr that used to convert.
  - ToError() is gone (CS1061). Delete the call — you already hold an Error.
  - ValidationErr.Create() is gone (CS0117). Only Validate and ValidateAsync build a
    ValidationError now; its constructor is internal.
  - AsValidationResult() and RuleSetsExecuted are gone (CS1061). Failures carries the
    ValidationFailure list, and ToDictionary() groups messages by property as before.
  - UseFallbackValidationErrorMessage is gone (CS1061) with no replacement. A
    ValidationError always carries at least one failure, so the empty case it covered
    cannot happen. Delete the call.
  - To read failure detail from a plain Error, pattern match: if (error is
    ValidationError validationError). Report every place you had to add that match.
  - Tell me if the resolved FluentValidation version moved. The package now allows
    >= 11.1.0 && < 13.0.0 where 6.x pinned one version, so the upgrade can pull a
    different FluentValidation than the solution was tested against. Do not pin it back
    without asking me.

## Step 5 — verify

Build clean, then run the test suite. Report: the diagnostic counts before and after,
every silent change from step 1 with the decision you made, and anything you could not
resolve.

## Step 6 — offer these, do not apply them

Once the build is clean, tell me about these optional things and let me decide. Do not
install a package or change a severity in this step.

- Waystone.Monads.Shouldly, which replaces assertions on IsSome and Unwrap in the test
  suite. Its WMS2001 and WMS2002 rules are batch-fixable.
- Waystone.Monads.Linq, which adds C# query syntax over the monads.
- The recommended severity preset, which makes the seven misuse rules build errors. A
  major upgrade is a reasonable moment to turn it on, but it is my call, not yours.
- Waystone.Monads.Extensions.Hosting, if the application is built on
  Microsoft.Extensions.Hosting. builder.AddWaystoneMonads(...) registers the
  configuration and installs it from the host's own start-up sequence, replacing the
  hand-written MonadOptions.Configure call. Say where the current Configure call lives so
  I can see what it would replace.
- Waystone.Monads.Extensions.DependencyInjection, only if the application has a container
  but is not built on the hosting abstractions. It gives services.AddWaystoneMonads(...),
  and the configuration is applied by a separate provider.UseWaystoneMonads() call. That
  second call is easy to forget; if it is missed the library writes a
  Waystone.Monads.ConfigurationNotApplied event rather than failing. Prefer the hosting
  package where both would work, because it makes that call for you.

## Rules

- Never suppress a diagnostic, add a pragma to disable one, or add a null-forgiving !
  to make an error go away. Every diagnostic here has a real fix.
- Do not change behaviour to make a test pass. If a test fails after the upgrade, that
  is either a step 1 change you missed or a real finding — report it, do not paper over
  it.
- Do not reformat, rename, or refactor anything the upgrade does not require.
```

</details>

{% hint style="warning" %}
**Two steps it cannot do for you**, both in step 1 of the prompt. Whether a null projection meant "absent" or was never supposed to happen is a domain question. So is whether an `IsNone` branch on a value-type option was standing in for "zero" — see [Silent change 3](/upgrading/older/v5-to-v6#silent-change-3-some-accepts-value-type-defaults). Expect the agent to bring both to you.
{% endhint %}

## Read this first

Six changes keep compiling and change what your code does. Three came in v6 and three in v7.

| From | Change                                                                                                                                 |
| ---- | -------------------------------------------------------------------------------------------------------------------------------------- |
| v6   | [`Try` with an async factory stops catching](#silent-change-1-try-with-an-async-factory)                                               |
| v6   | [Cancellation propagates instead of becoming a failure](#silent-change-2-cancellation-propagates)                                      |
| v6   | [`Option.Some` accepts value-type defaults](#silent-change-3-optionsome-accepts-value-type-defaults)                                   |
| v7   | [A projection returning null throws](/upgrading/v7/from-v6#silent-change-1-a-null-projection-throws)                                   |
| v7   | [`AndThen` rejects a null monad](/upgrading/v7/from-v6#silent-change-2-andthen-rejects-a-null-monad)                                   |
| v7   | [A scope disposed out of order restores nothing](/upgrading/v7/from-v6#silent-change-3-a-scope-disposed-out-of-order-restores-nothing) |

Everything else breaks the build.

## Do it in this order

Work through the v6 page and then the v7 page, rather than reading both at once. Two reasons, both practical.

**The v6 async changes come first because the v7 changes sit on top of them.** v6 made every async extension return `ValueTask` and moved async factories to `TryAsync`. The v7 `WM2022` rule and the null-projection guard both describe chains you will have rewritten by then, so doing v6 first means you touch each chain once.

**The loud v7 changes will hide the loud v6 ones.** `CS0234` on a removed extension class is a declaration-phase error, so it stops the compiler reporting anything in the body of every file that has the `using`. Land v6's loud changes, get a green build, then upgrade to 7.0.0.

1. **Read** [**v5.x to v6.x**](/upgrading/older/v5-to-v6) **in full and do its silent changes.** These are the ones with no compiler help, and they are the reason this page exists.
2. **Upgrade to 6.x and get a clean build.** Do not skip this. A single intermediate build separates two sets of diagnostics that are hard to tell apart in one pile.
3. **Read** [**v6.x to v7.0.0**](/upgrading/v7/from-v6) **and do its three silent changes.**
4. **Upgrade to `7.0.0` and work the diagnostics.** Build twice — see [Diagnostics that mask other diagnostics](/upgrading/v7/breaking-changes#diagnostics-that-mask-other-diagnostics).

If you cannot ship an intermediate 6.x build, the agent prompt handles both sets in one pass and reports them separately. It is a worse position to be in, not an equal one.

## The three v6 silent changes, in brief

Full detail on [v5.x to v6.x](/upgrading/older/v5-to-v6). This is enough to know whether they apply to you.

### Silent change 1: Try with an async factory

`Option.Try` and `Result.Try` given an `async` factory return before the factory has finished, so nothing is caught. Use `TryAsync`.

`WM1011` reports every one of these, as a **warning**, so this is the one v6 silent change your build will actually mention. Do not suppress it.

See [Silent change 1](/upgrading/older/v5-to-v6#silent-change-1-try-with-an-async-factory).

### Silent change 2: cancellation propagates

From 6.0.0 an `OperationCanceledException` is no longer converted into a `None` or an `Err`. It propagates to your caller.

Prefer that. If you genuinely relied on the old behaviour, opt back in:

```csharp
MonadOptions.Configure(options => options.UseCancellationAsFailure());
```

Note the modern spelling — in 7.0.0 the `Use…` methods are on a builder, and the inferred lambda parameter above is what makes this line identical in both versions.

See [Silent change 2](/upgrading/older/v5-to-v6#silent-change-2-cancellation-propagates).

### Silent change 3: Option.Some accepts value-type defaults

`Option.Some(0)` returns a `Some` where 5.x threw, and `Option<int> x = 0;` gave you a `None` in 5.x and a `Some(0)` in 6.x.

This is the change with no tooling behind it for anyone below 5.5.0, and no tooling at all from 6.0.0 onward. Every `IsNone` branch on a value-type option has to be read by someone who knows whether it was standing in for zero.

{% hint style="info" %}
**In 7.0.0 the implicit conversion is gone entirely**, so `Option<int> x = 0;` no longer compiles at all. That turns this particular assignment from a silent change into a build error on the v7 step — which is the one piece of luck on this path. It does not help with `Option.Some(0)` written out in full.
{% endhint %}

See [Silent change 3](/upgrading/older/v5-to-v6#silent-change-3-some-accepts-value-type-defaults) on the v6 page.

## The loud changes from both versions

Rather than repeat two tables, here is where each list lives:

* **v6's loud changes** — `ValueTask` everywhere, `FlatMap` removed, deriving from `Option` or `Result` no longer allowed. On [v5.x to v6.x](/upgrading/older/v5-to-v6).
* **v7's loud changes** — implicit conversions removed, configuration moved to a builder, extension classes collapsed, five obsolete members gone, parameter renames, and `TryAsync` and `CollectAsync` returning `ValueTask`. On [v6.x to v7.0.0](/upgrading/v7/from-v6), and as a reference table with diagnostics on [Every v7 break](/upgrading/v7/breaking-changes).

Two of v6's loud changes are worth flagging here because they multiply on this path:

**`.AsTask()` sites come from both versions.** v6 moved the async extensions to `ValueTask`; v7 moved `TryAsync` and `CollectAsync` too. Coming from 5.x you cannot tell the two apart from the diagnostic, and you do not need to — treat every `CS0029` between `Task` and `ValueTask` the same way. Prefer changing the declared type or awaiting the value; `.AsTask()` allocates.

**`FlatMap` and the removed extension classes overlap.** A call written as `FlatMapExtensions.FlatMap(...)` breaks twice — once because the method was renamed to `AndThen` in v6, and once because the class is gone in v7. Rename first, then drop the qualifier.

## Analyzer rules across both versions

Delete `.editorconfig` entries and `#pragma` directives for every retired id. None of them is reused, so a stale entry neither errors nor warns — it simply does nothing, which is worse, because it reads as though something is configured.

| Retired in | Ids                                              |
| ---------- | ------------------------------------------------ |
| v6.0.0     | `WM1004`, `WM1007`, `WM1009`, `WM1010`, `WM2014` |
| v7.0.0     | `WM2010`                                         |

Added across the two versions: `WM1011`, `WM2015`, `WM2016`, `WM2017` and more in v6 — see [v5.x to v6.x](/upgrading/older/v5-to-v6#analyzer-rules-that-changed) — and `WM2022` in v7.

## Everything on one page

| Change                                           | From | Breaks the build?                 | What to do                                   |
| ------------------------------------------------ | ---- | --------------------------------- | -------------------------------------------- |
| `Try` with an async factory                      | v6   | **No**, but `WM1011` warns        | Use `TryAsync`                               |
| Cancellation propagates                          | v6   | **No**                            | Handle it, or `UseCancellationAsFailure`     |
| `Option.Some` accepts value-type defaults        | v6   | **No**                            | Read every `IsNone` branch on a value type   |
| Async extensions return `ValueTask`              | v6   | **Yes**                           | Change the declared type or await it         |
| `FlatMap` removed                                | v6   | **Yes**                           | Rename to `AndThen`                          |
| Deriving from `Option` or `Result`               | v6   | **Yes**                           | Compose rather than derive                   |
| A projection returning null throws               | v7   | **No**                            | `AndThen` with `Option.FromNullable`         |
| `AndThen` rejects a null monad                   | v7   | **No**                            | Return `None` or an `Err`                    |
| A scope disposed out of order                    | v7   | **No**                            | Use `using`                                  |
| Implicit conversions removed                     | v7   | **Yes**                           | Take the code fix on `CS0029` / `CS1503`     |
| Configuration moved to a builder                 | v7   | **Yes**, for explicit types only  | Let the lambda parameter infer               |
| Extension classes collapsed                      | v7   | **Yes**                           | Drop the `using static`                      |
| Five obsolete members removed                    | v7   | **Yes**                           | See the v7 page                              |
| Parameter renames                                | v7   | **Yes**, for named arguments only | Take the code fix on `CS1739`                |
| `TryAsync` and `CollectAsync` return `ValueTask` | v7   | **Yes**, if you name the type     | Change the declared type, or add `.AsTask()` |


# Deprecations

API that has been removed, the version that removed it, and what to use instead.

## What this page is for

We deprecate before we delete. Anything on its way out is marked `[Obsolete]` first, so your build reports a `CS0618` warning naming the replacement and the version that removes it. We only delete in a major release.

Everything under *Pending removal* is marked now and goes in the release named. Everything under *Removed in* has already gone.

{% hint style="info" %}
Packages in the Waystone family share one version number, so a v6.0.0 of `Waystone.Monads` means a v6.0.0 of every package.
{% endhint %}

**Nothing is pending removal today.** `7.0.0` carries no `[Obsolete]` member at all — everything that was marked in 6.x has now gone, and nothing new has been marked for 8.0.0. The next thing to appear on this page will be marked in a 7.x minor.

## Removed in 7.0.0

### Seeing handled exceptions through a hand-written delegate

**Deprecated in:** 6.7.0 · **Removed in:** 7.0.0 · **Replacement:** `Waystone.Monads.Extensions.Logging`

| Removed                                                          | Replacement                                                                                                                                                 |
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MonadOptions.UseExceptionLogger(Action<Exception, CallerInfo>)` | `UseLoggerFactoryFrom(IServiceProvider)`, `UseLoggerFactory(ILoggerFactory)` or `UseLogger(ILogger)`, called on the builder inside `MonadOptions.Configure` |

#### How to migrate

Install the package and hand the library your logger. Delete the old call in the same change.

```
dotnet add package Waystone.Monads.Extensions.Logging
```

```diff
-MonadOptions.Configure(options => options.UseExceptionLogger((ex, caller) =>
-{
-    Log.Error(ex, "Monad caught {Member}:{Line}", caller.MemberName, caller.LineNumber);
-}));
+MonadOptions.Configure(options => options.UseLoggerFactoryFrom(app.Services));
```

Without a service provider, build a factory and pass it directly:

```diff
-MonadOptions.Configure(options => options.UseExceptionLogger(MyLogger.Handle));
+MonadOptions.Configure(options => options.UseLoggerFactory(loggerFactory));
```

You lose nothing in the move. Every entry still carries the exception and the same call-site details (`MemberName`, `ArgumentExpression` and `LineNumber`), and you gain level and category filtering from `appsettings.json`, which an opaque delegate could never give you. See [Observability](/guides/observability).

{% hint style="danger" %}
**If you are still on 6.x, delete the old call as you add the package.** Both fire for the whole of 6.x, so leaving `UseExceptionLogger` in place logs every handled exception twice. On 7.0.0 the member is gone, so the double-logging window is closed — you get a `CS1061` instead.
{% endhint %}

{% hint style="info" %}
**You may not need to log at all.** If you only wanted counts, the library publishes a `Waystone.Monads` meter and needs no package and no configuration — see [Observability](/guides/observability).
{% endhint %}

#### Why this changed

`UseExceptionLogger` holds exactly one delegate. Configure a second observer and it replaces the first, silently — so the library could never support more than one integration watching these exceptions at a time.

The library now writes each handled exception to a `DiagnosticListener`, which any number of subscribers can share, and publishes a counter on a meter named after itself. The package is one subscriber. Your own tooling can be another, and neither has to know the other exists.

You also had to write the delegate yourself, for whichever logging library you use. Handing us an `ILogger` is less code and behaves the way every other .NET library does.

### Working an error code out from an enum at run time

**Deprecated in:** 6.2.0 · **Removed in:** 7.0.0 · **Replacement:** the members generated by `[ErrorCodeCatalog]`

| Removed                           | Replacement                                                               |
| --------------------------------- | ------------------------------------------------------------------------- |
| `ErrorCode.FromEnum(Enum)`        | `MyEnumCatalog.Codes.Member`, or `value.ToErrorCode()`                    |
| `ErrorCodeFactory.FromEnum(Enum)` | `[ErrorCodeCatalog(Format = "…")]`, or `[assembly: ErrorCodeFormat("…")]` |
| `Error.FromEnum(Enum, string)`    | `MyEnumCatalog.Errors.Member(message)`, or `value.ToError(message)`       |
| `Result.Err<TOk>(Enum, string)`   | `Result.Err<TOk>(MyEnumCatalog.Errors.Member(message))`                   |

{% hint style="info" %}
`Error.FromEnum` and `Result.Err<TOk>(Enum, string)` were deprecated in 6.3.0, a release after the other two. They call the same reflection path and disappear at the same time, but they carried no warning until now.
{% endhint %}

#### How to migrate

Mark the enum with `[ErrorCodeCatalog]` and use what it generates.

{% hint style="warning" %}
**Do this before you upgrade, not after.** In 6.x the analyzer offered a lightbulb fix for each of these on the deprecation warning itself. In 7.0.0 the members are gone, so there is no warning to attach a fix to and the fix provider was removed with them. On 7.0.0 you get a `CS0117` or `CS1061` and the rewrite is yours to do.
{% endhint %}

```diff
-ErrorCode code = ErrorCode.FromEnum(OrderError.NotFound);
+ErrorCode code = OrderErrorCatalog.Codes.NotFound;
```

```diff
-Error error = Error.FromEnum(OrderError.NotFound, "no order with that id");
+Error error = OrderErrorCatalog.Errors.NotFound("no order with that id");
```

```diff
-Result<int, Error> result = Result.Err<int>(OrderError.NotFound, "gone");
+Result<int, Error> result = Result.Err<int>(OrderErrorCatalog.Errors.NotFound("gone"));
```

When the member is not known where you are standing, the generated extensions take their place, and the fix produces these instead:

```diff
-ErrorCode code = ErrorCode.FromEnum(error);
+ErrorCode code = error.ToErrorCode();
```

```diff
-Error error = Error.FromEnum(value, "gone");
+Error error = value.ToError("gone");
```

If you shaped your codes by subclassing `ErrorCodeFactory` and overriding `FromEnum`, say the same thing with a `Format` — see [Source generation](/reference/source-generation). `FromException` is not deprecated, and a factory that only overrides it needs no change.

#### Why this changed

Both of these work the code out by reflection at run time, which costs you three things a generated constant gives you: the compiler cannot see the code, so a renamed member changes your wire contract silently; the declared `Format` cannot be applied, because it is read at compile time; and the analyzers and the [error code registry](/reference/source-generation/reviewing-codes#reviewing-your-codes-as-a-list) cannot review a string nothing in the build can see.

### Removed in 7.0.0 with no deprecation window

Three things went in 7.0.0 without being marked `[Obsolete]` first. That is not how this page normally works, so each one says why no warning release was possible.

#### The implicit conversions to Option and Result

| Removed                                           | Replacement                                           |
| ------------------------------------------------- | ----------------------------------------------------- |
| `implicit operator Option<T>(T value)`            | `Option.Some(value)`, or `Option.FromNullable(value)` |
| `implicit operator Result<TOk, TErr>(TOk value)`  | `Result.Ok<TOk, TErr>(value)`                         |
| `implicit operator Result<TOk, TErr>(TErr value)` | `Result.Err<TOk, TErr>(value)`                        |

**Why no warning release.** An `[Obsolete]` implicit conversion still takes part in overload resolution. Marking these would have produced a `CS0618` and left the conversions working — so the silent wrong-branch behaviour they were removed to prevent would have carried on through the whole of 7.x. There was no ordering that gave both a warning and a fix.

There is a code fix, on the `CS0029` or `CS1503` you get instead. Where a `Result` carries the same type on both sides it offers both `Ok` and `Err` and does not choose for you.

#### The per-family extension classes

| Removed                                                                                             | Replacement        |
| --------------------------------------------------------------------------------------------------- | ------------------ |
| `AndThenExtensions`, `MapExtensions`, `IsSomeAndExtensions` and the rest under `Options.Extensions` | `OptionExtensions` |
| The same set under `Results.Extensions`                                                             | `ResultExtensions` |

Called as extensions, nothing changes. Only a `using static` or a qualified static call breaks, as `CS0234` or `CS0103`.

**Why no warning release.** Two static classes declaring the same extension member for the same receiver is `CS0121`, so the old and new spellings could not coexist for a version — and `[Obsolete]` does not remove a member from overload resolution.

#### The MonadOptionsScope disposal contract

Nothing was removed here; a documented behaviour changed, which is why the release carries a `!`.

A scope now restores only when it is the innermost one still open. Disposing it at any other point restores nothing and writes a `Waystone.Monads.ScopeDisposedOutOfOrder` diagnostic event. In 6.x the same mistake restored the wrong options silently.

**Why no warning release.** There is nothing to mark. The old behaviour was not a member, and a rule cannot see the order in which disposals will happen at run time. The event exists because that is the only place the mistake is visible.

Full contract on [Configuration](/guides/configuration#what-happens-when-you-dispose-out-of-order).

### Rule ids that are gaps

A retired analyzer rule id is never reused. So a stale `.editorconfig` entry or `#pragma` naming one does nothing at all — it does not error, it does not warn, and it reads as though something is configured when nothing is. Delete them.

| Retired in | Ids                                              |
| ---------- | ------------------------------------------------ |
| 6.0.0      | `WM1004`, `WM1007`, `WM1009`, `WM1010`, `WM2014` |
| 7.0.0      | `WM2010`                                         |

`WM2014` is the one worth knowing about, because it was the migration aid for [`Option.FlatMap`](#optionflatmap) and it went in the same release as the method it pointed at. `WM2010` went because there are no implicit conversions left for it to report on.

## Removed in 6.0.0

### Option.FlatMap

**Deprecated in:** 5.4.0 · **Removed in:** 6.0.0 · **Replacement:** `AndThen`

| Removed                                                                     | Replacement                                      |
| --------------------------------------------------------------------------- | ------------------------------------------------ |
| `Option<T>.FlatMap<TOut>(Func<T, Option<TOut>>)`                            | `Option<T>.AndThen<TOut>(Func<T, Option<TOut>>)` |
| `FlatMapAsync` on `Option<T>`, `Task<Option<T>>` and `ValueTask<Option<T>>` | `AndThenAsync`                                   |

#### How to migrate

Rename the call. The parameters, the behaviour and the return type do not change.

```diff
-Option<int> option = Find(id).FlatMap(Parse);
+Option<int> option = Find(id).AndThen(Parse);
```

Upgrade to 5.5.0 first if you have not already. `WM2014` reports every call site and its quick fix does the rename. That rule is removed in 6.0.0 along with the method, so it cannot help you after the upgrade.

#### Why this changed

`Result<TOk, TErr>` already spelled this operation `AndThen`, so one library had two names for one idea. Rust, which both types follow, calls it `and_then`. `AndThen` is the name that agrees with the rest of the library.

### Try overloads that accept an async factory

**Deprecated in:** 5.2.0 · **Removed in:** 6.0.0 · **Replacement:** `TryAsync`

| Removed                                                            | Replacement                                                             |
| ------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| `Option.Try<T>(Func<Task<T>>, …)`                                  | `Option.TryAsync<T>(Func<Task<T>>, …)`                                  |
| `Result.Try<TOk, TErr>(Func<Task<TOk>>, Func<Exception, TErr>, …)` | `Result.TryAsync<TOk, TErr>(Func<Task<TOk>>, Func<Exception, TErr>, …)` |

{% hint style="danger" %}
**Removing these does not break your build, and that is the problem.** Your call site rebinds to the synchronous overload, keeps compiling, and stops catching exceptions. Read [Silent change 1](/upgrading/older/v5-to-v6#silent-change-1-try-with-an-async-factory) before you upgrade, and turn on [`WM1011`](/reference/analyzers/runtime-bugs#wm1011) — it finds every affected call.
{% endhint %}

#### How to migrate

Rename the call and keep the `await` where it already was.

```diff
-Option<int> option = await Option.Try(() => FetchAsync());
+Option<int> option = await Option.TryAsync(() => FetchAsync());

-Result<int, string> result = await Result.Try(() => FetchAsync(), ex => ex.Message);
+Result<int, string> result = await Result.TryAsync(() => FetchAsync(), ex => ex.Message);
```

#### Why this changed

A lambda whose body is a `throw` expression converts to both `Func<T>` and `Func<Task<T>>`. When both overloads are called `Try`, the compiler cannot choose between them, and you had to declare the delegate type to break the tie:

```csharp
// ambiguous, will not compile
Result<int, Error> result = Result.Try<int>(() => throw new InvalidOperationException());

// what you had to write instead
Result<int, Error> result = Result.Try<int>(new Func<int>(() => throw new InvalidOperationException()));
```

Giving the async overloads their own name removes the ambiguity, so the call site above compiles as written.

#### What is not affected

The synchronous overloads keep the `Try` name:

* `Option.Try<T>(Func<T>, …)`
* `Result.Try<TOk, TErr>(Func<TOk>, Func<Exception, TErr>, …)`
* `Result.Try<TOk>(Func<TOk>, …)`

{% hint style="info" %}
`Result.TryAsync<TOk>(Func<Task<TOk>>, …)` , the overload that defaults the error type to `Error`, was introduced as `TryAsync` and never had a `Try` spelling. There was nothing to migrate.
{% endhint %}

## Not a deprecation, but it removed API

v6 also closed the `Option<T>` and `Result<TOk, TErr>` hierarchies, which removes the ability to derive from them. That was never marked `[Obsolete]`, because there is no way to obsolete "inheriting from this type". See [v5.x to v6.x](/upgrading/older/v5-to-v6#loud-change-you-can-no-longer-derive-from-option-or-result).

See [Async](/guides/async) for the full async surface, and [Upgrading](/upgrading/upgrading) for the upgrades that have already shipped.

For 7.0.0 specifically, [Every v7 break](/upgrading/v7/breaking-changes) lists all of the above plus the changes that were never public API in the first place, each with the compiler diagnostic it produces.


# Older upgrades

Every major version hop before v7, newest first.

One page per major version hop, newest first. If you are walking several hops, read up this list from where you are.

| Upgrade                                   | What you have to change                                                 |
| ----------------------------------------- | ----------------------------------------------------------------------- |
| [v5.x to v6.x](/upgrading/older/v5-to-v6) | Three changes that keep compiling and change what your code does        |
| [v4.x to v5.x](/upgrading/older/v4-to-v5) | Some async extensions return `ValueTask` instead of `Task`              |
| [v3.x to v4.x](/upgrading/older/v3-to-v4) | `MonadsGlobalConfig` becomes `MonadOptions`                             |
| [v2.x to v3.x](/upgrading/older/v2-to-v3) | Async overloads gain an `Async` suffix and move to extension namespaces |
| [v1.x to v2.x](/upgrading/older/v1-to-v2) | `Bind` becomes `Try`, and error logging moves to global config          |

Each page opens with a collapsed agent prompt for that hop.

Every page here describes an upgrade that landed on a version older than 7.0.0, so the replacement code it names may itself have been removed since. [Deprecations](/upgrading/deprecations) is the current list.

Going to 7.0.0 from 5.x? Do not read `v5.x to v6.x` and then the v7 page. Take [From v5.x](/upgrading/v7/from-v5), which covers both at once and gives the order.


# v5.x to v6.x

Three changes in v6 keep compiling and change what your code does. Read this page before you upgrade, and start with the two silent ones.

<details>

<summary>Upgrade with an agent — copy this prompt</summary>

Pointed at your solution, in Claude Code or a similar tool. It covers every mechanical part of this upgrade.

```
Upgrade this solution from Waystone.Monads v5 to v6. Work through these steps in
order and report what you changed at each one.

1. Find every call to `Option.Try(`, `Option.TryAsync(`, `Result.Try(` and
   `Result.TryAsync(`. For each one, decide whether the factory is asynchronous.
   If it is, and the call uses `Try` rather than `TryAsync`, change it to
   `TryAsync` and add an `await` at the point the caller already awaits. Do not
   rely on the compiler to find these — most of them still compile after the
   upgrade and silently stop catching exceptions.

2. Find every `Try` and `TryAsync` call whose factory can be cancelled — it takes
   a CancellationToken, or calls something that does. In v6 an
   OperationCanceledException propagates instead of becoming a None or an Err.
   Add a `catch (OperationCanceledException)` where the caller needs to handle
   cancellation, or tell me if you think we should opt back into the old
   behaviour with `MonadOptions.UseCancellationAsFailure()`.

3. Rename every `FlatMap` to `AndThen` and every `FlatMapAsync` to
   `AndThenAsync`. Parameters and behaviour are unchanged.

4. Find every call that hands an async delegate to a synchronous Waystone
   method — `option.Map(x => FetchAsync(x))` and the like — and switch it to the
   Async sibling with an `await`. You can spot these by the return type: an
   `Option<Task<T>>` or a `Result<Task<T>, E>` is always wrong. A `Task<T>`
   returned by `Match` or `MapOr` is fine, because the caller can await it.

5. Build. For every CS0029 or CS1503 involving `Task` and `ValueTask` on a
   Waystone async extension, add `.AsTask()` to the call. If the site simply
   awaits the result, remove the annotation instead of adding `.AsTask()`.

6. Find every `Task.WhenAll` over Waystone async extensions and add `.AsTask()`
   to each argument.

7. Delete every `catch (InvalidOperationException)` that wraps an `Option.Some`
   call. It is dead code in v6. If the code needs to handle a null, catch
   `ArgumentNullException` instead.

8. Find any type that derives from `Option<T>` or `Result<TOk, TErr>`. These no
   longer compile and cannot be fixed by changing the derived type. Report them
   to me with a suggestion for composing the monad instead.

9. Remove any `.editorconfig` entries for WM1004, WM1007, WM1009, WM1010 and
   WM2014. Those rules no longer exist.

10. Build again and report anything left over. Do not suppress a WM1011 warning
    — bring it to me instead.
```

</details>

{% hint style="warning" %}
**One step it cannot do for you.** `Option.Some(0)` returns a `Some` in v6 where it used to throw, and `Option<int> x = 0;` gives you `Some(0)` where it used to give you `None`. Deciding whether an `IsNone` branch was standing in for "zero" needs someone who knows what the code means. See [Silent change 3](#silent-change-3-some-accepts-value-type-defaults).
{% endhint %}

## Read this first

v6 has three changes that do **not** break your build:

1. **`Option.Try(() => SomethingAsync())` stops catching exceptions.** It still compiles. It now returns `Option<Task<T>>`.
2. **A cancelled operation inside `Try` now propagates** instead of becoming `None` or an `Err`.
3. **`Option.Some(0)` returns a `Some`** where it used to throw, and `Option<int> x = 0;` gives you `Some(0)` where it used to give you `None`.

Everything else in v6 breaks loudly. The compiler will find it for you.

Work through the three sections below in order. The agent prompt above covers everything mechanical.

## Silent change 1: Try with an async factory

**This is the most dangerous change in the release.** It compiles, it runs, and it silently stops handling exceptions.

v5 deprecated `Option.Try(Func<Task<T>>)` and `Result.Try(Func<Task<TOk>>, Func<Exception, TErr>)`. v6 deletes them.

Deleting them does not leave you with a compiler error, because the synchronous overload can take their place. `Option.Try<T>(Func<T>)` constrains `T` to `notnull`, and a `Task<int>` is not null:

```csharp
// v5: Option<int>, exceptions caught, awaited inside Try
// v6: Option<Task<int>>, exceptions NOT caught, task never awaited
var result = Option.Try(() => FetchCountAsync());
```

`T` binds to `Task<int>`. `Try` calls the factory, gets a task back, wraps it in a `Some`, and returns. It never awaits, so:

* **Your exception handling is gone.** A throw inside `FetchCountAsync` escapes to your caller. It does not become a `None` or an `Err`, and your [configured exception logger](/guides/configuration) never sees it.
* **The task may never be awaited**, depending on what you do with the result.

You only get a compiler error if the call site assigns to an explicitly typed local:

```csharp
// CS0029 — this one is safe, the compiler catches it
Task<Option<int>> safe = Option.Try(() => FetchCountAsync());
```

### The repair

Call `TryAsync` and `await` where you were already awaiting:

```diff
-var result = Option.Try(() => FetchCountAsync());
+var result = await Option.TryAsync(() => FetchCountAsync());

-var result = Result.Try(() => FetchCountAsync(), ex => ex.Message);
+var result = await Result.TryAsync(() => FetchCountAsync(), ex => ex.Message);
```

### The analyzer finds these for you

[`WM1011`](/reference/analyzers/runtime-bugs#wm1011) is a **warning**, not a suggestion, because it fires on code that runs. It reports any call that traps a task inside an `Option` or a `Result` — `Try` with an async factory, but also `option.Map(x => FetchAsync(x))` and anything else with an `Async` sibling it should have used.

It ships with no quick fix, deliberately. Renaming to the `Async` sibling leaves you with an unawaited task, and no fix can decide where your `await` belongs.

{% hint style="info" %}
`WM1011` also catches this mistake in code that predates v6, where someone passed an async factory to the synchronous overload by accident. That is a real bug being found, not an upgrade artefact.
{% endhint %}

## Silent change 2: cancellation propagates

In v5, `Try` and `TryAsync` caught **every** exception, including `OperationCanceledException`. A cancelled operation came back as a `None` or an `Err`, indistinguishable from a genuine failure.

In v6 they let cancellation through:

```csharp
using var cts = new CancellationTokenSource();
cts.Cancel();

// v5: None<int>, and your exception logger records the cancellation
// v6: throws OperationCanceledException
Option<int> option = await Option.TryAsync(() => FetchAsync(cts.Token));
```

`TaskCanceledException` derives from `OperationCanceledException`, so it propagates too.

### Why we changed it

Cancellation is not a failure. It is you telling the operation to stop. Swallowing it turns a deliberate shutdown into what looks like a bad result, and the caller that requested the cancellation then has to guess whether the `None` it received means "cancelled" or "genuinely absent".

### What to check

Look for code that relied on a cancellation becoming a `None` or an `Err`. It now needs a `catch`:

```diff
-Option<int> option = await Option.TryAsync(() => FetchAsync(token));
-if (option.IsNone) { /* cancelled or failed, cannot tell which */ }
+try
+{
+    Option<int> option = await Option.TryAsync(() => FetchAsync(token));
+}
+catch (OperationCanceledException)
+{
+    // handle the cancellation
+}
```

### If you want the old behaviour

Opt back in once, at startup:

```csharp
MonadOptions.Configure(options => options.UseCancellationAsFailure());
```

That restores the v5 behaviour everywhere: a cancellation is caught, logged, and becomes a `None` or an `Err` again. You can also scope it to one region with `MonadOptions.BeginScope`. See [Configuration](/guides/configuration).

We recommend leaving it off. The opt-in exists so that upgrading is not blocked on rewriting every call site at once.

## Silent change 3: Some accepts value-type defaults

In v5, a `Some` cannot hold the default of its type. `Option.Some(0)` throws, and `Option<int> x = 0;` gives you `None`.

In v6, only `null` is rejected. `Option.Some(0)` gives you a `Some` holding `0`, and so does `Option<int> x = 0;`.

We made this change because `Option<int>` could not represent part of its own domain. Zero is an ordinary integer. `Option<bool>` was close to useless, because `false` is an ordinary bool.

### The expressions that flip

Nothing fails to compile. No signature changes. Your build stays green and these expressions start returning a different value:

| Expression                                               | v5     | v6              |
| -------------------------------------------------------- | ------ | --------------- |
| `Option.Some(0)`                                         | throws | `Some(0)`       |
| `Option<int> x = someInt;` when `someInt` is `0`         | `None` | `Some(0)`       |
| `Option.FromNullable(nullableInt)` when the value is `0` | `None` | `Some(0)`       |
| `Option.Try(() => ComputeCount())` when the count is `0` | `None` | `Some(0)`       |
| `option.Map(x => x - x)`                                 | `None` | `Some(0)`       |
| `option.Reduce(...)` producing a default                 | `None` | `Some(default)` |

The same applies to `false`, `'\0'`, `Guid.Empty`, `DateTime.MinValue`, `DateTimeOffset.MinValue`, `TimeSpan.Zero`, `IntPtr.Zero` and any enum's zero member.

### What to do

1. **Search for `Option<` over a value type.** Every one is a candidate.
2. **Check each `IsNone` branch on those options.** Ask whether it treats a zero as "no result". If it does, that branch stops running in v6.
3. **Delete any `catch (InvalidOperationException)` around `Option.Some`.** It is dead code now.

There is no automatic migration, and the analyzer cannot do this for you. A rule that fired on every `Option<T>` where `T` is a value type would fire on most of the library's users.

{% hint style="warning" %}
`WM1010` shipped in 5.5.0 to warn you about exactly this, and v6 removes it — the change it forecast has happened. Upgrade to 5.5.0 **first**, fix what `WM1010` reports, then move to v6. It only reaches the call sites where it can prove the value is a default, so it covers none of the six expressions in the table above.
{% endhint %}

### Null still throws, with a different exception

`Option.Some(null!)` throws in v6, as it did in v5. The exception type changes from `InvalidOperationException` to `ArgumentNullException`.

Use [`Option.FromNullable`](/reference/option/creation#optionfromnullable) when the value may be null.

`FromNullable<T>(T?) where T : struct` no longer rejects the default either, so it now behaves the same way as its reference-type sibling.

### UnwrapOrDefault gets harder to read

This follows directly from the relaxation. Both of these return `0`:

```csharp
Option.None<int>().UnwrapOrDefault();  // 0, because there is no value
Option.Some(0).UnwrapOrDefault();      // 0, because the value is 0
```

You cannot tell them apart. This is not new in kind, since `Result` has always been in this position, but it now applies to every `Option` over a value type.

Use `UnwrapOrNull` and `MapOrNull`, shipped in 5.4.0, when you need to distinguish:

```csharp
int? absent = Option.None<int>().UnwrapOrNull();  // null
int? present = Option.Some(0).UnwrapOrNull();     // 0
```

[`WM2015`](/reference/analyzers/idioms#wm2015) points you at them.

## Loud change: async extensions all return ValueTask

Every async extension on `Option` and `Result` now returns `ValueTask` or `ValueTask<T>`. 185 signatures changed. In v5 some returned `Task` and some returned `ValueTask`; now the rule is uniform.

You get `CS0029` or `CS1503` at every affected call site. Nothing changes silently.

`Option.TryAsync` and `Result.TryAsync` are the exception in v6. They are static factories rather than extensions, so they returned `Task` in v5 and still return `Task` in v6. Leave those call sites alone — adding `.AsTask()` there will not compile.

{% hint style="info" %}
**7.0.0 closes that exception.** `TryAsync` and `CollectAsync` return `ValueTask` there, so `.AsTask()` does compile. If you are going straight to v7, see [v6.x to v7.x](/upgrading/v7/from-v6#loud-change-tryasync-and-collectasync-return-valuetask).
{% endhint %}

### The repair

Add `.AsTask()` where you need a `Task`:

```diff
-Task<Option<int>> task = option.MapAsync(FetchAsync);
+Task<Option<int>> task = option.MapAsync(FetchAsync).AsTask();
```

Your IDE offers this fix automatically. It registers against the compiler's own `CS0029` and `CS1503`, so it appears wherever the error appears.

If you simply `await` the result, nothing changes at all.

### Where this costs you

`Task.WhenAll` needs `Task` arguments, so fan-out code now has to call `.AsTask()` on each one — which allocates the exact `Task` the change avoids:

```csharp
await Task.WhenAll(
    a.MapAsync(FetchAsync).AsTask(),
    b.MapAsync(FetchAsync).AsTask());
```

This is the one place the change makes your code worse.

### The measured trade-off

`ValueTask` is not unconditionally cheaper. For a three-link chain:

| Receiver                     | Change           |
| ---------------------------- | ---------------- |
| Synchronous `Option`, `Some` | −144 B (−33%)    |
| Synchronous `Option`, `None` | −144 B (−67%)    |
| Already-completed `Task`     | −216 B           |
| **Genuinely pending task**   | **+84 B (+10%)** |

A fluent chain is built eagerly, so a pending head makes every link pending — there is no partial case. `AsyncValueTaskMethodBuilder` holds the state machine inline in its box, which makes it larger than the `Task` it replaces.

Full numbers are in `bench/Waystone.Monads.Benchmarks/README.md` in the source repository.

## Loud change: FlatMap is gone

`Option<T>.FlatMap` and the five `FlatMapAsync` extensions are deleted. Rename to `AndThen` and `AndThenAsync`:

```diff
-Option<int> option = Find(id).FlatMap(Parse);
+Option<int> option = Find(id).AndThen(Parse);
```

The parameters, the behaviour and the return type are unchanged.

`WM2014`, the rule that reported every `FlatMap` call, is removed in v6 — with no `FlatMap` left it would report nothing forever. Upgrade to 5.5.0 first and let it build your to-do list.

## Loud change: you can no longer derive from Option or Result

`Option<T>` and `Result<TOk, TErr>` are closed. `Some`, `None`, `Ok` and `Err` are the only cases, and an outside type that tries to add a third gets `CS0534` on an internal member it cannot see or override.

**There is no way around this and no migration path.** If you added a case, you have to compose the monad instead of inheriting it — hold an `Option<T>` in your type rather than being one.

`WM1007`, the v5 warning that told you not to derive, is gone in v6 because the compiler now says it instead.

Six members moved from the base type to the cases and are now `abstract`:

* `Option.And`, `Option.MapOrDefault`, `Option.Reduce`, `Option.AsEnumerable`
* `Result.MapOrDefault`, `Result.AsEnumerable`

Behaviour is identical. This only affects anyone who **overrode** them on a derived case — the same people the closed hierarchy already stops.

## Fixed: Unzip no longer throws on a defaulted component

`Option.Some((0, "x")).Unzip()` threw in v5. It now returns `(Some(0), Some("x"))`.

This falls out of the `Some` relaxation. We considered returning `None` for the defaulted component instead and rejected it: `UnwrapOr(-1)` would then hand back `-1` for a `0` that was genuinely there.

If you wrote defensive code around the old throw, delete it rather than re-pointing it.

## Changed: two Nones of the same type are now the same object

`None<T>` is a cached singleton in v6, so `Option.None<int>()` returns the same instance every time.

```csharp
// v5: false
// v6: true
ReferenceEquals(Option.None<int>(), Option.None<int>());
```

Equality and hashing are unchanged — two `None` values were already equal, and still are. Only `ReferenceEquals` answers differently.

This removes an allocation from every operation that produces a `None`: `Option.None<int>()` went from 1.65 ns and 24 B to 0.18 ns and 0 B.

{% hint style="info" %}
A `with` expression goes through the compiler-generated clone, not the factory, so it still hands back a second instance. The singleton is a guarantee of `Option.None<T>()`, not of the type.
{% endhint %}

## New: state overloads that avoid a closure

Every hot-path transform gained a sibling that takes your data as an argument and hands it to the delegate, so the delegate captures nothing:

```diff
-option.Map(value => value + offset);
+option.Map(offset, static (value, state) => value + state);
```

This is purely additive. No existing signature changed and there is nothing to migrate.

Covered methods, as at 6.0:

* `Option`: `Map`, `MapOr`, `MapOrElse`, `Filter`, `AndThen`
* `Result`: `Map`, `MapOr`, `MapOrElse`, `MapErr`, `AndThen`
* `Option.Try`, `Option.TryAsync`, `Result.Try`, `Result.TryAsync`

Later 6.x releases added more. See [Where you can use it](/reference/state-overloads#where-you-can-use-it) for the current set.

The closure costs exactly 88 bytes at every call site: 24 for the display class, 64 for the delegate. The state overload removes all of it.

See [State overloads](/reference/state-overloads) for the detail, including why the `static` keyword matters.

[`WM2017`](/reference/analyzers/idioms#wm2017) points you at these when it sees a delegate that captures.

## Analyzer rules that changed

### Removed

| Rule     | Why                                                                  |
| -------- | -------------------------------------------------------------------- |
| `WM1004` | Described the default-value invariant, which no longer exists        |
| `WM1007` | Deriving from `Option` or `Result` is a compile error now            |
| `WM1009` | `Option<bool>` is genuinely useful in v6, so the advice is withdrawn |
| `WM1010` | Forecast the `Some` relaxation, which has now happened               |
| `WM2014` | There is no `FlatMap` left to report                                 |

None of these IDs will be reused. If you suppressed one in `.editorconfig`, drop the entry.

### Added

| Rule                                                 | Severity   | What it reports                                        |
| ---------------------------------------------------- | ---------- | ------------------------------------------------------ |
| [`WM1011`](/reference/analyzers/runtime-bugs#wm1011) | Warning    | An async delegate passed to a synchronous method       |
| [`WM2016`](/reference/analyzers/idioms#wm2016)       | Suggestion | An eager argument that is not free to evaluate         |
| [`WM2017`](/reference/analyzers/idioms#wm2017)       | Suggestion | A delegate that captures where a state overload exists |

### Reworded

`WM1001` and `WM1005` now describe `Some` as rejecting null rather than rejecting the default of the type, and `WM1001` names `ArgumentNullException`. `WM2015` now names the value it hands back. No behaviour changed in any of the three.

## Everything on one page

| Change                              | Breaks the build? | What to do                                 |
| ----------------------------------- | ----------------- | ------------------------------------------ |
| `Try` with an async factory         | **No**            | Switch to `TryAsync` and `await`           |
| Cancellation propagates             | **No**            | Catch it, or `UseCancellationAsFailure()`  |
| `Some` accepts value-type defaults  | **No**            | Review every `IsNone` on a value type      |
| Async extensions return `ValueTask` | Yes               | Add `.AsTask()`                            |
| `FlatMap` removed                   | Yes               | Rename to `AndThen`                        |
| Hierarchies closed                  | Yes               | Compose instead of inherit                 |
| `Unzip` fixed                       | No                | Delete defensive code                      |
| `None<T>` is a singleton            | No                | Nothing, unless you used `ReferenceEquals` |
| State overloads added               | No                | Nothing — adopt them where it helps        |


# v4.x to v5.x

{% hint style="info" %}
**This page describes a historical upgrade.** Its replacement code was correct for `5.x`, and later majors have removed some of the API it names. Land on `5.x` first if you are following these steps, then read the pages after this one in order. [Deprecations](/upgrading/deprecations) lists what has gone since.
{% endhint %}

<details>

<summary>Upgrade with an agent — copy this prompt</summary>

Pointed at your solution, in Claude Code or a similar tool.

```
Upgrade this solution from Waystone.Monads v4 to v5. Report what you changed.

1. Build and capture every CS0029 and CS1503 involving Task and ValueTask on a
   Waystone async extension. In v5 several of those extensions return ValueTask<T>
   where they returned Task<T>.

2. For each one, prefer changing the declared type of the local or field to
   ValueTask<T>, or simply awaiting the value. Add .AsTask() only where the value is
   passed to something that demands a Task — Task.WhenAll is the usual case.
   .AsTask() allocates; do not reach for it first.

3. Find every async lambda passed to a Waystone async extension that returns a
   ValueTask<T>. v5 no longer accepts those. Change the lambda to return T or Task<T>.

4. Build again and report anything left. Do not suppress a diagnostic or add a
   null-forgiving ! to make one go away.
```

</details>

Simplified the API for chaining async extension methods on `Option<T>` and `Result<TOk, TErr>` , removing support for async lambdas that returned a `ValueTask<T>`. Additionally, optimized the return type of specific extensions that would return either a synchronous value or a Task to have a type of `ValueTask`.

```diff
-Task<string> output = result.MatchAsync(async x => await doWork(x), e => e.ToString());
+ValueTask<string> output = result.MatchAsync(async x => await doWork(x), e => e.ToString());
```


# v3.x to v4.x

{% hint style="info" %}
**This page describes a historical upgrade.** Its replacement code was correct for `4.x`, and later majors have removed some of the API it names — `UseExceptionLogger`, `ErrorCode.FromEnum` and the `ErrorCodeFactory.FromEnum` override are all gone in `7.0.0`. `UseErrorCodeFactory` itself remains, but it can no longer shape enum codes. Land on `4.x` first if you are following these steps, then read the pages in order. [Deprecations](/upgrading/deprecations) lists what has gone since.
{% endhint %}

<details>

<summary>Upgrade with an agent — copy this prompt</summary>

Pointed at your solution, in Claude Code or a similar tool.

```
Upgrade this solution from Waystone.Monads v3 to v4. Report what you changed.

1. Replace every MonadsGlobalConfig call with MonadOptions.Configure. The Use* methods
   move inside the callback, and FallbackErrorCode and FallbackErrorMessage are set as
   properties on the options object.

2. Find every implementation of IErrorCodeFormatter<T>. That interface is gone. Convert
   each to a class deriving from ErrorCodeFactory, and register it once at start-up with
   MonadOptions.Configure(options => options.UseErrorCodeFactory(new MyFactory())).

3. Delete the formatter argument from every ErrorCode.FromEnum call. The factory is
   applied for the life of the application now, not per call.

4. Build and report anything left. Do not suppress a diagnostic to make it go away.
```

</details>

Replaced `MonadsGlobalConfig` with `MonadOptions` to enable better DX when configuring library behaviours.

```diff
-MonadsGlobalCongig.UseExceptionLogger(...);
+MonadOptions.Configure(options => {
+    options.UseExceptionLogger(...)
+           .UseErrorCodeFactory(...)
+    options.FallbackErrorCode = "error.unknown";
+    options.FallbackErrorMessage = "An unknown error has occurred.";
+});
```

The `IErrorCodeFormatter<T>` interface has been removed in favour of `ErrorCodeFactory` so that it can be applied once during your app's life-cycle, instead of during each invocation of the error code creation methods. You can override the default formatting by inheriting `ErrorCodeFactory` and invoking `MonadOptions.UseErrorCodeFactory()`.

```diff
-class MyErrorCodeFormatter<MyErrorCodeEnum> : IErrorCodeFormatter<MyErrorCodeEnum>;
-ErrorCode.FromEnum(MyErrorCodeEnum.BadRequest, new MyErrorCodeFormatter<MyErrorCodeEnum>());
+class MyErrorCodeFactory : ErrorCodeFactory;
+MonadOptions.Configure(options => options.UseErrorCodeFactory(new MyErrorCodeFactory()));
```


# v2.x to v3.x

{% hint style="info" %}
**This page describes a historical upgrade.** Its replacement code was correct for `3.x`, and later majors have removed some of the API it names. Land on `3.x` first if you are following these steps, then read the pages after this one in order. [Deprecations](/upgrading/deprecations) lists what has gone since.
{% endhint %}

<details>

<summary>Upgrade with an agent — copy this prompt</summary>

Pointed at your solution, in Claude Code or a similar tool.

```
Upgrade this solution from Waystone.Monads v2 to v3. Report what you changed.

1. Add the Async suffix to every awaited Waystone call — Map becomes MapAsync, AndThen
   becomes AndThenAsync, and so on. The synchronous names no longer have async
   overloads.

2. Add using Waystone.Monads.Options.Extensions; and
   using Waystone.Monads.Results.Extensions; to every file that now fails to resolve
   one of those names. The async overloads are extension methods in v3, not instance
   members.

3. Collapse the intermediate awaits the old shape needed. Because the extensions apply
   to Task<Option<T>> and Task<Result<TOk, TErr>>, several steps chain from one await
   instead of one await each.

4. Build and report anything left.
```

</details>

Renamed async overloads for methods to have the `Async` suffix

```diff
-await option.Map(...);
+await option.MapAsync(...);
```

Fundamentally changed how async overloads are declared. They are now extension methods instead of existing in the Option/Result instance. This enables method chaining on `Task<Result<T, E>>` and `Task<Option<T>>`.

```diff
-var a = await option.Map(...);
-var b = await a.Map(...);
+var c = await option.MapAsync(...).MapAsync(...);
```

Moved async overloads into `Results.Extensions` and `Options.Extensions` namespaces.

```diff
using Waystone.Monads.Results;
using Waystone.Monads.Options;
+using Waystone.Monads.Results.Extensions;
+using Waystone.Monads.Options.Extensions;
```


# v1.x to v2.x

{% hint style="info" %}
**This page describes a historical upgrade.** Its replacement code was correct for `2.x`, and later majors have removed some of the API it names. Land on `2.x` first if you are following these steps, then read the pages after this one in order. [Deprecations](/upgrading/deprecations) lists what has gone since.
{% endhint %}

<details>

<summary>Upgrade with an agent — copy this prompt</summary>

Pointed at your solution, in Claude Code or a similar tool.

```
Upgrade this solution from Waystone.Monads v1 to v2. Report what you changed.

1. Rename every Option.Bind and Result.Bind factory call to Try. Parameters and
   behaviour are otherwise unchanged.

2. Delete the exception-handling callback from every Option.Try call. Option.Try no
   longer takes one. Result.Try still does — leave those.

3. Add one MonadsGlobalConfig.UseExceptionLogger call at start-up, pointed at the
   solution's existing logger, so the exceptions the library handles are still
   reported. Show me where you put it.

4. Build and report anything left.
```

</details>

## Renamed `Bind` to `Try`

The `Option.Bind` and `Result.Bind` factory methods have been renamed to `Try` to better adhere to functional programming concepts.

`Bind` is often associated with `FlatMap`, a way of composing functions together in a pipeline. This renaming removes the confusion.

```diff
-Option.Bind(() => CreateSome(), ex => Console.WriteLine(ex));
+Option.Try(() => CreateSome());

-Result.Bind(() => CreateOk(), ex => HandleEx(ex));
+Result.Try(() => CreateOk(), ex => HandleEx(ex));
```

## Introduced `MonadsGlobalConfig`

This configuration allows the setting of a global error logger that will be invoked whenever an exception is caught and handled by the library.

```csharp
MonadsGlobalConfig.UseExceptionLogger((ex) => {
    Console.WriteLine(ex); // replace with your logger's log method, e.g. serilog
});
```

## Removed local error handling for Option

Removed the local handle error callback on the `Option.Try` methods in favour of the `MonadsGlobalConfig`.

```diff
// program.cs
+MonadsGlobalConfig.UseExceptionLogger((ex) => {
+    Console.WriteLine(ex); // replace with your logger's log method, e.g. serilog
+});

// usage
-Option.Bind(() => CreateSome(), ex => Console.WriteLine(ex));
+Option.Try(() => CreateSome());
```


# Welcome

`Waystone.WideLogEvents` is a set of libraries inspired by [Logging Sucks - by Boris Tane](https://loggingsucks.com/). This package provides a way for capturing and managing a set of properties throughout a logical scope, which can then be enriched into your log events.

### Key Concepts

#### Wide Log Events

A wide log event is a pattern where instead of logging many small, disconnected events, you accumulate relevant information throughout a process (like a HTTP request) and log it all at once at the end. This makes it much easier to correlate data and understand the full context of an operation.

#### Wide Log Event Context

The central point for managing properties within a scope. It uses `AsyncLocal` to ensure properties are correctly tracked across async calls.

#### Wide Log Event Scope

A disposable scope that manages the lifecycle of properties. When a scope is created, it starts a new set of properties. When disposed, it restores the previous scope's properties.

### Usage

#### Start a Scope

Wrap your operation in a `WideLogEventScope`

```cs
using (var scope = WideLogEventContext.BeginScope())
{
    // Your code here
}
```

#### Pushing Properties

You can push properties to the current scope at any time

```cs
WideLogEventContext.PushProperty("CustomerId", 12345);
WideLogEventContext.PushProperty("OrderDetails", new { Total = 100.00, ItemCount = 3 });
```

## Links

* [Source on GitHub](https://github.com/draekien-industries/waystone-dotnet) — these packages live under `src/Serilog.Enrichers.Waystone.WideLogEvents`
* [Serilog.Enrichers.Waystone.WideLogEvents on NuGet](https://www.nuget.org/packages/Serilog.Enrichers.Waystone.WideLogEvents)
* [Serilog.Enrichers.Waystone.WideLogEvents.AspNetCore on NuGet](https://www.nuget.org/packages/Serilog.Enrichers.Waystone.WideLogEvents.AspNetCore)
* [Report an issue](https://github.com/draekien-industries/waystone-dotnet/issues) — for the libraries or for these docs


# Serilog

You can use `Waystone.WideLogEvents` with `Serilog` by installing the `Serilog.Enrichers.Waystone.WideLogEvents` package.

```bash
dotnet add package Serilog.Enrichers.Waystone.WideLogEvents
```

### Usage

Configure Serilog to use the Wide Log Events enricher and optional sampling filter:

```cs
using Serilog;
using Serilog.Enrichers.Waystone.WideLogEvents;

Log.Logger = new LoggerConfiguration()
    .Enrich.FromWideLogEventsContext()
    .Filter.WithWideLogEventsSampling(options => {
        options.InformationSampleRate = 0.5; // Log 50% of info logs

        // Optionally provide a custom random number generator
        options.RandomDoubleProvider = new MyRandomProvider();
    })
    // ... other configuration
    .CreateLogger();
```

### Customizing Randomness

The sampling filter uses `IRandomDoubleProvider` to determine whether a log event should be sampled. By default, it uses a simple implementation that instantiates a `new Random()`.

To use `Random.Shared` or a custom RNG, implement the interface:

```cs
public class MyRandomProvider : IRandomDoubleProvider
{
    public double NextDouble() => Random.Shared.NextDouble();
}
```


# Serilog + AspNetCore

You can use `Waystone.WideLogEvents` with `Serilog`  in `ASP.NET Core` applications by installing the `Serilog.Enrichers.Waystone.WideLogEvents.AspNetCore` package.

```bash
dotnet add package Serilog.Enrichers.Waystone.WideLogEvents.AspNetCore
```

### Usage

#### Configure Serilog

In your `Program.cs`, configure Serilog to use the wide log events enricher and the sampling filter:

```cs
using Serilog;
using Serilog.Enrichers.Waystone.WideLogEvents;

builder.Host.UseSerilog((context, config) => config
   .Enrich.FromWideLogEventsContext()
   .Filter.WithWideLogEventsSampling()
   .ReadFrom.Configuration(context.Configuration));
```

#### Register Middleware

Add the `UseWideLogEventsContext` middleware to your application pipeline. This middleware should typically be placed early in the pipeline to capture as much information as possible.

```cs
using Serilog.Enrichers.Waystone.WideLogEvents.AspNetCore;

var app = builder.Build();

app.UseWideLogEventsContext();

// ... other middleware
```

#### Push Properties

You can now push properties to the `WideLogEventContext` anywhere in your request lifecycle (e.g. in controllers, minimal API handlers, or services):

```cs
using Waystone.WideLogEvents;

app.MapGet("/weatherforecast", () =>
{
    var forecast = // ... get forecast

    // This property will be included in the final "wide" log for this request
    WideLogEventContext.PushProperty("ForecastCount", forecast.Length);

    return forecast;
});
```

### Configuration Options

You can customize the sampling behavior of the log events by configuring the sampling filter

```cs
builder.Host.UseSerilog((context, config) => config
   .Enrich.FromWideLogEventsContext()
   .Filter.WithWideLogEventsSampling(options =>
   {
       options.InformationSampleRate = 0.5; // Log 50% of information logs
       options.ErrorSampleRate = 1.0;       // Log 100% of error logs

       // Custom random provider for sampling decisions
       options.RandomDoubleProvider = new MyRandomProvider();
   })
   .ReadFrom.Configuration(context.Configuration));
```

#### Random Provider

See [Serilog](/waystone.widelogevents/quickstart/serilog#customizing-randomness)


