Trellis.Core API Reference

Package: Trellis.Core
Namespace: Trellis
Purpose: Provides Trellis result, maybe, scalar-value, and HTTP-oriented error primitives for railway-oriented application flows.

See also: trellis-api-cookbook.md — recipes using this package, trellis-api-asp.md, trellis-api-primitives.md.


Use this file when

  • You need exact signatures for Result, Result<T>, Maybe<T>, Error, WriteOutcome<T>, Page<T>, DDD primitives, specifications, or custom primitive base classes.
  • You are composing domain/application flows and need the canonical ROP operation: Bind, Map, Tap, Ensure, Combine, ParallelAsync, AsTask, or AsValueTask.
  • You are defining aggregates, entities, domain events, specifications, or source-generated Required*<TSelf> value objects.

Patterns Index

Use this table before searching the long type catalog.

Goal Canonical API See
Return success/failure without payload Result.Ok() / Result.Fail(error) (returns Result<Unit>) Result
Return success/failure with payload Result.Ok(value) / Result.Fail<T>(error) Result<TValue>
Turn a boolean guard into a result Result.Ensure(condition, error) or .Ensure(...) in a chain Result, Ensure family
Start independent async result-producing operations concurrently Result.ParallelAsync(...), then combine the returned tasks Result
Combine multiple validated typed fields into a tuple static Result.Combine<T1,T2>(Result<T1>, Result<T2>) or instance r1.Combine(r2), then .Map(...) Combine family
Combine multiple boolean guards Result.Ensure(...).Combine(Result.Ensure(...)) then .Bind(...) (extension Combine aggregates errors and adds each value as the next tuple element; pass a Result<Unit> from a no-payload guard and ignore it with _ in the next lambda) Combine family
Adapt an already-computed result to async APIs .AsTask() / .AsValueTask() ResultTaskAdapterExtensions
Model expected absence Maybe<T>, Maybe.Some(value), Maybe<T>.None Maybe<T>
Convert absence to a domain failure maybe.ToResult(error) / maybe.ToResult(errorFactory) MaybeExtensions
Create HTTP-oriented domain errors Closed Error cases plus ResourceRef.For<TResource>(id) Error, Error Cases
Page list responses new Page<T>(items, next, previous, requestedLimit, appliedLimit) (or Page.Empty<T>(...) when there are no items), Cursor Pagination
Model aggregates/entities/events Aggregate<TId>, Entity<TId>, IDomainEvent Domain-Driven Design
Move reusable query predicates out of repositories Specification<T> Specification<T>
Define custom required value objects partial class X : RequiredString<X> / RequiredGuid<X> / other Required* bases Primitive value object base classes

Canonical async handler skeleton

Every async command/query handler that composes Trellis primitives follows the same await-then-chain shape. The sync verbs (Bind/Map/Ensure) extend Result<T> receivers with sync delegates only. The async verbs (BindAsync/MapAsync/EnsureAsync) extend Result<T>, Task<Result<T>>, and ValueTask<Result<T>> receivers; on a sync receiver they take an async delegate (Task<...> or ValueTask<...>), while on a Task/ValueTask receiver they additionally provide sync-delegate convenience overloads. A Task<Result<T>> is not a Result<T> and does not expose the sync extensions — calling .Bind(...) on a Task<Result<T>> fails with CS1929: 'Task<Result<T>>' does not contain a definition for 'Bind'.

// Generic handler — Task<Result<TOut>>
public async Task<Result<OrderResponse>> Handle(CreateDraftOrderCommand cmd, CancellationToken ct)
{
    // 1. Sync precondition — produces a Result<Unit>, chains synchronously.
    var preconditions = Result.Ensure(cmd.LineItems.Count > 0,
                            Error.UnprocessableContent.ForField("lineItems", "required", "..."))
                        .Bind(_ => Result.Ensure(!cmd.HasDuplicates,
                            Error.UnprocessableContent.ForField("lineItems", "duplicate_product", "...")));

    if (preconditions.IsFailure) return Result.Fail<OrderResponse>(preconditions.Error);

    // 2. Async precondition — returns Task<Result<T>>; await BEFORE chaining sync extensions,
    //    or use the *Async siblings (BindAsync/EnsureAsync/MapAsync) which extend Task<Result<T>>.
    return await LoadCustomerAsync(cmd.CustomerId, ct)                  // Task<Result<Customer>>
        .EnsureAsync(c => c.IsActive,
            c => new Error.Forbidden("customer.inactive",
                ResourceRef.For<Customer>(c.Id)))                        // Task<Result<Customer>>
        .BindAsync(c => LoadProductsAsync(cmd.LineItems, ct))           // Task<Result<IReadOnlyList<Product>>>
        .MapAsync(products => Order.CreateDraft(cmd, products))         // Task<Result<Order>>
        .MapAsync(order => OrderResponse.From(order));                  // Task<Result<OrderResponse>>
}

// No-payload handler — Task<Result<Unit>>
public Task<Result<Unit>> Handle(CancelOrderCommand cmd, CancellationToken ct) =>
    LoadOrderAsync(cmd.OrderId, ct)                                     // Task<Result<Order>>
        .BindAsync(order => order.Cancel())                             // Task<Result<Unit>> (Cancel returns Result<Unit>)
        .BindAsync(_ => _uow.SaveChangesResultAsync(ct));               // Task<Result<Unit>>

Common build failures and their fix:

Diagnostic What the model wrote Fix
CS1929: 'Task<Result<T>>' does not contain a definition for 'Bind' LoadAsync(...).Bind(x => ...) Use BindAsync (which extends Task<Result<T>>), or await first then .Bind(...)
CS0411: type arguments for 'Map<TOut>' cannot be inferred someTaskResult.Map(...) after a CheckAsync whose result type can't be inferred await the precondition into a concrete Result<T> before projecting; or use MapAsync<TOut>(...)
CS0121: ambiguous between 'BindAsync<...>(Result<T>, Func<T, Task<Result<R>>>)' and 'BindAsync<...>(Result<T>, Func<T, ValueTask<Result<R>>>)' A sync Result<T> (not Task<Result<T>>) receiver with an inline async lambda whose return type can't be inferred between Task and ValueTask — both delegate-shape overloads exist on the sync receiver. Either extract the lambda to a named method with an explicit Task<Result<R>> return type, or pin the delegate type at the call site: Func<T, Task<Result<R>>> next = c => ...; result.BindAsync(next);. See Bind family — Task vs ValueTask ambiguity

Common traps

  • Do not use throwing value access in production code. Prefer TryGetValue, Match, Bind, Map, or deconstruction guarded by the success flag.
  • Do not use default(Result<T>) as success. The default state is a typed new Error.Unexpected("default_initialized") failure.
  • For no-payload success, use Result.Ok() (which returns Result<Unit>). The Trellis.Unit type is the canonical "no value" payload — it is a public readonly record struct with a single value (Unit.Default).
  • Use ParallelAsync only for independent work. If operation B depends on operation A, compose with Bind/BindAsync instead.

First-30-minutes surprises

Surprise What it actually is
Result<T>.Value getter does not exist (CS1061) Removed from the current API because it was the primary cause of unsafe value access. Extract via TryGetValue(out var v), Match(...), deconstruction var (ok, v, err) = result;, or chain with Bind/Map. Maybe<T>.Value does exist but is hidden from IntelliSense and gated by analyzer TRLS003 — guard with HasValue/TryGetValue/Match/GetValueOrDefault. The two types do not have symmetric value-access ergonomics.
Implicit T → Result<T> and Error → Result<T> were removed (CS0029) Use the explicit factory: return Result.Ok(value); and return Result.Fail<T>(error);. The C# compiler flags every site with CS0029: cannot implicitly convert type 'T' to 'Result<T>'.
Aggregate<TId> / Entity<TId> already provide CreatedAt and LastModified (CS0108) Both are DateTimeOffset (not DateTime) and infrastructure-managed by Trellis EF Core. Defining your own public DateTime CreatedAt { ... } on the aggregate triggers CS0108: 'X.CreatedAt' hides inherited member 'Entity<TId>.CreatedAt'. If your spec calls for an audit timestamp, use the inherited base property instead of declaring a new one.
Result<T> has .Error (nullable, never throws) but not .Value result.Error returns Error? (null on success). result.TryGetError(out var err) is the safe Boolean form. Reading the success value still requires TryGetValue/Match/destructuring.

Breaking changes from v1

Migration notes for users moving from the previous Trellis.Core API surface.

Change Previous API Current API Migration
Result success factory Result.Success(value) / Result.Success<T>(...) / Result.Success() Result.Ok(value) / Result.Ok<T>(...) / Result.Ok() Mechanical find-and-replace of Result.SuccessResult.Ok
Result failure factory Result.Failure<T>(error) / Result.Failure(error) Result.Fail<T>(error) / Result.Fail(error) Mechanical find-and-replace of Result.FailureResult.Fail
Deferred success factory Result.Success(Func<T> funcOk) (removed) Inline the factory: Result.Ok(funcOk())
Deferred failure factory Result.Failure<T>(Func<Error> errorFactory) (removed) Inline the factory: Result.Fail<T>(errorFactory())
Conditional factory Result.SuccessIf(cond, value, error) / Result.SuccessIf(cond, t1, t2, error) (removed) Use a ternary: cond ? Result.Ok(value) : Result.Fail<T>(error)
Inverse-conditional factory Result.FailureIf(cond, value, error) / Result.FailureIf(predicate, value, error) (removed) Use a ternary: cond ? Result.Fail<T>(error) : Result.Ok(value)
Async-conditional factories Result.SuccessIfAsync(predicate, value, error) / Result.FailureIfAsync(predicate, value, error) (removed) (await predicate()) ? Result.Ok(value) : Result.Fail<T>(error) (invert as needed; parens required because await binds tighter than ?:)
Exception → result helpers Result.FromException(ex) / Result.FromException<T>(ex) (removed) Use Result.Fail(new Error.InternalServerError(faultId) { Detail = ex.Message, Cause = ... }) or rely on Result.Try / Result.TryAsync for inline exception capture.
Implicit operators on Result<T> Result<T> r = value; and Result<T> r = error; (removed) Use the explicit factory: Result.Ok(value) / Result.Fail<T>(error). The compiler flags every site with CS0029.
Non-generic Result for void flows Result was a separate readonly struct for success/failure with no payload, distinct from Result<T>. The non-generic Result instance type was removed. Result is now a public static partial class factory only; for no-payload success/failure use Result<Unit> (returned by parameterless Result.Ok() / Result.Fail(error) / Result.Ensure(...) / Result.Try(...) factories). The Trellis.Unit type is a public readonly record struct with a single value (Unit.Default). Replace Result parameter/return types with Result<Unit>; replace Task<Result> with Task<Result<Unit>>; in lambdas after .Bind(...) / BindAsync(...) accept the Unit argument explicitly (_ => or (Unit _) =>).
Error as open class hierarchy Error was a class with 18 hand-written subclasses (ValidationError, NotFoundError, …) and static factory helpers (Error.Validation(...), Error.NotFound(...), …). Error is an abstract record with 20 nested sealed record cases (Error.NotFound, Error.UnprocessableContent, …). Closed via private constructor; no static factories. Replace Error.X("msg") factories with new Error.X(payload) { Detail = "msg" }. Replace concrete subclass type names (ValidationError, NotFoundError) with Error.UnprocessableContent, Error.NotFound. See "Error Cases (closed ADT)" below.
MatchErrorExtensions result.MatchError(onValidation: ..., onNotFound: ..., onUnexpected: ...) (removed) Use a switch expression on the closed ADT: result.Match(_ => ..., e => e switch { Error.NotFound nf => ..., Error.UnprocessableContent uc => ..., _ => ... }). C# verifies exhaustiveness against the closed catalog.
FlattenValidationErrorsExtensions result.FlattenValidationErrors() (removed) Combine over multiple Result<T> automatically merges Error.UnprocessableContent.Fields and .Rules.
Error.Instance field error.Instance (string-shaped HTTP vocabulary) (removed) The ASP wire layer synthesizes ProblemDetails.Instance from the request URL plus any ResourceRef carried by the typed payload.
Public Value / Error accessors on Result<T> Both threw on the wrong branch. result.Error is public Error? and never throws (null on success). The throwing result.Value getter was removed entirely because it was the primary cause of unsafe value access. Read errors with if (result.Error is { } error) { ... } or result.TryGetError(out var error). Extract success values with result.TryGetValue(out var v), result.TryGetValue(out var v, out var err), result.Match(...), or var (ok, v, err) = result; (Deconstruct).
WriteOutcome<T> package + namespace Trellis.Asp.WriteOutcome<T> (in Trellis.Asp) Trellis.WriteOutcome<T> (in Trellis.Core) Replace using Trellis.Asp; with using Trellis; for any file that names WriteOutcome<T> directly. The type, its case records, and member shapes are unchanged; only the assembly and namespace move. ASP-specific HTTP mapping remains in Trellis.Asp through ToHttpResponse(...) / ToHttpResponseAsync(...) and the typed MVC adapters AsActionResult<T>() / AsActionResultAsync<T>().
Package id Trellis.Results Trellis.Core Replace <PackageReference Include="Trellis.Results" ... /> with <PackageReference Include="Trellis.Core" ... />. The CLR namespace stays Trellis — no using changes are needed. The legacy Trellis.Results package is unlisted with a redirect notice; there is no metapackage shim.
OpenTelemetry ActivitySource name "Trellis.Results" "Trellis.Core" Update OTel subscriptions: builder.AddSource("Trellis.Results")builder.AddSource("Trellis.Core"). The RopTrace.ActivitySourceName constant exposes the name programmatically.
Test helper namespace Trellis.Results.Tests.* Trellis.Core.Tests.* Internal change only — affects users who took an InternalsVisibleTo dependency on the test assembly (none expected).
Package merge: DDD <PackageReference Include="Trellis.DomainDrivenDesign" .../> (removed) All DDD types (Aggregate<T>, Entity<T>, ValueObject, Specification<T>, etc.) moved into Trellis.Core. Drop the Trellis.DomainDrivenDesign PackageReference; the types are still in namespace Trellis; so no using changes are needed.
Package merge: Primitives generator <PackageReference Include="Trellis.Primitives.Generator" .../> (removed) The Required* source generator is now bundled inside Trellis.Core.nupkg (analyzers/dotnet/cs/Trellis.Core.Generator.dll). Installing Trellis.Core (or any package depending on it) attaches the analyzer automatically. Drop the standalone PackageReference.
Required* base classes Trellis.Primitives Trellis.Core Source-tree consumers may need to ensure they reference Trellis.Core. Namespace is unchanged (Trellis), so no using edits are required.
Package merge: Asp generator <PackageReference Include="Trellis.AspSourceGenerator" .../> (removed) The ASP source generator is now bundled inside Trellis.Asp.nupkg (analyzers/dotnet/cs/Trellis.AspSourceGenerator.dll). Installing Trellis.Asp attaches the analyzer automatically. Drop the standalone PackageReference.
Package merge: EF Core generator <PackageReference Include="Trellis.EntityFrameworkCore.Generator" .../> (removed) The EF Core source generator (Maybe<T> partial properties + owned value-object helpers) is now bundled inside Trellis.EntityFrameworkCore.nupkg (analyzers/dotnet/cs/Trellis.EntityFrameworkCore.Generator.dll). Installing Trellis.EntityFrameworkCore attaches the analyzer automatically. Drop the standalone PackageReference.
Package merge: Asp authorization <PackageReference Include="Trellis.Asp.Authorization" .../> (removed) The ASP.NET actor providers (ClaimsActorProvider, EntraActorProvider, DevelopmentActorProvider, CachingActorProvider, AddTrellisAspAuthorization()) are now part of Trellis.Asp.nupkg. The CLR namespace stays Trellis.Asp.Authorization — no using changes needed. Drop the standalone PackageReference. Trellis.Asp now transitively brings in Trellis.Authorization.

The renames bring the factory names in line with Rust (Ok/Err), F# (Ok), and FluentResults (Ok/Fail). The IsSuccess/IsFailure predicate properties are not renamed — predicates read as questions and stay long-form.


Types

public interface IResult

Base success/failure contract.

Properties

Name Type Notes
IsSuccess bool true for success results. Marked [MemberNotNullWhen(false, nameof(Error))].
IsFailure bool true for failure results. Marked [MemberNotNullWhen(true, nameof(Error))].
Error Error? null on success; never throws.

Methods

Signature Notes
bool TryGetError(out Error? error) Non-throwing failure extractor. [NotNullWhen(true)] on the out parameter.

Factory Methods

None.


public interface IResult<TValue> : IResult

Typed success/failure contract. Note: there is no Value property — the previous Value getter threw on failure and was the leading source of TRLS003. Use TryGetValue to extract the success payload.

Properties

None (inherits IsSuccess, IsFailure, Error from IResult).

Methods

Signature Notes
bool TryGetValue([MaybeNullWhen(false)] out TValue value) Non-throwing success extractor. Returns true and binds value on success; returns false and leaves value at default on failure.

Factory Methods

None.


public interface IFailureFactory<TSelf> where TSelf : IFailureFactory<TSelf>

Static factory contract for producing a failure instance of the implementing type.

Properties

None.

Methods

Signature Notes
static abstract TSelf CreateFailure(Error error) Used by generic pipeline code

Factory Methods

CreateFailure(Error error).


public static partial class Result

Static factory and helper surface for Result<TValue>. There is no non-generic instance Result type — for no-payload success/failure, use Result<Unit> (returned by the parameterless overloads listed below).

Default-state invariant. default(Result<T>) represents a failure carrying the shared new Error.Unexpected("default_initialized") sentinel — not success. This makes uninitialized state a typed failure rather than a silent success that would hide a programming error. Always construct via Result.Ok(...) or Result.Fail(error). Analyzer TRLS019 flags explicit default(Result<T>) at call sites.

Result is public static partial class Result. It hosts the static factory and helper methods used to build every Result<TValue>.

Static factory methods

Signature Notes
public static Result<TValue> Ok<TValue>(TValue value) Success factory
public static Result<Unit> Ok() Success without payload (returns Result<Unit>)
public static Result<TValue> Fail<TValue>(Error error) Failure factory
public static Result<Unit> Fail(Error error) Failure without payload (returns Result<Unit>)
public static Result<Unit> Ensure(bool flag, Error error) Converts a boolean to Result<Unit>
public static Result<Unit> Ensure(Func<bool> predicate, Error error) Deferred predicate version
public static Task<Result<Unit>> EnsureAsync(Func<Task<bool>> predicate, Error error) Async predicate version
public static Result<T> Try<T>(Func<T> func, Func<Exception, Error>? map = null) Converts thrown exceptions to failures
public static Task<Result<T>> TryAsync<T>(Func<Task<T>> func, Func<Exception, Error>? map = null) Async exception capture
public static Result<Unit> Try(Action work, Func<Exception, Error>? map = null) No-payload exception capture (returns Result<Unit>)
public static Task<Result<Unit>> TryAsync(Func<Task> work, Func<Exception, Error>? map = null) Async no-payload exception capture
public static Result<(T1, T2)> Combine<T1, T2>(Result<T1> r1, Result<T2> r2) Combines two results; passing a Result<Unit> adds Unit as the next tuple element
public static Result<(T1, ..., T9)> Combine<...>(...) Additional generated arities up to 9
public static (Task<Result<T1>>, ..., Task<Result<T9>>) ParallelAsync<...>(...) Starts async result-producing operations in parallel, arities 2-9

The default exception mapper produces new Error.InternalServerError(FaultId: Guid.NewGuid().ToString("N")) { Detail = ex.Message }. OperationCanceledException is always rethrown rather than mapped.

Factory Methods

Ok, Fail, Ensure, Try, TryAsync, Combine, and ParallelAsync. Removed from the current API (see "Breaking changes from v1" above): Success, Failure, Success(Func<T>), Failure<T>(Func<Error>), SuccessIf, FailureIf, SuccessIfAsync, FailureIfAsync, FromException, and the non-generic Result instance type itself (CreateFailure, IFailureFactory<Result>, IEquatable<Result>, etc.).


public readonly struct Result<TValue> : IResult<TValue>, IEquatable<Result<TValue>>, IFailureFactory<Result<TValue>>

Represents either a successful TValue or a failure Error.

Default-state invariant. default(Result<T>) represents a failure carrying the shared new Error.Unexpected("default_initialized") sentinel — not success with default(T). All failure-facing APIs (Error, TryGetError, Deconstruct, Equals, GetHashCode, ToString, AsUnit) route through this sentinel so that default(Result<T>) is observationally equivalent to Result.Fail<T>(new Error.Unexpected("default_initialized")). Always construct via Result.Ok(value) or Result.Fail<T>(error). Analyzer TRLS019 flags explicit default(Result<T>) at call sites.

No Value property. The throwing public TValue Value getter was removed. Use TryGetValue, Match, or Deconstruct to extract success values.

Properties

Name Type Notes
Error Error? null on success; never throws. Pattern-match on the value (e.g. if (result.Error is { } error)) for imperative branches. For default(Result<T>), returns the shared new Error.Unexpected("default_initialized") sentinel.
IsSuccess bool Success flag. [MemberNotNullWhen(false, nameof(Error))].
IsFailure bool Failure flag. [MemberNotNullWhen(true, nameof(Error))]. default(Result<T>).IsFailure is true.

Methods

Signature Notes
public static Result<TValue> CreateFailure(Error error) Implements IFailureFactory<Result<TValue>>; lets generic pipeline behaviors construct failures polymorphically. Equivalent to Result.Fail<TValue>(error).
public bool TryGetValue([MaybeNullWhen(false)] out TValue value) Non-throwing success extractor. [MemberNotNullWhen(false, nameof(Error))].
public bool TryGetValue([MaybeNullWhen(false)] out TValue value, [NotNullWhen(false)] out Error? error) Combined extractor — binds both value (on success) and error (on failure) in one call, eliminating the need for result.Error! after a failed single-out TryGetValue.
public bool TryGetError([NotNullWhen(true)] out Error? error) Non-throwing failure extractor; on default(Result<T>) returns true with the Error.Unexpected sentinel.
public void Deconstruct(out bool isSuccess, out TValue? value, out Error? error) Deconstruction support: var (ok, value, error) = result;.
public Result<Unit> AsUnit() Discards the success value, returning a Result<Unit>. On a default-initialized failure, returns an explicit Result.Fail(sentinel) (never another default).
public bool Equals(Result<TValue> other) Value equality. Equal if both are success with EqualityComparer<TValue>.Default.Equals over the values, or both are failure with equal Error. Default-initialized failures route through the shared sentinel.
public override bool Equals(object? obj) Object equality.
public override int GetHashCode() Hash code matching Equals.
public override string ToString() "Success({value})" or "Failure({Code}: {Detail})".

Operators

The implicit conversion operators (TValue → Result<TValue>, Error → Result<TValue>) were removed from the current API. Use Result.Ok(value) / Result.Fail<T>(error).

Signature Notes
public static bool operator ==(Result<TValue> left, Result<TValue> right) Equality
public static bool operator !=(Result<TValue> left, Result<TValue> right) Inequality

Factory Methods

Use the static Result type.


public static class Maybe

Non-generic helpers for creating Maybe<T> and optional result flows.

Properties

None.

Methods

Signature Notes
public static Maybe<T> From<T>(T? value) where T : notnull Wraps nullable input
public static Result<Maybe<TOut>> Optional<TIn, TOut>(TIn? value, Func<TIn, Result<TOut>> function) where TIn : class where TOut : notnull Runs function only when a reference value exists
public static Result<Maybe<TOut>> Optional<TIn, TOut>(TIn? value, Func<TIn, Result<TOut>> function) where TIn : struct where TOut : notnull Value-type overload

Factory Methods

From and Optional.


public static class MaybeInvariant

Multi-field validation helpers for Maybe<T> values. Each method returns Result<Unit> — success when the invariant holds, or an Error.UnprocessableContent whose Fields list carries one FieldViolation per offending field. Field paths are normalized via InputPointer.ForProperty(name) (RFC 6901 JSON Pointer).

Methods

Signature Notes
public static Result<Unit> AllOrNone<T1, T2>(Maybe<T1> first, Maybe<T2> second, string firstFieldName, string secondFieldName) All fields present or all absent. Arities 2, 3, 4.
public static Result<Unit> Requires<T1, T2>(Maybe<T1> source, Maybe<T2> required, string sourceFieldName, string requiredFieldName) If source is present, required must be too. Arity 2.
public static Result<Unit> MutuallyExclusive<T1, T2>(Maybe<T1> first, Maybe<T2> second, string firstFieldName, string secondFieldName) At most one field may be present. Arities 2, 3.
public static Result<Unit> ExactlyOne<T1, T2>(Maybe<T1> first, Maybe<T2> second, string firstFieldName, string secondFieldName) Exactly one field must be present. Arities 2, 3.
public static Result<Unit> AtLeastOne<T1, T2>(Maybe<T1> first, Maybe<T2> second, string firstFieldName, string secondFieldName) At least one field must be present. Arities 2, 3.

Usage

// All-or-none: street + city must both be provided or both omitted
MaybeInvariant.AllOrNone(command.Street, command.City, "street", "city");

// Requires: if discount is given, reason is required
MaybeInvariant.Requires(command.Discount, command.DiscountReason, "discount", "discountReason");

// ExactlyOne: must provide either email or phone
MaybeInvariant.ExactlyOne(command.Email, command.Phone, "email", "phone");

public readonly struct Maybe<T> where T : notnull

Optional value container for domain optionality.

Default-state invariant. default(Maybe<T>) equals Maybe<T>.None (the type already uses an _isValueSet discriminator). Although correct, prefer the explicit Maybe<T>.None for readability. Analyzer TRLS019 flags explicit default(Maybe<T>) at call sites and recommends Maybe<T>.None instead.

Properties

Name Type Notes
None Maybe<T> Static empty instance
Value T Throws when HasNoValue is true
HasValue bool Present flag
HasNoValue bool Empty flag

Methods

Signature Notes
public static Maybe<T> From(T? value) Static constructor
public T GetValueOrThrow(string? errorMessage = null) Throwing extractor
public T GetValueOrDefault(T defaultValue) Fallback extractor
public T GetValueOrDefault(Func<T> defaultFactory) Deferred fallback
public bool TryGetValue(out T value) Non-throwing extractor
public Maybe<TResult> Map<TResult>(Func<T, TResult> selector) where TResult : notnull Maps present value
public TResult Match<TResult>(Func<T, TResult> some, Func<TResult> none) Branches on presence
public Maybe<TResult> Bind<TResult>(Func<T, Maybe<TResult>> selector) where TResult : notnull Flat-map
public Maybe<T> Or(T fallback) Fallback value
public Maybe<T> Or(Func<T> fallbackFactory) Deferred fallback value
public Maybe<T> Or(Maybe<T> fallback) Fallback maybe
public Maybe<T> Or(Func<Maybe<T>> fallbackFactory) Deferred fallback maybe
public Maybe<T> Where(Func<T, bool> predicate) Keeps value only when predicate passes
public Maybe<T> Tap(Action<T> action) Side effect on value
public override bool Equals(object? obj) Equality
public bool Equals(Maybe<T> other) Equality
public bool Equals(T? other) Equality against raw value
public override int GetHashCode() Hash code
public override string ToString() Debug string

Operators

Signature Notes
public static implicit operator Maybe<T>(T value) Implicit success-like wrap
public static bool operator ==(Maybe<T> maybe, T value) Equality
public static bool operator !=(Maybe<T> maybe, T value) Inequality
public static bool operator ==(Maybe<T> maybe, object? other) Equality
public static bool operator !=(Maybe<T> maybe, object? other) Inequality
public static bool operator ==(Maybe<T> first, Maybe<T> second) Equality
public static bool operator !=(Maybe<T> first, Maybe<T> second) Inequality

Factory Methods

None and From.


public interface IScalarValue<TSelf, TPrimitive> where TSelf : IScalarValue<TSelf, TPrimitive> where TPrimitive : IComparable

Contract for scalar value objects that validate and expose a primitive payload.

Properties

Name Type Notes
Value TPrimitive Wrapped primitive

Methods

Signature Notes
static abstract Result<TSelf> TryCreate(TPrimitive value, string? fieldName = null) Primitive-based validation entry point
static abstract Result<TSelf> TryCreate(string? value, string? fieldName = null) String-based validation entry point
static virtual TSelf Create(TPrimitive value) Throws on validation failure

Factory Methods

TryCreate and Create.


public interface IFormattableScalarValue<TSelf, TPrimitive> : IScalarValue<TSelf, TPrimitive> where TSelf : IFormattableScalarValue<TSelf, TPrimitive> where TPrimitive : IComparable

Extends IScalarValue for culture-aware string parsing.

Properties

Inherited only.

Methods

Signature Notes
static abstract Result<TSelf> TryCreate(string? value, IFormatProvider? provider, string? fieldName = null) Culture-aware parse-and-validate

Factory Methods

TryCreate(string?, IFormatProvider?, string?).


public abstract record Error

Closed discriminated union of error values. Each case is a nested sealed record mirroring an entry from the IANA HTTP Status Code Registry (RFC 9110, RFC 6585) and carrying a strongly-typed payload. The base record has a private constructor — only the cases declared in Error.cs may inherit, which makes switch over an Error reference exhaustive at the language level.

Properties

Name Type Notes
Kind string Stable, IANA-aligned slug (e.g. "not-found", "unprocessable-content"). Survives CLR renames. Suitable for telemetry, problem-details type URI synthesis, and wire serialization. Abstract; each case overrides.
Code string Per-instance machine-readable code. Defaults to Kind; cases whose payload carries a per-instance ReasonCode (e.g. Conflict, Forbidden, BadRequest, InternalServerError) override this.
Detail string? Human-readable detail. Init-only (Detail = "..."). Boundary renderer prefers it when non-null; otherwise it computes a localized message from Kind/Code plus the typed payload.
Cause Error? Structured cause chain. Never holds a live System.Exception — wrap context as a child Error. Cycles are detected at init and throw InvalidOperationException.

Methods

Signature Notes
public string GetDisplayMessage() Computes the rendered detail. Returns Detail when non-null; otherwise composes from Kind/Code and the typed payload. For an UnprocessableContent carrying a single FieldViolation, returns just that violation's Detail.
public override bool Equals(object? obj) / Equals(Error? other) Value equality over discriminator + typed payload + Detail. Cause is excluded so two errors with identical surface payload compare equal regardless of how deeply they were wrapped (mirrors System.Exception precedent). Collection-bearing payloads use EquatableArray<T> for sequence equality.
public override int GetHashCode() Hash matches Equals.

Construction (no static factory methods)

Construct cases directly: new Error.NotFound(payload) { Detail = "..." }. The base type intentionally exposes no static Error.Validation(...) / Error.NotFound(...) helpers — every call site names the case it produces.


Concrete error cases

Twenty nested sealed record cases under Error. Each case constructor is internal from external code's perspective only insofar as the base ctor is private; cases themselves are public sealed records and instantiable with new.

Case Constructor Wire status Notes
Error.BadRequest (string ReasonCode, InputPointer? At = null) 400 Code returns ReasonCode; At is an optional RFC 6901 JSON Pointer to the offending input.
Error.Unauthorized (EquatableArray<AuthChallenge> Challenges = default) 401 Round-trips real WWW-Authenticate (per RFC 9110 §11.6.1).
Error.Forbidden (string PolicyId, ResourceRef? Resource = null) 403 Code returns PolicyId.
Error.NotFound (ResourceRef Resource) 404 Resource (e.g. ResourceRef.For<Order>(orderId)) drives ProblemDetails instance.
Error.MethodNotAllowed (EquatableArray<string> Allow) 405 Allow populates the Allow response header (RFC 9110 §15.5.6).
Error.NotAcceptable (EquatableArray<string> Available) 406 Available media types.
Error.Conflict (ResourceRef? Resource, string ReasonCode) 409 Code returns ReasonCode.
Error.Gone (ResourceRef Resource) 410 Soft-deleted resource.
Error.PreconditionFailed (ResourceRef Resource, PreconditionKind Condition) 412 Optimistic concurrency mismatch. Condition is the typed precondition kind (IfMatch, IfNoneMatch, IfModifiedSince, IfUnmodifiedSince).
Error.ContentTooLarge (long? MaxBytes = null) 413
Error.UnsupportedMediaType (EquatableArray<string> Supported) 415
Error.RangeNotSatisfiable (long CompleteLength, string Unit = "bytes") 416 Drives Content-Range synthesis.
Error.UnprocessableContent (EquatableArray<FieldViolation> Fields, EquatableArray<RuleViolation> Rules = default) 422 The single domain-validation case — replaces the previous validation error class. Carries both per-field violations and cross-field rule violations. Single-violation factories (preferred over manual construction): Error.UnprocessableContent.ForField(string propertyName, string reasonCode, string? detail = null) (escapes propertyName via InputPointer.ForProperty), ForField(InputPointer field, string reasonCode, string? detail = null) (use for nested/array pointers or InputPointer.Root), and ForRule(string reasonCode, string? detail = null) (single rule, empty fields). Use Validate builder when aggregating multiple violations.
Error.PreconditionRequired (PreconditionKind Condition) 428 Missing concurrency token on PUT.
Error.TooManyRequests (RetryAfterValue? RetryAfter = null) 429
Error.InternalServerError (string FaultId) 500 Code returns FaultId. Never holds a live Exception; the FaultId indexes into the logging/telemetry layer.
Error.Unexpected (string ReasonCode) 500 "Shouldn't happen" condition: default-initialized Result/Result<T> (per §3.5.1), exhausted match arms, internal invariant violations. Code returns ReasonCode (e.g. "default_initialized"). Distinct from InternalServerError: no opaque per-incident FaultId, and the ASP boundary does not attach a faultId extension.
Error.NotImplemented (string Feature) 501 Code returns Feature.
Error.ServiceUnavailable (RetryAfterValue? RetryAfter = null) 503
Error.Aggregate (EquatableArray<Error> Errors)
(IEnumerable<Error> errors)
(params Error[] errors)
207 / extensions.errors Composition node. Disallows Cause (pure composition). Auto-flattens nested Aggregate instances at construction. Three constructor overloads accept either an EquatableArray<Error>, any IEnumerable<Error>, or a params array; all three throw ArgumentException if no errors are supplied. InputPointer.ForProperty is used to escape ~ and / per RFC 6901.

Supporting types

Type Shape Purpose
ResourceRef readonly record struct (string Type, string? Id = null) plus ResourceRef.For(string type, object? id = null) and ResourceRef.For<TResource>(object? id = null) Aggregate identity. The For(...) helpers convert IDs with invariant formatting when possible and For<TResource> uses typeof(TResource).Name exactly.
InputPointer readonly record struct (string Path) RFC 6901 JSON Pointer (e.g. /email). Construct via InputPointer.ForProperty("email"), or use the document-root sentinel InputPointer.Root (path ""). InputPointer.Root is the canonical pointer for whole-body / object-level violations.
FieldViolation sealed record (InputPointer Field, string ReasonCode, ImmutableDictionary<string,string>? Args = null, string? Detail = null) Single per-field violation inside UnprocessableContent.Fields. Detail is the 4th positional parameter; supplies the boundary renderer's user-facing message when non-null. Equals/GetHashCode compare Args by content.
RuleViolation sealed record (string ReasonCode, EquatableArray<InputPointer> Fields = default, ImmutableDictionary<string,string>? Args = null, string? Detail = null) Multi-field invariant or object-level rule inside UnprocessableContent.Rules. Detail is the 4th positional parameter. Equals/GetHashCode compare Args by content.
AuthChallenge sealed record (string Scheme, ImmutableDictionary<string,string>? Params = null) Carried by Unauthorized to round-trip WWW-Authenticate. Equals/GetHashCode compare Params by content; parameter order is not significant.
RetryAfterValue sealed class, see below Retry-After as delay seconds or absolute date.
PreconditionKind enum { IfMatch, IfNoneMatch, IfModifiedSince, IfUnmodifiedSince } Typed precondition vocabulary.
EquatableArray<T> readonly struct (ImmutableArray<T> Items) Wraps ImmutableArray<T> so records get sequence equality (built-in records compare arrays by reference). See dedicated section below. Construct with EquatableArray.Create(...), EquatableArray.From(items), EquatableArray<T>.Empty, or implicitly from an ImmutableArray<T>.

public sealed class RetryAfterValue : IEquatable<RetryAfterValue>

Represents Retry-After as either delay seconds or a date.

Properties

Name Type
IsDelaySeconds bool
IsDate bool
DelaySeconds int
Date DateTimeOffset

Methods

Signature Notes
public static RetryAfterValue FromSeconds(int seconds) Delay form
public static RetryAfterValue FromDate(DateTimeOffset date) Absolute-date form
public string ToHeaderValue() RFC header value
public override string ToString() String form
public bool Equals(RetryAfterValue? other) Equality
public override bool Equals(object? obj) Equality
public override int GetHashCode() Hash code

Factory Methods

FromSeconds and FromDate.


public sealed record EntityTagValue

Represents strong, weak, or wildcard ETags.

Properties

Name Type
OpaqueTag string
IsWeak bool
IsWildcard bool

Methods

Signature Notes
public static EntityTagValue Strong(string opaqueTag) Strong ETag
public static EntityTagValue Weak(string opaqueTag) Weak ETag
public static EntityTagValue Wildcard() Wildcard ETag
public static Result<EntityTagValue> TryParse(string? headerValue) Parse from HTTP header
public bool StrongEquals(EntityTagValue other) Strong comparison
public bool WeakEquals(EntityTagValue other) Weak comparison
public string ToHeaderValue() RFC header form
public override string ToString() String form

Factory Methods

Strong, Weak, Wildcard, and TryParse.


public sealed class RepresentationMetadata

Metadata used by Trellis ASP helpers for validators, caching, and response headers.

Properties

Name Type
ETag EntityTagValue?
LastModified DateTimeOffset?
Vary IReadOnlyList<string>?
ContentLanguage IReadOnlyList<string>?
ContentLocation string?
AcceptRanges string?

Methods

Signature Notes
public static Builder Create() Starts fluent builder
public static RepresentationMetadata WithETag(EntityTagValue eTag) Convenience metadata
public static RepresentationMetadata WithStrongETag(string opaqueTag) Strong ETag convenience

Factory Methods

Create, WithETag, WithStrongETag.


public sealed class RepresentationMetadata.Builder

Fluent builder for RepresentationMetadata.

Properties

None.

Methods

Signature Notes
public Builder SetETag(EntityTagValue eTag) Sets ETag
public Builder SetStrongETag(string opaqueTag) Convenience strong ETag
public Builder SetWeakETag(string opaqueTag) Convenience weak ETag
public Builder SetLastModified(DateTimeOffset lastModified) Sets last modified
public Builder AddVary(params string[] fieldNames) Adds Vary fields
public Builder AddContentLanguage(params string[] languages) Adds content languages
public Builder SetContentLocation(string uri) Sets content location
public Builder SetAcceptRanges(string value) Sets Accept-Ranges
public RepresentationMetadata Build() Builds metadata

Factory Methods

Use RepresentationMetadata.Create().


public abstract record WriteOutcome<T>

Closed union representing the outcome of a write operation (create / replace / accept-for-async) returned by Application-layer repositories. Transport adapters (e.g. Trellis.Asp's WriteOutcomeExtensions) translate each case to a protocol-specific response. The case set aligns with RFC 9110 §9.3.4 because HTTP is the most commonly served transport, but WriteOutcome<T> itself takes no dependency on any transport package.

public abstract record WriteOutcome<T>
{
    public sealed record Created(T Value, string Location, RepresentationMetadata? Metadata = null)         : WriteOutcome<T>;
    public sealed record Updated(T Value, RepresentationMetadata? Metadata = null)                          : WriteOutcome<T>;
    public sealed record UpdatedNoContent(RepresentationMetadata? Metadata = null)                          : WriteOutcome<T>;
    public sealed record Accepted(T StatusBody, string? MonitorUri = null, RetryAfterValue? RetryAfter = null)      : WriteOutcome<T>;
    public sealed record AcceptedNoContent(string? MonitorUri = null, RetryAfterValue? RetryAfter = null)           : WriteOutcome<T>;
}
Case Members Transports as
Created T Value, string Location, RepresentationMetadata? Metadata HTTP 201 Created + Location
Updated T Value, RepresentationMetadata? Metadata HTTP 200 OK
UpdatedNoContent RepresentationMetadata? Metadata HTTP 204 No Content
Accepted T StatusBody, string? MonitorUri, RetryAfterValue? RetryAfter HTTP 202 Accepted + body
AcceptedNoContent string? MonitorUri, RetryAfterValue? RetryAfter HTTP 202 Accepted

The base record's constructor is private; new cases cannot be added by consumers.


public sealed class RailwayTrackAttribute : Attribute

Annotates result helpers with whether they operate on the success or failure railway.

Properties

Name Type
Track TrackBehavior

Methods

Signature Notes
public RailwayTrackAttribute(TrackBehavior track) Constructor

Factory Methods

None.


public enum TrackBehavior

Values: Success, Failure.


public static class ResultDebugSettings

Global debug switch for result tracing.

Properties

Name Type
EnableDebugTracing bool

Methods

None.

Factory Methods

None.


public static class ResultsTraceProviderBuilderExtensions

OpenTelemetry helper for Trellis result instrumentation. Lives in Trellis.Core\src\ResultsTraceProviderBuilderExtensions.cs and takes a hard dependency on the OpenTelemetry.Trace package — Trellis.Core references the OpenTelemetry SDK so consumers do not need a separate package reference to opt in.

Methods

Signature Notes
public static TracerProviderBuilder AddResultsInstrumentation(this TracerProviderBuilder builder) Registers the Trellis ROP ActivitySource (named "Trellis.Core", exposed as RopTrace.ActivitySourceName) with the supplied OpenTelemetry tracer-provider builder. Returns the same builder for chaining.

public readonly struct EquatableArray<T> : IEquatable<EquatableArray<T>>

Wraps ImmutableArray<T> so records and other value-equal types get sequence equality. Built-in record equality compares arrays by reference; this wrapper restores element-wise comparison. A default-initialized EquatableArray<T> represents an empty sequence — two default values compare equal, and Items always returns ImmutableArray<T>.Empty instead of an uninitialized array.

LINQ / FluentAssertions / IEnumerable<T> consumers. EquatableArray<T> exposes a duck-typed GetEnumerator() (allocation-free foreach) but does not implement IEnumerable<T> — this is intentional, to keep the value-type sequence-equality wrapper allocation-free. Methods that bind on IEnumerable<T> (Select, Where, Any, ToList, FluentAssertions' Should().ContainSingle() / Should().HaveCount(...) / Should().BeEquivalentTo(...), string.Join, etc.) won't see the contents directly. Project through .Items first (which returns the wrapped ImmutableArray<T>, an IEnumerable<T>):

// ❌ Doesn't compile: 'EquatableArray<RuleViolation>' does not contain a definition for 'Where'
unproc.Rules.Where(r => r.ReasonCode == "...");

// ❌ FluentAssertions: 'object does not contain a definition for ContainSingle'
unproc.Rules.Should().ContainSingle();

// ✅ Use .Items
unproc.Rules.Items.Where(r => r.ReasonCode == "...");
unproc.Rules.Items.Should().ContainSingle().Which.ReasonCode.Should().Be("...");

Properties

Name Type Notes
Items ImmutableArray<T> The wrapped array. Returns ImmutableArray<T>.Empty for default-initialized values rather than the uninitialized default.
Length int Number of items.
IsEmpty bool True when the wrapped array is empty.
this[int index] T Indexer over the wrapped array.
Empty EquatableArray<T> Static empty instance, mirrors ImmutableArray<T>.Empty.

Methods

Signature Returns Description
public EquatableArray(ImmutableArray<T> items) Wraps an existing immutable array.
public static EquatableArray<T> Create(params T[] items) EquatableArray<T> Builds from a params array.
public static EquatableArray<T> From(IEnumerable<T> items) EquatableArray<T> Builds from any enumerable.
public ImmutableArray<T>.Enumerator GetEnumerator() ImmutableArray<T>.Enumerator Allocation-free foreach support.
public bool Equals(EquatableArray<T> other) bool Sequence equality using EqualityComparer<T>.Default.
public override bool Equals(object? obj) bool Object equality.
public override int GetHashCode() int Combines hashes of all items via HashCode.

Operators

Signature Notes
public static bool operator ==(EquatableArray<T> left, EquatableArray<T> right) Equality
public static bool operator !=(EquatableArray<T> left, EquatableArray<T> right) Inequality
public static implicit operator EquatableArray<T>(ImmutableArray<T> items) Implicit conversion from ImmutableArray<T>

public static class EquatableArray

Non-generic factory companion that allows type inference at the call site.

Signature Returns Description
public static EquatableArray<T> Create<T>(params T[] items) EquatableArray<T> Inferred-T factory; equivalent to EquatableArray<T>.Create(items).
public static EquatableArray<T> From<T>(IEnumerable<T> items) EquatableArray<T> Inferred-T factory; equivalent to EquatableArray<T>.From(items).

Extension Methods

MaybeExtensions

Signature
public static Maybe<T> AsMaybe<T>(this T? value) where T : struct
public static Maybe<T> AsMaybe<T>(this T value) where T : class
public static T? AsNullable<T>(in this Maybe<T> value) where T : struct
public static Result<TValue> ToResult<TValue>(in this Maybe<TValue> maybe, Error error) where TValue : notnull
public static Result<TValue> ToResult<TValue>(in this Maybe<TValue> maybe, Func<Error> ferror) where TValue : notnull
public static Result<TValue> ToResult<TValue>(this TValue value)

MaybeExtensionsAsync

Signature
public static Task<Result<TValue>> ToResultAsync<TValue>(this Task<Maybe<TValue>> maybeTask, Error error) where TValue : notnull
public static ValueTask<Result<TValue>> ToResultAsync<TValue>(this ValueTask<Maybe<TValue>> maybeTask, Error error) where TValue : notnull
public static Task<Result<TValue>> ToResultAsync<TValue>(this Task<Maybe<TValue>> maybeTask, Func<Error> ferror) where TValue : notnull
public static ValueTask<Result<TValue>> ToResultAsync<TValue>(this ValueTask<Maybe<TValue>> maybeTask, Func<Error> ferror) where TValue : notnull
public static Task<TResult> MatchAsync<TValue, TResult>(this Task<Maybe<TValue>> maybeTask, Func<TValue, TResult> some, Func<TResult> none) where TValue : notnull
public static ValueTask<TResult> MatchAsync<TValue, TResult>(this ValueTask<Maybe<TValue>> maybeTask, Func<TValue, TResult> some, Func<TResult> none) where TValue : notnull
public static Task<TResult> MatchAsync<TValue, TResult>(this Task<Maybe<TValue>> maybeTask, Func<TValue, Task<TResult>> some, Func<Task<TResult>> none) where TValue : notnull
public static ValueTask<TResult> MatchAsync<TValue, TResult>(this ValueTask<Maybe<TValue>> maybeTask, Func<TValue, ValueTask<TResult>> some, Func<ValueTask<TResult>> none) where TValue : notnull

MaybeChooseExtensions

Signature
public static IEnumerable<T> Choose<T>(this IEnumerable<Maybe<T>> source) where T : notnull
public static IEnumerable<TResult> Choose<T, TResult>(this IEnumerable<Maybe<T>> source, Func<T, TResult> selector) where T : notnull

MaybeLinqExtensions

Signature
public static Maybe<TOut> Select<TIn, TOut>(this Maybe<TIn> maybe, Func<TIn, TOut> selector) where TIn : notnull where TOut : notnull
public static Maybe<TResult> SelectMany<TSource, TCollection, TResult>(this Maybe<TSource> source, Func<TSource, Maybe<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector) where TSource : notnull where TCollection : notnull where TResult : notnull

MaybeCollectionExtensions

Signature
public static Maybe<T> TryFirst<T>(this IEnumerable<T> source) where T : notnull
public static Maybe<T> TryFirst<T>(this IEnumerable<T> source, Func<T, bool> predicate) where T : notnull
public static Maybe<T> TryLast<T>(this IEnumerable<T> source) where T : notnull
public static Maybe<T> TryLast<T>(this IEnumerable<T> source, Func<T, bool> predicate) where T : notnull

Result pipeline extension families

The result API contains a large generated extension surface. Exact public families:

Static Class Public Surface
BindExtensions, BindExtensionsAsync Bind/BindAsync for Result<T> plus generated tuple overloads for arities 2-9
BindZipExtensions, BindZipExtensionsAsync Zips one result into another result-producing function, with sync/Task/ValueTask combinations and tuple arities
CheckExtensions, CheckExtensionsAsync Runs side-effect validations that return Result<T> (use Result<Unit> for no-payload validators) while preserving original success value
CheckIfExtensions, CheckIfExtensionsAsync Conditional Check variants
CombineExtensions, CombineExtensionsAsync, CombineErrorExtensions Combines results, including tuple and enumerable forms
DiscardExtensions, DiscardTaskExtensions, DiscardValueTaskExtensions Drops the Result<T> value entirely (returns void/Task/ValueTask) for intentional fire-and-forget pipelines
EnsureExtensions, EnsureExtensionsAsync, EnsureAllExtensions, EnsureAllExtensionsAsync Predicate-based validation on successful values; includes collection-wide validation
GetValueOrDefaultExtensions Non-throwing value fallback helpers
ResultLinqExtensions, ResultLinqExtensionsTaskAsync, ResultLinqExtensionsTaskLeftAsync, ResultLinqExtensionsTaskRightAsync, ResultLinqExtensionsValueTaskAsync, ResultLinqExtensionsValueTaskLeftAsync, ResultLinqExtensionsValueTaskRightAsync LINQ query syntax support via Select/SelectMany/Where for Result<T>, Task<Result<T>> and ValueTask<Result<T>> (mixed sync/async sources and continuations)
MapExtensions, MapExtensionsAsync, MapIfExtensions, MapOnFailureExtensions Success-path mapping, conditional mapping, and failure remapping; tuple overloads generated for arities 2-9
MatchExtensions, MatchExtensionsAsync, MatchTupleExtensions, MatchTupleExtensionsAsync Terminal branching for normal and tuple results. (The previous MatchErrorExtensions API was removed — use result.Match(_ => ..., e => e switch { Error.NotFound nf => ..., ... }) against the closed catalog.)
NullableExtensions, NullableExtensionsAsync Converts nullable reference/value types to Result<T>
RecoverExtensions, RecoverExtensionsAsync, RecoverOnFailureExtensions, RecoverOnFailureExtensionsAsync Converts failures into fallback success values or results
TapExtensions, TapExtensionsAsync, TapOnFailureExtensions, TapOnFailureExtensionsAsync Side effects on success or failure; tuple overloads generated for arities 2-9
ToMaybeExtensions, ToMaybeExtensionsAsync Converts Result<T> to Maybe<T>
TraverseExtensions, SequenceAllExtensions, TraverseAllExtensions Traverses collections through result-producing functions; *All variants accumulate failures via Error.Combine instead of short-circuiting
WhenExtensions, WhenExtensionsAsync, WhenAllExtensionsAsync Conditional execution and async fan-in utilities

Representative exact signatures:

public static Result<TResult> Bind<TValue, TResult>(this Result<TValue> result, Func<TValue, Result<TResult>> func)
public static Task<Result<TResult>> BindAsync<TValue, TResult>(this Result<TValue> result, Func<TValue, Task<Result<TResult>>> func)
public static Result<TOut> Map<TIn, TOut>(this Result<TIn> result, Func<TIn, TOut> map)
public static Result<TValue> Ensure<TValue>(this Result<TValue> result, Func<TValue, bool> predicate, Error error)
public static TOut Match<TIn, TOut>(this Result<TIn> result, Func<TIn, TOut> onSuccess, Func<Error, TOut> onFailure)
public static Result<T> ToResult<T>(this T? obj, Error error) where T : class
public static Maybe<T> ToMaybe<T>(this Result<T> result) where T : notnull
public static Result<TValue> Recover<TValue>(this Result<TValue> result, Func<Error, TValue> fallbackFunc)
public static Result<TValue> TapOnFailure<TValue>(this Result<TValue> result, Action<Error> action)

For tuple-enabled families, generated overloads cover the declared arity ranges shown above; no ValueTuple arities higher than 9 are public in this package.


Extension class catalog (full signatures)

The reference signatures below cover every Result*Extensions(Async) static class shipped by Trellis.Core. Each subsection lists the static class name(s), a methods table, and one representative usage example. All members live in the Trellis namespace.

Task adapter family — ResultTaskAdapterExtensions

Adapters for returning a synchronous Result<T> from an async-shaped API without target-typed new(...) wrappers.

Signature Returns Description
public static Task<Result<T>> AsTask<T>(this Result<T> result) Task<Result<T>> Wraps the exact result state in a completed Task.
public static ValueTask<Result<T>> AsValueTask<T>(this Result<T> result) ValueTask<Result<T>> Wraps the exact result state in a completed ValueTask.
public ValueTask<Result<OrderId>> Handle(CreateOrderCommand cmd, CancellationToken ct) =>
    OrderId.TryCreate(cmd.OrderId)
        .Bind(id => Order.Create(id))
        .Tap(repo.Add)
        .Map(order => order.Id)
        .AsValueTask();

Bind family — BindExtensions, BindExtensionsAsync, BindZipExtensions, BindZipExtensionsAsync

Sequential composition of result-producing functions. Bind is the monadic flatMap; BindZip keeps the upstream value in scope by zipping it into the next stage. For no-payload steps, return Result<Unit> and use _ to ignore the Unit argument in the next lambda.

Signature Returns Description
public static Result<TResult> Bind<TValue, TResult>(this Result<TValue> result, Func<TValue, Result<TResult>> func) Result<TResult> Generic-to-generic flatMap. Short-circuits on failure.
public static Task<Result<TResult>> BindAsync<TValue, TResult>(this Task<Result<TValue>> resultTask, Func<TValue, Task<Result<TResult>>> func) Task<Result<TResult>> All combinations of Result<T>/Task<Result<T>>/ValueTask<Result<T>> × sync-/async-lambda are exposed (12 overloads on BindExtensionsAsync).
public static Result<(T1, T2)> BindZip<T1, T2>(this Result<T1> result, Func<T1, Result<T2>> func) Result<(T1, T2)> Zips upstream value with the bind result so downstream stages see both. Tuple arities 2–9 are generated.
public static Task<Result<(T1, T2)>> BindZipAsync<T1, T2>(this Task<Result<T1>> resultTask, Func<T1, Task<Result<T2>>> func) Task<Result<(T1, T2)>> Async BindZip; generated for every Result/Task/ValueTask combination.
Result<Order> Place(OrderId id) =>
    LoadCustomer(id)
        .BindZip(c => LoadCart(c.Id))    // Result<(Customer, Cart)>
        .Bind((customer, cart) => Charge(customer, cart));

Trap — BindAsync Task vs ValueTask overload ambiguity (CS0121). When the lambda passed to BindAsync is an inline expression whose return type can't be inferred between Task<Result<R>> and ValueTask<Result<R>>, the compiler reports both overloads as candidates. Two reliable fixes:

  1. Named method with explicit return type — extract the lambda body to a method declared as private Task<Result<R>> NextStage(T value, CancellationToken ct) { ... } and pass NextStage (the method group resolves unambiguously).
  2. Typed local delegateFunc<T, Task<Result<R>>> next = c => ...; .BindAsync(next); forces the Task overload.

Avoid storing the lambda inline as var next = ...; because the inferred type may still be ambiguous.

Map family — MapExtensions, MapExtensionsAsync, MapIfExtensions, MapOnFailureExtensions

Pure transformation of the success value (or failure error). Use Map when the lambda returns a plain value; switch to Bind when it returns a Result.

Signature Returns Description
public static Result<TOut> Map<TIn, TOut>(this Result<TIn> result, Func<TIn, TOut> func) Result<TOut> Synchronous map on Result<T>.
public static Task<Result<TOut>> MapAsync<TIn, TOut>(this Task<Result<TIn>> resultTask, Func<TIn, Task<TOut>> func) Task<Result<TOut>> MapExtensionsAsync exposes all Task/ValueTask × sync-/async-lambda combinations (6 overloads).
public static Result<T> MapOnFailure<T>(this Result<T> result, Func<Error, Error> map) Result<T> Replaces the failure Error.
public static Task<Result<T>> MapOnFailureAsync<T>(this Task<Result<T>> resultTask, Func<Error, Task<Error>> mapAsync) Task<Result<T>> MapOnFailureExtensions exposes all sync/Task/ValueTask combinations of MapOnFailure/MapOnFailureAsync.
Task<Result<OrderDto>> Pipeline(OrderId id) =>
    LoadOrderAsync(id)
        .MapAsync(o => OrderDto.From(o))
        .MapOnFailureAsync(e => e is Error.NotFound ? new Error.Gone(ResourceRef.For<Order>(id)) : e);

Tap and TapOnFailure families — TapExtensions, TapExtensionsAsync, TapOnFailureExtensions, TapOnFailureExtensionsAsync

Side effects without altering the result. Tap runs on success; TapOnFailure runs on failure.

Signature Returns Description
public static Result<TValue> Tap<TValue>(this Result<TValue> result, Action<TValue> action) Result<TValue> Sync side effect on success.
public static Task<Result<TValue>> TapAsync<TValue>(this Task<Result<TValue>> resultTask, Func<TValue, Task> func) Task<Result<TValue>> TapExtensionsAsync covers all sync/Task/ValueTask × value-/no-value-lambda combinations (12 overloads).
public static Result<TValue> TapOnFailure<TValue>(this Result<TValue> result, Action<Error> action) Result<TValue> Sync side effect on failure.
public static Task<Result<TValue>> TapOnFailureAsync<TValue>(this Task<Result<TValue>> resultTask, Func<Error, Task> func) Task<Result<TValue>> TapOnFailureExtensionsAsync covers all sync/Task/ValueTask × error-/no-arg-lambda combinations (12 non-tuple overloads; tuple arities 2–9 generate the same set per arity).
Task<Result<Order>> Save(Order o) =>
    repo.SaveAsync(o)
        .TapAsync(saved => logger.LogInformationAsync($"saved {saved.Id}"))
        .TapOnFailureAsync(err => logger.LogWarningAsync($"failed: {err.Code}"));

Match family — MatchExtensions, MatchExtensionsAsync, MatchTupleExtensions, MatchTupleExtensionsAsync

Terminal branching: produce a value (Match) or run side effects (Switch).

Signature Returns Description
public static TOut Match<TIn, TOut>(this Result<TIn> result, Func<TIn, TOut> onSuccess, Func<Error, TOut> onFailure) TOut Sync match on Result<T>.
public static void Switch<TIn>(this Result<TIn> result, Action<TIn> onSuccess, Action<Error> onFailure) void Sync side-effect terminal.
public static Task<TOut> MatchAsync<TIn, TOut>(this Task<Result<TIn>> resultTask, Func<TIn, Task<TOut>> onSuccess, Func<Error, Task<TOut>> onFailure) Task<TOut> MatchExtensionsAsync covers all sync/Task/ValueTask × sync-/async-/cancellation-lambda combinations (~10 overloads).
public static Task SwitchAsync<TIn>(this Task<Result<TIn>> resultTask, Func<TIn, Task> onSuccess, Func<Error, Task> onFailure) Task SwitchAsync overloads cover Task and ValueTask, with optional CancellationToken variants.
public static Task<TOut> MatchAsync<T1, T2, TOut>(this Result<(T1, T2)> result, Func<T1, T2, Task<TOut>> onSuccess, Func<Error, Task<TOut>> onFailure) Task<TOut> MatchTupleExtensions (sync) and MatchTupleExtensionsAsync (async) generate MatchAsync / SwitchAsync for tuple arities 2–9.
IActionResult Render(Result<Order> r) =>
    r.Match(
        order => Ok(OrderDto.From(order)),
        err   => err switch
        {
            Error.NotFound nf            => NotFound(nf.Resource.Id),
            Error.UnprocessableContent u => UnprocessableEntity(u.Fields),
            _                            => Problem(err.GetDisplayMessage()),
        });

Recover family — RecoverExtensions, RecoverExtensionsAsync, RecoverOnFailureExtensions, RecoverOnFailureExtensionsAsync

Convert failures back into successes (Recover) or chain a fallback result-producing operation (RecoverOnFailure).

Signature Returns Description
public static Result<TValue> Recover<TValue>(this Result<TValue> result, Func<Error, TValue> fallbackFunc) Result<TValue> Three sync overloads on RecoverExtensions: constant fallback, Func<TValue>, Func<Error, TValue>.
public static Task<Result<TValue>> RecoverAsync<TValue>(this Task<Result<TValue>> resultTask, Func<Error, Task<TValue>> fallbackFunc) Task<Result<TValue>> RecoverExtensionsAsync covers all Task/ValueTask × sync-/async-lambda combinations.
public static Result<T> RecoverOnFailure<T>(this Result<T> result, Func<Error, Result<T>> func) Result<T> Four sync overloads on RecoverOnFailureExtensions: with/without Error argument, with/without predicate gate.
public static Task<Result<T>> RecoverOnFailureAsync<T>(this Task<Result<T>> resultTask, Func<Error, Task<Result<T>>> funcAsync) Task<Result<T>> RecoverOnFailureExtensionsAsync exposes ~16 overloads for Task/ValueTask × predicate-gated/ungated × value-/error-lambda.
Task<Result<Settings>> Load(UserId id) =>
    settingsRepo.LoadAsync(id)
        .RecoverOnFailureAsync(
            e => e is Error.NotFound,
            err => Task.FromResult(Result.Ok(Settings.Defaults)));

Ensure family — EnsureExtensions, EnsureExtensionsAsync, EnsureAllExtensions, EnsureAllExtensionsAsync

Predicate-based validation. Ensure short-circuits on the first failed predicate; EnsureAll accumulates every failure into a single Error.Aggregate for applicative-style validation.

Signature Returns Description
public static Result<TValue> Ensure<TValue>(this Result<TValue> result, Func<TValue, bool> predicate, Error error) Result<TValue> Sync ensure with predicate + Error. Five sync overloads (with/without value arg, factory error, embedded result).
public static Result<TValue> Ensure<TValue>(this Result<TValue> result, Func<TValue, bool> predicate, Func<TValue, Error> errorPredicate) Result<TValue> Sync ensure with lazy error factory.
public static Result<string> EnsureNotNullOrWhiteSpace(this string? str, Error error) Result<string> Lifts a possibly-blank string to Result<string>.
public static Result<T> EnsureNotNull<T>(this Result<T?> result, Error error) where T : class Result<T> Reference-type EnsureNotNull overload that strips the nullable annotation.
public static Result<T> EnsureNotNull<T>(this Result<T?> result, Error error) where T : struct Result<T> Value-type EnsureNotNull overload that unwraps the nullable.
public static Task<Result<TValue>> EnsureAsync<TValue>(this Task<Result<TValue>> resultTask, Func<TValue, Task<bool>> predicate, Error error) Task<Result<TValue>> EnsureExtensionsAsync covers all Result<T>/Task<Result<T>>/ValueTask<Result<T>> receivers × Func<TValue, bool>/Task<bool>/ValueTask<bool> predicates × constant-, factory-, async-factory- and embedded-Result<TValue> error producers (~34 overloads across the six Ensure.* partial files).
public static Result<TValue> EnsureAll<TValue>(this Result<TValue> result, params (Func<TValue, bool> predicate, Error error)[] checks) Result<TValue> Applicative validation: runs every check and folds failures via error.Combine(...) into one Error.Aggregate.
public static Task<Result<TValue>> EnsureAllAsync<TValue>(this Task<Result<TValue>> resultTask, params (Func<TValue, bool> predicate, Error error)[] checks) Task<Result<TValue>> Task overload of EnsureAllAsync.
public static ValueTask<Result<TValue>> EnsureAllAsync<TValue>(this ValueTask<Result<TValue>> resultTask, params (Func<TValue, bool> predicate, Error error)[] checks) ValueTask<Result<TValue>> ValueTask overload of EnsureAllAsync.
Result<Quote> Validate(Quote q) =>
    Result.Ok(q).EnsureAll(
        (x => x.Total > 0,            Error.UnprocessableContent.ForField("total", "must_be_positive")),
        (x => x.Currency.Length == 3, Error.UnprocessableContent.ForField("currency", "iso4217")));

Result<string> NotBlank(string? raw) =>
    raw.EnsureNotNullOrWhiteSpace(new Error.BadRequest("blank", InputPointer.Root));

Check / CheckIf families — CheckExtensions, CheckExtensionsAsync, CheckIfExtensions, CheckIfExtensionsAsync

Run a side-effect validator that returns its own Result/Result<TK> while preserving the upstream success value. CheckIf adds a conditional gate.

Signature Returns Description
public static Result<T> Check<T, TK>(this Result<T> result, Func<T, Result<TK>> func) Result<T> Validator returning Result<TK>; original value is preserved on success. For no-payload validators, return Result<Unit>.
public static Task<Result<T>> CheckAsync<T, TK>(this Task<Result<T>> resultTask, Func<T, Task<Result<TK>>> func) Task<Result<T>> CheckExtensionsAsync covers all Task/ValueTask combinations.
public static Result<T> CheckIf<T, TK>(this Result<T> result, bool condition, Func<T, Result<TK>> func) Result<T> Boolean-gated check; runs only when condition is true.
public static Result<T> CheckIf<T, TK>(this Result<T> result, Func<T, bool> predicate, Func<T, Result<TK>> func) Result<T> Predicate-gated check.
public static Task<Result<T>> CheckIfAsync<T, TK>(this Task<Result<T>> resultTask, bool condition, Func<T, Task<Result<TK>>> func) Task<Result<T>> CheckIfExtensionsAsync covers all Task/ValueTask × bool-/predicate-gated combinations.
Result<Quote> q = Result.Ok(quote)
    .Check(QuoteValidators.AllItemsInStock)
    .CheckIf(quote.IsExpedited, QuoteValidators.HonorsCutoff);

Combine family — CombineExtensions, CombineExtensionsAsync, CombineErrorExtensions

Aggregates results into tuples (success-track) or merges errors via Error.Aggregate (failure-track). Tuple arities 2–9 are generated.

Signature Returns Description
public static Result<(T1, T2)> Combine<T1, T2>(this Result<T1> t1, Result<T2> t2) Result<(T1, T2)> Tuple combine; failures fold via Error.Aggregate. When either operand is Result<Unit>, Unit becomes the next tuple element (use _ in the destructuring lambda to ignore it).
public static Task<Result<(T1, T2)>> CombineAsync<T1, T2>(this Task<Result<T1>> tt1, Task<Result<T2>> tt2) Task<Result<(T1, T2)>> CombineExtensionsAsync covers every Task/ValueTask × Task/ValueTask combination.
public static Error Combine(this Error? left, Error right) Error On CombineErrorExtensions: combines two errors into an Error.Aggregate, flattening nested aggregates and treating null left as right.
return Result.Combine(streetCity, contact)
    .Map(_ => new Address(cmd.Street, cmd.City));

Discard family — DiscardExtensions, DiscardTaskExtensions, DiscardValueTaskExtensions

Drop the success value entirely (returns void/Task/ValueTask) for intentional fire-and-forget pipelines.

Signature Returns Description
public static void Discard<T>(this Result<T> result) void Documents intent that the success value is intentionally ignored.
public static Task DiscardAsync<T>(this Task<Result<T>> resultTask) Task Awaits and discards; on DiscardTaskExtensions.
public static ValueTask DiscardAsync<T>(this ValueTask<Result<T>> resultTask) ValueTask ValueTask variant on DiscardValueTaskExtensions.
await SendEmailAsync(msg).DiscardAsync(); // intentionally fire-and-forget the value

AsUnit family — AsUnitExtensions

Async wrappers around Result<T>.AsUnit() that strip the value while preserving success/failure state.

Signature Returns Description
public static Task<Result<Unit>> AsUnitAsync<T>(this Task<Result<T>> resultTask) Task<Result<Unit>> Awaits and projects to Result<Unit>.
public static ValueTask<Result<Unit>> AsUnitAsync<T>(this ValueTask<Result<T>> resultTask) ValueTask<Result<Unit>> ValueTask variant.
Task<Result<Unit>> done = pipeline.RunAsync(input).AsUnitAsync();

Debug family — ResultDebugExtensions, ResultDebugExtensionsAsync

Non-allocating diagnostic taps gated by ResultDebugSettings. They never alter the result; they only emit through Debug.WriteLine / configured sinks.

Signature Returns Description
public static Result<TValue> Debug<TValue>(this Result<TValue> result, string message = "") Result<TValue> Logs success or failure with the optional message.
public static Result<TValue> DebugDetailed<TValue>(this Result<TValue> result, string message = "") Result<TValue> Includes the success value and full error in the log.
public static Result<TValue> DebugWithStack<TValue>(this Result<TValue> result, string message = "", bool includeStackTrace = true) Result<TValue> Adds the current stack trace.
public static Result<TValue> DebugOnSuccess<TValue>(this Result<TValue> result, Action<TValue> action) Result<TValue> Custom sink invoked only on success.
public static Result<TValue> DebugOnFailure<TValue>(this Result<TValue> result, Action<Error> action) Result<TValue> Custom sink invoked only on failure.
public static Task<Result<TValue>> DebugAsync<TValue>(this Task<Result<TValue>> resultTask, string message = "") Task<Result<TValue>> ResultDebugExtensionsAsync mirrors every sync overload (DebugDetailedAsync, DebugWithStackAsync, DebugOnSuccessAsync, DebugOnFailureAsync) for Task<Result<T>> — including Func<T, Task> / Func<Error, Task> async sinks.
return await LoadAsync(id)
    .DebugAsync("after-load")
    .BindAsync(ChargeAsync)
    .DebugDetailedAsync("after-charge");

LINQ query-syntax family — ResultLinqExtensions, ResultLinqExtensionsTaskAsync, ResultLinqExtensionsTaskLeftAsync, ResultLinqExtensionsTaskRightAsync, ResultLinqExtensionsValueTaskAsync, ResultLinqExtensionsValueTaskLeftAsync, ResultLinqExtensionsValueTaskRightAsync

LINQ query expression support for Result<T>, Task<Result<T>>, and ValueTask<Result<T>>. The async overloads let from ... in ... clauses chain async result-producing operations directly — without await-ing each step into a sync block. Failures short-circuit subsequent steps with the same semantics as Bind / Map / Ensure.

Signature Returns Description
public static Result<TOut> Select<TIn, TOut>(this Result<TIn> result, Func<TIn, TOut> selector) Result<TOut> Sync Select — projects a successful value (delegates to Map).
public static Result<TResult> SelectMany<TSource, TCollection, TResult>(this Result<TSource> source, Func<TSource, Result<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector) Result<TResult> Sync SelectMany — enables multi-from chains.
public static Result<TSource> Where<TSource>(this Result<TSource> source, Func<TSource, bool> predicate) Result<TSource> Sync Where — converts to a generic "filtered out" failure when the predicate is false. Prefer Ensure for meaningful errors.
public static Task<Result<TOut>> Select<TIn, TOut>(this Task<Result<TIn>> resultTask, Func<TIn, TOut> selector) Task<Result<TOut>> Select over an async receiver.
public static Task<Result<TResult>> SelectMany<TSource, TCollection, TResult>(this Task<Result<TSource>> source, Func<TSource, Task<Result<TCollection>>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector) Task<Result<TResult>> SelectMany — async source, async continuation.
public static Task<Result<TResult>> SelectMany<TSource, TCollection, TResult>(this Task<Result<TSource>> source, Func<TSource, Result<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector) Task<Result<TResult>> SelectMany — async source, sync continuation (.Left).
public static Task<Result<TResult>> SelectMany<TSource, TCollection, TResult>(this Result<TSource> source, Func<TSource, Task<Result<TCollection>>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector) Task<Result<TResult>> SelectMany — sync source, async continuation (.Right).
public static Task<Result<TSource>> Where<TSource>(this Task<Result<TSource>> source, Func<TSource, bool> predicate) Task<Result<TSource>> Where over an async receiver.
public static ValueTask<Result<TOut>> Select<TIn, TOut>(this ValueTask<Result<TIn>> resultTask, Func<TIn, TOut> selector) ValueTask<Result<TOut>> Select over a ValueTask receiver.
public static ValueTask<Result<TResult>> SelectMany<TSource, TCollection, TResult>(this ValueTask<Result<TSource>> source, Func<TSource, ValueTask<Result<TCollection>>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector) ValueTask<Result<TResult>> SelectManyValueTask source and continuation.
public static ValueTask<Result<TResult>> SelectMany<TSource, TCollection, TResult>(this ValueTask<Result<TSource>> source, Func<TSource, Result<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector) ValueTask<Result<TResult>> SelectManyValueTask source, sync continuation (.Left).
public static ValueTask<Result<TResult>> SelectMany<TSource, TCollection, TResult>(this Result<TSource> source, Func<TSource, ValueTask<Result<TCollection>>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector) ValueTask<Result<TResult>> SelectMany — sync source, ValueTask continuation (.Right).
public static ValueTask<Result<TSource>> Where<TSource>(this ValueTask<Result<TSource>> source, Func<TSource, bool> predicate) ValueTask<Result<TSource>> Where over a ValueTask receiver.
// All-async LINQ — Task<Result<T>> participates in query syntax directly.
var orderDto = await (
    from user  in GetUserAsync(id)         // Task<Result<User>>
    from order in GetOrderAsync(user)      // Task<Result<Order>>
    select new OrderDto(user, order));

// Mixed sync/async — sync source flows into an async continuation (.Right).
var summary = await (
    from u in LoadCachedUser(id)           // Result<User>
    from o in FetchOrderAsync(u)           // Task<Result<Order>>
    select new Summary(u, o));

// And the reverse — async source with a sync validation step (.Left).
var validated = await (
    from u in LoadUserAsync(id)            // Task<Result<User>>
    from p in ValidatePermissions(u)       // Result<Permissions>
    select new Authorized(u, p));

CancellationToken pattern. Closure-capture a CancellationToken from the surrounding method and call ct.ThrowIfCancellationRequested() inside any async selector that needs to honor cancellation; the query expression itself does not introduce a token parameter.

Exceptions. Exceptions thrown inside selectors propagate through the await (matching BindAsync / MapAsync semantics). They are not converted to Result.Fail.

Traverse — TraverseExtensions

Folds a sequence of inputs through a Result-producing selector into a single Result<IReadOnlyList<TOut>>.

Signature Returns Description
public static Result<IReadOnlyList<TOut>> Traverse<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, Result<TOut>> selector) Result<IReadOnlyList<TOut>> Sync traversal; short-circuits on the first failure.
public static Task<Result<IReadOnlyList<TOut>>> TraverseAsync<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, Task<Result<TOut>>> selector) Task<Result<IReadOnlyList<TOut>>> Async traversal; sequential evaluation.
public static Task<Result<IReadOnlyList<TOut>>> TraverseAsync<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, CancellationToken, Task<Result<TOut>>> selector, CancellationToken cancellationToken = default) Task<Result<IReadOnlyList<TOut>>> Cancellation-token overload.
public static ValueTask<Result<IReadOnlyList<TOut>>> TraverseAsync<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, ValueTask<Result<TOut>>> selector) ValueTask<Result<IReadOnlyList<TOut>>> ValueTask variant.
public static ValueTask<Result<IReadOnlyList<TOut>>> TraverseAsync<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, CancellationToken, ValueTask<Result<TOut>>> selector, CancellationToken cancellationToken = default) ValueTask<Result<IReadOnlyList<TOut>>> ValueTask + cancellation-token variant.
public static Task<Result<Unit>> TraverseAsync<TIn>(this IEnumerable<TIn> source, Func<TIn, CancellationToken, Task<Result<Unit>>> selector, CancellationToken cancellationToken = default) Task<Result<Unit>> No-payload selector overload — short-circuits on the first failure and returns Result<Unit> for void-flavoured fan-out.
public static Result<IReadOnlyList<T>> Sequence<T>(this IEnumerable<Result<T>> source) Result<IReadOnlyList<T>> Identity-selector form of Traverse. Lifts an IEnumerable<Result<T>> to Result<IReadOnlyList<T>>; short-circuits on the first failure.
public static Result<Unit> Sequence(this IEnumerable<Result<Unit>> source) Result<Unit> No-payload Sequence overload for void-flavoured pipelines; short-circuits on the first failure.
Task<Result<IReadOnlyList<Order>>> orders =
    ids.TraverseAsync((id, ct) => repo.LoadAsync(id, ct), cancellationToken);

// Sequence: when you already have IEnumerable<Result<T>> from a Select.
Result<IReadOnlyList<Money>> subtotals =
    lineItems.Select(item => item.ComputeSubtotal()).Sequence();

TraverseAll / SequenceAll — TraverseAllExtensions, SequenceAllExtensions

Accumulating-error counterparts to Traverse / Sequence. Run the selector over every item (no short-circuit) and fold failures via the existing Error.Combine extension. A single failure returns unchanged (no Error.Aggregate wrap); multiple UnprocessableContent failures merge their fields/rules; heterogeneous failures flatten into Error.Aggregate. Use these when you need to surface every failure (form-style validation) rather than the first.

Signature Returns Description
public static Result<IReadOnlyList<TOut>> TraverseAll<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, Result<TOut>> selector) Result<IReadOnlyList<TOut>> Accumulating sync traversal; folds every failure via Error.Combine.
public static Task<Result<IReadOnlyList<TOut>>> TraverseAllAsync<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, Task<Result<TOut>>> selector) Task<Result<IReadOnlyList<TOut>>> Accumulating async traversal; selectors are awaited sequentially.
public static Task<Result<IReadOnlyList<TOut>>> TraverseAllAsync<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, CancellationToken, Task<Result<TOut>>> selector, CancellationToken cancellationToken = default) Task<Result<IReadOnlyList<TOut>>> Accumulating async traversal with cancellation; mirrors TraverseAsync shape.
public static ValueTask<Result<IReadOnlyList<TOut>>> TraverseAllAsync<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, ValueTask<Result<TOut>>> selector) ValueTask<Result<IReadOnlyList<TOut>>> Accumulating ValueTask traversal for zero-allocation scenarios.
public static ValueTask<Result<IReadOnlyList<TOut>>> TraverseAllAsync<TIn, TOut>(this IEnumerable<TIn> source, Func<TIn, CancellationToken, ValueTask<Result<TOut>>> selector, CancellationToken cancellationToken = default) ValueTask<Result<IReadOnlyList<TOut>>> Accumulating ValueTask traversal with cancellation.
public static Task<Result<Unit>> TraverseAllAsync<TIn>(this IEnumerable<TIn> source, Func<TIn, CancellationToken, Task<Result<Unit>>> selector, CancellationToken cancellationToken = default) Task<Result<Unit>> Accumulating Result<Unit> traversal with cancellation; void-flavoured pipelines.
public static Result<IReadOnlyList<T>> SequenceAll<T>(this IEnumerable<Result<T>> source) Result<IReadOnlyList<T>> Identity-selector accumulating sequence; visits every item, folds failures.
public static Result<Unit> SequenceAll(this IEnumerable<Result<Unit>> source) Result<Unit> Accumulating Sequence over Result<Unit> for void-flavoured pipelines.
// Form-style validation: collect every field error in one pass.
Result<IReadOnlyList<EmailAddress>> emails =
    raw.TraverseAll(EmailAddress.TryCreate);
//   ↳ on multiple invalid entries, returns one Error.UnprocessableContent
//     whose Fields/Rules concatenate every per-item violation.

// Heterogeneous failures flatten into Error.Aggregate:
Result<IReadOnlyList<Order>> orders =
    operations.SequenceAll();   // Result<NotFound> + Result<Conflict> → Error.Aggregate

TraverseAll matches Traverse's full async surface: sync, Task, Task + CancellationToken, ValueTask, ValueTask + CancellationToken, plus a Task<Result<Unit>> + CancellationToken overload. SequenceAll is sync-only because the existing Sequence is sync-only; if Sequence ever gains async siblings, SequenceAll follows at the same time.

When / WhenAll — WhenExtensions, WhenExtensionsAsync, WhenAllExtensionsAsync

Conditional execution and async fan-in.

Signature Returns Description
public static Result<T> When<T>(this Result<T> result, Func<T, bool> predicate, Func<T, Result<T>> operation) Result<T> Runs operation only when the predicate holds.
public static Result<T> Unless<T>(this Result<T> result, Func<T, bool> predicate, Func<T, Result<T>> operation) Result<T> Inverse of When.
public static Task<Result<T>> WhenAsync<T>(this Result<T> result, Func<T, bool> predicate, Func<T, Task<Result<T>>> operation) Task<Result<T>> WhenExtensionsAsync covers Task/ValueTask × predicate-/no-predicate × Result/Task-Result combinations for both WhenAsync and UnlessAsync.
public static Task<Result<T>> UnlessAsync<T>(this Task<Result<T>> resultTask, Func<T, Task<Result<T>>> operation) Task<Result<T>> Async inverse-When.
public static Task<Result<(T1, T2)>> WhenAllAsync<T1, T2>(this (Task<Result<T1>> t1, Task<Result<T2>> t2) tasks) Task<Result<(T1, T2)>> WhenAllExtensionsAsync runs tasks concurrently via Task.WhenAll and folds the results. Tuple arities 2–9 are generated.
Task<Result<(Profile, Preferences)>> bundle =
    (LoadProfileAsync(id), LoadPreferencesAsync(id)).WhenAllAsync();

ToMaybe — ToMaybeExtensions, ToMaybeExtensionsAsync

Project a Result<T> to a Maybe<T> (failure → None).

Signature Returns Description
public static Maybe<TValue> ToMaybe<TValue>(this Result<TValue> result) where TValue : notnull Maybe<TValue> Sync projection.
public static Task<Maybe<TValue>> ToMaybeAsync<TValue>(this Task<Result<TValue>> resultTask) where TValue : notnull Task<Maybe<TValue>> Awaits and projects.
public static ValueTask<Maybe<TValue>> ToMaybeAsync<TValue>(this ValueTask<Result<TValue>> resultTask) where TValue : notnull ValueTask<Maybe<TValue>> ValueTask variant.
Maybe<Order> maybe = await repo.TryLoadAsync(id).ToMaybeAsync();

Pagination

Cursor-based pagination primitives. Cursor is opaque to clients; servers choose the encoding. Page<T> couples items with adjacent cursors and observable server-side limit clamping.

public readonly record struct Cursor

public readonly record struct Cursor
{
    public Cursor(string token);
    public string Token { get; }
}
Member Description
Cursor(string token) Constructs a cursor; throws ArgumentException if token is null or empty.
Token The opaque continuation token. Server-defined encoding; clients must echo it back unchanged.

Absence of a cursor is represented by null (Cursor?). There is no "empty cursor" — a constructed Cursor always carries a non-empty token.

public readonly record struct Page<T>

public readonly record struct Page<T>
{
    public Page(
        IReadOnlyList<T> items,
        Cursor? next,
        Cursor? previous,
        int requestedLimit,
        int appliedLimit);

    public IReadOnlyList<T> Items { get; }
    public Cursor?          Next { get; }
    public Cursor?          Previous { get; }
    public int              RequestedLimit { get; }
    public int              AppliedLimit { get; }
    public int              DeliveredCount { get; }
    public bool             WasCapped { get; }
}
Member Description
Page(IReadOnlyList<T>, Cursor?, Cursor?, int, int) Validated constructor. Throws ArgumentNullException on null Items, ArgumentOutOfRangeException on a non-positive limit or AppliedLimit > RequestedLimit.
Items The items returned for this page. Never null when constructed via the public ctor.
Next Cursor for the next page, or null on the last page.
Previous Cursor for the previous page, or null on the first page (or when the source doesn't support reverse).
RequestedLimit The limit the client requested.
AppliedLimit The limit the server actually applied (after server-side cap).
DeliveredCount Items.Count, defensive against default(Page<T>) (returns 0 when Items is null).
WasCapped true when AppliedLimit < RequestedLimit.

public static class Page

Non-generic factory companion (mirrors the Result / Result<T> split — keeps the generic surface minimal per CA1000).

Signature Returns Description
public static Page<T> Empty<T>(int requestedLimit, int appliedLimit) Page<T> An empty page (no items, no cursors) for the supplied limits.

Wire shape. Trellis.Asp projects Page<T> to 200 OK with a JSON body envelope and a co-emitted Link header (RFC 8288). See HttpResponseExtensions.ToHttpResponse for the Result<Page<T>> overload. Trellis intentionally does not use 206 Partial Content for collection pagination — RFC 9110 §14 was designed for byte-range transfer and lacks proxy/CDN support for collection ranges.

public Task<Result<Page<Order>>> List(string? cursorToken, int limit, CancellationToken ct) =>
    repo.ListAsync(cursorToken is null ? null : new Cursor(cursorToken), limit, ct)
        .MapAsync(rows => new Page<Order>(
            Items: rows.Items,
            Next: rows.HasMore ? new Cursor(rows.NextToken!) : null,
            Previous: rows.PrevToken is null ? null : new Cursor(rows.PrevToken),
            RequestedLimit: limit,
            AppliedLimit: Math.Min(limit, MaxLimit)));

Error Cases (closed ADT)

Case Constructor Default Code HTTP Status
Error.BadRequest (string ReasonCode, InputPointer? At = null) ReasonCode 400
Error.Unauthorized (EquatableArray<AuthChallenge> Challenges = default) unauthorized 401
Error.Forbidden (string PolicyId, ResourceRef? Resource = null) PolicyId 403
Error.NotFound (ResourceRef Resource) not-found 404
Error.MethodNotAllowed (EquatableArray<string> Allow) method-not-allowed 405
Error.NotAcceptable (EquatableArray<string> Available) not-acceptable 406
Error.Conflict (ResourceRef? Resource, string ReasonCode) ReasonCode 409
Error.Gone (ResourceRef Resource) gone 410
Error.PreconditionFailed (ResourceRef Resource, PreconditionKind Condition) precondition-failed 412
Error.ContentTooLarge (long? MaxBytes = null) content-too-large 413
Error.UnsupportedMediaType (EquatableArray<string> Supported) unsupported-media-type 415
Error.RangeNotSatisfiable (long CompleteLength, string Unit = "bytes") range-not-satisfiable 416
Error.UnprocessableContent (EquatableArray<FieldViolation> Fields, EquatableArray<RuleViolation> Rules = default) unprocessable-content 422
Error.PreconditionRequired (PreconditionKind Condition) precondition-required 428
Error.TooManyRequests (RetryAfterValue? RetryAfter = null) too-many-requests 429
Error.InternalServerError (string FaultId) FaultId 500
Error.Unexpected (string ReasonCode) ReasonCode 500
Error.NotImplemented (string Feature) Feature 501
Error.ServiceUnavailable (RetryAfterValue? RetryAfter = null) service-unavailable 503
Error.Aggregate (EquatableArray<Error> Errors) aggregate depends on contained errors; serialized via ProblemDetails.Extensions["errors"] (RFC 9457)

Examples

Result flow

using Trellis;

Result<int> Divide(int left, int right) =>
    Result.Ensure(right != 0, new Error.BadRequest("right_must_not_be_zero")
        { Detail = "Right operand must not be zero" })
        .Map(_ => left / right);

Maybe to Result

using Trellis;

Maybe<string> maybeEmail = Maybe.From("user@example.com");

Result<string> emailResult = maybeEmail.ToResult(
    Error.UnprocessableContent.ForField("email", "required", "Email is required"));

Reading errors without throwing

using Trellis;

Result<Order> result = await mediator.SendAsync(new PlaceOrder(...));

// Pattern-matching: result.Error is null on success, never throws
if (!result.TryGetValue(out var order, out var error))
{
    return error switch
    {
        Error.NotFound nf            => NotFound(nf.Resource.Id),
        Error.UnprocessableContent uc => UnprocessableEntity(uc.Fields),
        Error.Conflict c             => Conflict(c.ReasonCode),
        _                            => Problem(error.GetDisplayMessage()),
    };
}

return Ok(order);

Multi-field validation

using Trellis;

var streetCity = MaybeInvariant.AllOrNone(cmd.Street, cmd.City, "street", "city");
var contact    = MaybeInvariant.ExactlyOne(cmd.Email, cmd.Phone, "email", "phone");

// Combine merges any UnprocessableContent.Fields/Rules from multiple results
return Result.Combine(streetCity, contact)
    .Map(_ => new Address(cmd.Street, cmd.City));

Domain-Driven Design

The DDD primitives (Aggregate<T>, Entity<T>, ValueObject, Specification<T>, ...) live in Trellis.Core. They share the Trellis namespace.

Types

IEntity

public interface IEntity
Name Type Description
CreatedAt DateTimeOffset UTC timestamp for the first successful persistence of the entity.
LastModified DateTimeOffset UTC timestamp for the latest successful persistence update.
Signature Returns Description
No methods.

Entity<TId>

public abstract class Entity<TId> : IEntity where TId : notnull
Name Type Description
Id TId Immutable identity value for the entity.
CreatedAt DateTimeOffset Infrastructure-managed creation timestamp.
LastModified DateTimeOffset Infrastructure-managed last-modified timestamp.
Signature Returns Description
protected Entity(TId id) Initializes the entity identity.
public override bool Equals(object? obj) bool Returns true for the same reference before checking default IDs; otherwise compares exact runtime type and non-default IDs.
public static bool operator ==(Entity<TId>? a, Entity<TId>? b) bool Identity-based equality operator.
public static bool operator !=(Entity<TId>? a, Entity<TId>? b) bool Identity-based inequality operator.
public override int GetHashCode() int Combines runtime type and Id.

IAggregate

public interface IAggregate : IChangeTracking
Name Type Description
ETag string Optimistic concurrency token for the aggregate.
IsChanged bool Inherited from IChangeTracking; implemented by Aggregate<TId> as domain-event-based change tracking by default.
Signature Returns Description
IReadOnlyList<IDomainEvent> UncommittedEvents() IReadOnlyList<IDomainEvent> Returns the domain events raised since the last AcceptChanges().
void AcceptChanges() void Inherited from IChangeTracking; marks the aggregate as committed.

Aggregate<TId>

public abstract class Aggregate<TId> : Entity<TId>, IAggregate where TId : notnull
Name Type Description
DomainEvents List<IDomainEvent> Protected mutable event buffer for derived aggregate methods.
ETag string Persistence-managed optimistic concurrency token.
IsChanged bool [JsonIgnore] virtual change-tracking flag; default implementation is DomainEvents.Count > 0.
Signature Returns Description
protected Aggregate(TId id) Initializes the aggregate identity.
public IReadOnlyList<IDomainEvent> UncommittedEvents() IReadOnlyList<IDomainEvent> Returns a read-only snapshot of current domain events.
public void AcceptChanges() void Clears DomainEvents.

IDomainEvent

public interface IDomainEvent
Name Type Description
OccurredAt DateTimeOffset Timestamp (with explicit UTC offset) for when the domain event occurred.
Signature Returns Description
No methods.

ValueObject

public abstract class ValueObject : IComparable<ValueObject>, IComparable, IEquatable<ValueObject>
Name Type Description
No public or protected properties. Equality and ordering are driven by methods.
Signature Returns Description
protected abstract IEnumerable<IComparable?> GetEqualityComponents() IEnumerable<IComparable?> Returns the ordered components used for equality, comparison, and hash-code generation.
protected static IComparable? MaybeComponent<T>(Maybe<T> maybe) where T : notnull, IComparable IComparable? Converts Maybe<T> to an equality component by returning the inner value or null.
public override bool Equals(object? obj) bool Delegates to Equals(ValueObject? other).
public bool Equals(ValueObject? other) bool Structural equality check against the same runtime type.
public override int GetHashCode() int Computes and caches a hash code from the equality components.
public virtual int CompareTo(ValueObject? other) int Compares equality components in order.
public static bool operator ==(ValueObject? a, ValueObject? b) bool Structural equality operator.
public static bool operator !=(ValueObject? a, ValueObject? b) bool Structural inequality operator.
public static bool operator <(ValueObject? left, ValueObject? right) bool Ordering operator based on CompareTo(ValueObject?).
public static bool operator <=(ValueObject? left, ValueObject? right) bool Ordering operator based on CompareTo(ValueObject?).
public static bool operator >(ValueObject? left, ValueObject? right) bool Ordering operator based on CompareTo(ValueObject?).
public static bool operator >=(ValueObject? left, ValueObject? right) bool Ordering operator based on CompareTo(ValueObject?).

ScalarValueObject<TSelf, T>

public abstract class ScalarValueObject<TSelf, T> : ValueObject, IConvertible, IFormattable
where TSelf : ScalarValueObject<TSelf, T>, IScalarValue<TSelf, T>
where T : IComparable
Name Type Description
Value T Wrapped scalar value.
Signature Returns Description
protected ScalarValueObject(T value) Stores the wrapped scalar value.
protected override IEnumerable<IComparable?> GetEqualityComponents() IEnumerable<IComparable?> Default scalar equality uses only Value.
public override string ToString() string Returns Value?.ToString() ?? string.Empty.
public static implicit operator T(ScalarValueObject<TSelf, T> valueObject) T Unwraps the scalar value object to its primitive value.
public static TSelf Create(T value) TSelf Calls TSelf.TryCreate(value) and throws InvalidOperationException on failure.
public TypeCode GetTypeCode() TypeCode Returns Type.GetTypeCode(typeof(T)).
public bool ToBoolean(IFormatProvider? provider) bool Converts Value with Convert.ToBoolean.
public byte ToByte(IFormatProvider? provider) byte Converts Value with Convert.ToByte.
public char ToChar(IFormatProvider? provider) char Converts Value with Convert.ToChar.
public DateTime ToDateTime(IFormatProvider? provider) DateTime Converts Value with Convert.ToDateTime.
public decimal ToDecimal(IFormatProvider? provider) decimal Converts Value with Convert.ToDecimal.
public double ToDouble(IFormatProvider? provider) double Converts Value with Convert.ToDouble.
public short ToInt16(IFormatProvider? provider) short Converts Value with Convert.ToInt16.
public int ToInt32(IFormatProvider? provider) int Converts Value with Convert.ToInt32.
public long ToInt64(IFormatProvider? provider) long Converts Value with Convert.ToInt64.
public sbyte ToSByte(IFormatProvider? provider) sbyte Converts Value with Convert.ToSByte.
public float ToSingle(IFormatProvider? provider) float Converts Value with Convert.ToSingle.
public string ToString(IFormatProvider? provider) string Converts Value with Convert.ToString.
public string ToString(string? format, IFormatProvider? formatProvider) string Uses IFormattable when the wrapped value supports it; otherwise uses Convert.ToString.
public object ToType(Type conversionType, IFormatProvider? provider) object Converts Value to an arbitrary type via Convert.ChangeType.
public ushort ToUInt16(IFormatProvider? provider) ushort Converts Value with Convert.ToUInt16.
public uint ToUInt32(IFormatProvider? provider) uint Converts Value with Convert.ToUInt32.
public ulong ToUInt64(IFormatProvider? provider) ulong Converts Value with Convert.ToUInt64.

AggregateETagExtensions

public static class AggregateETagExtensions
Name Type Description
No public properties.
Signature Returns Description
public static Result<T> OptionalETag<T>(this Result<T> result, EntityTagValue[]? expectedETags) where T : IAggregate Result<T> If expectedETags is null, returns the original result unchanged; otherwise enforces strong ETag matching.
public static Result<T> RequireETag<T>(this Result<T> result, EntityTagValue[]? expectedETags) where T : IAggregate Result<T> Requires an If-Match value and enforces strong ETag matching.
public static Task<Result<T>> OptionalETagAsync<T>(this Task<Result<T>> resultTask, EntityTagValue[]? expectedETags) where T : IAggregate Task<Result<T>> Async Task wrapper for OptionalETag<T>.
public static ValueTask<Result<T>> OptionalETagAsync<T>(this ValueTask<Result<T>> resultTask, EntityTagValue[]? expectedETags) where T : IAggregate ValueTask<Result<T>> Async ValueTask wrapper for OptionalETag<T>.
public static Task<Result<T>> RequireETagAsync<T>(this Task<Result<T>> resultTask, EntityTagValue[]? expectedETags) where T : IAggregate Task<Result<T>> Async Task wrapper for RequireETag<T>.
public static ValueTask<Result<T>> RequireETagAsync<T>(this ValueTask<Result<T>> resultTask, EntityTagValue[]? expectedETags) where T : IAggregate ValueTask<Result<T>> Async ValueTask wrapper for RequireETag<T>.

Specification<T>

public abstract class Specification<T>
Name Type Description
CacheCompilation bool Protected virtual switch that controls whether IsSatisfiedBy(T entity) reuses a lazily compiled delegate.
Signature Returns Description
protected Specification() Initializes the lazy compiled delegate cache.
public abstract Expression<Func<T, bool>> ToExpression() Expression<Func<T, bool>> Returns the canonical expression tree for the specification.
public bool IsSatisfiedBy(T entity) bool Evaluates the specification in memory.
public Specification<T> And(Specification<T> other) Specification<T> Returns a composed AND specification.
public Specification<T> Or(Specification<T> other) Specification<T> Returns a composed OR specification.
public Specification<T> Not() Specification<T> Returns a negated specification.
public static implicit operator Expression<Func<T, bool>>(Specification<T> spec) Expression<Func<T, bool>> Converts the specification directly to its expression tree.

TrellisJsonValidationException

namespace Trellis;

public sealed class TrellisJsonValidationException : System.Text.Json.JsonException
Signature Returns Description
public TrellisJsonValidationException() Default constructor.
public TrellisJsonValidationException(string message) Creates an instance with a curated, user-safe message.
public TrellisJsonValidationException(string message, Exception innerException) Wraps an inner exception with the supplied message.

Marker subclass of System.Text.Json.JsonException thrown by Trellis JSON converters when a structured value object's invariants are violated during deserialization (e.g., CompositeValueObjectJsonConverter<Money> rejecting a negative amount). Trellis.Asp's ScalarValueValidationMiddleware recognizes this subtype and surfaces its Message and JsonException.Path in the resulting Problem Details payload, restoring DX parity with MVC's per-field model-binder error reporting. Plain JsonException instances are deliberately not surfaced because their messages can include internal type names; converters opt in to message surfacing by throwing this subclass with a curated message (e.g., error.GetDisplayMessage() from a Result failure).

Primitive value object base classes

These types ship in Trellis.Core. They are the building blocks for strongly-typed primitive value objects — derive a partial class from one of the Required*<TSelf> bases and the bundled Trellis.Core.Generator source generator emits the TryCreate / Create / Parse / TryParse / JsonConverter boilerplate. The validation attributes (StringLengthAttribute, RangeAttribute, EnumValueAttribute) attach declarative invariants that the generator wires into the generated validation. The concrete primitives that derive from these bases (EmailAddress, Money, etc.) live in Trellis.Primitives — see trellis-api-primitives.md.

RangeAttribute

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class RangeAttribute : Attribute
Name Type Description
Constructor arguments are consumed by the source generator; no public properties are exposed.
Signature Returns Description
public RangeAttribute(int minimum, int maximum) RangeAttribute Range metadata for RequiredInt<TSelf> and whole-number RequiredDecimal<TSelf>.
public RangeAttribute(long minimum, long maximum) RangeAttribute Range metadata for RequiredLong<TSelf>.
public RangeAttribute(double minimum, double maximum) RangeAttribute Fractional range metadata for RequiredDecimal<TSelf>.

StringLengthAttribute

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class StringLengthAttribute : Attribute
Name Type Description
MaximumLength int Inclusive maximum length.
MinimumLength int Inclusive minimum length; defaults to 0.
Signature Returns Description
public StringLengthAttribute(int maximumLength) StringLengthAttribute Length metadata for RequiredString<TSelf>.

EnumValueAttribute

[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
public sealed class EnumValueAttribute : Attribute
Name Type Description
Value string Canonical symbolic name for a RequiredEnum<TSelf> member.
Signature Returns Description
public EnumValueAttribute(string value) EnumValueAttribute Overrides the default field-name-based symbolic value.

StringExtensions

public static class StringExtensions
Name Type Description
Static helper type.
Signature Returns Description
public static string NormalizeFieldName(this string? fieldName, string defaultName) string Uses fieldName when present, otherwise camel-cases defaultName.
public static T ParseScalarValue<T>(string? s) where T : class, IScalarValue<T, string> T Throws FormatException based on T.TryCreate.
public static bool TryParseScalarValue<T>([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out T result) where T : class, IScalarValue<T, string> bool Safe parsing helper based on T.TryCreate.
public static string ToCamelCase(this string? str) string Lowercases the first character only.

RequiredEnumJsonConverter<TRequiredEnum>

public sealed class RequiredEnumJsonConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TRequiredEnum> : JsonConverter<TRequiredEnum>
    where TRequiredEnum : RequiredEnum<TRequiredEnum>, IScalarValue<TRequiredEnum, string>
Name Type Description
Converter type; no public properties.
Signature Returns Description
public override TRequiredEnum? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) TRequiredEnum? Accepts only JSON string and null; string values are resolved through RequiredEnum<TRequiredEnum>.TryFromName(name).
public override void Write(Utf8JsonWriter writer, TRequiredEnum value, JsonSerializerOptions options) void Writes value.Value as a JSON string.

RequiredString<TSelf>

public abstract class RequiredString<TSelf> : ScalarValueObject<TSelf, string>
    where TSelf : RequiredString<TSelf>, IScalarValue<TSelf, string>
Name Type Description
Value string Inherited scalar value.
Length int Convenience access to Value.Length.
Signature Returns Description
public bool StartsWith(string value) bool Delegates to string.StartsWith(string).
public bool Contains(string value) bool Delegates to string.Contains(string).
public bool EndsWith(string value) bool Delegates to string.EndsWith(string).
public static TSelf Create(string value) TSelf Inherited throwing scalar factory. Source-generated overloads are listed below.

RequiredGuid<TSelf>

public abstract class RequiredGuid<TSelf> : ScalarValueObject<TSelf, Guid>
    where TSelf : RequiredGuid<TSelf>, IScalarValue<TSelf, Guid>
Name Type Description
Value Guid Inherited scalar value.
Signature Returns Description
public static TSelf Create(Guid value) TSelf Inherited throwing scalar factory. Source-generated overloads are listed below.

RequiredInt<TSelf>

public abstract class RequiredInt<TSelf> : ScalarValueObject<TSelf, int>
    where TSelf : RequiredInt<TSelf>, IScalarValue<TSelf, int>
Name Type Description
Value int Inherited scalar value.
Signature Returns Description
public static TSelf Create(int value) TSelf Inherited throwing scalar factory. Source-generated overloads are listed below.

RequiredDecimal<TSelf>

public abstract class RequiredDecimal<TSelf> : ScalarValueObject<TSelf, decimal>
    where TSelf : RequiredDecimal<TSelf>, IScalarValue<TSelf, decimal>
Name Type Description
Value decimal Inherited scalar value.
Signature Returns Description
public static TSelf Create(decimal value) TSelf Inherited throwing scalar factory. Source-generated overloads are listed below.

RequiredLong<TSelf>

public abstract class RequiredLong<TSelf> : ScalarValueObject<TSelf, long>
    where TSelf : RequiredLong<TSelf>, IScalarValue<TSelf, long>
Name Type Description
Value long Inherited scalar value.
Signature Returns Description
public static TSelf Create(long value) TSelf Inherited throwing scalar factory. Source-generated overloads are listed below.

RequiredBool<TSelf>

public abstract class RequiredBool<TSelf> : ScalarValueObject<TSelf, bool>
    where TSelf : RequiredBool<TSelf>, IScalarValue<TSelf, bool>
Name Type Description
Value bool Inherited scalar value.
Signature Returns Description
public static TSelf Create(bool value) TSelf Inherited throwing scalar factory. Source-generated overloads are listed below.

RequiredDateTime<TSelf>

public abstract class RequiredDateTime<TSelf> : ScalarValueObject<TSelf, DateTime>
    where TSelf : RequiredDateTime<TSelf>, IScalarValue<TSelf, DateTime>
Name Type Description
Value DateTime Inherited scalar value.
Signature Returns Description
public override string ToString() string Formats Value using invariant round-trip format "O".
public static TSelf Create(DateTime value) TSelf Inherited throwing scalar factory. Source-generated overloads are listed below.

RequiredEnum<TSelf>

public abstract class RequiredEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TSelf>
    : IEquatable<RequiredEnum<TSelf>>
    where TSelf : RequiredEnum<TSelf>, IScalarValue<TSelf, string>
Name Type Description
Value string Canonical symbolic identity; defaults to the public static field name unless [EnumValue] overrides it.
Ordinal int Declaration-order metadata; not a wire/storage identity.
Signature Returns Description
public static IReadOnlyCollection<TSelf> GetAll() IReadOnlyCollection<TSelf> Returns all discovered public static readonly members.
public static Result<TSelf> TryFromName(string? name, string? fieldName = null) Result<TSelf> Case-insensitive symbolic lookup.
public bool Is(params TSelf[] values) bool True when this instance matches any provided member.
public bool IsNot(params TSelf[] values) bool Negation of Is(params TSelf[]).
public override string ToString() string Returns Value.
public override int GetHashCode() int Case-insensitive hash of Value.
public override bool Equals(object? obj) bool Case-insensitive symbolic equality.
public bool Equals(RequiredEnum<TSelf>? other) bool Case-insensitive symbolic equality.
public static bool operator ==(RequiredEnum<TSelf>? left, RequiredEnum<TSelf>? right) bool Equality operator.
public static bool operator !=(RequiredEnum<TSelf>? left, RequiredEnum<TSelf>? right) bool Inequality operator.

Source-generated members

The incremental generator at Trellis.Core/generator/RequiredPartialClassGenerator.cs (bundled inside Trellis.Core.nupkg at analyzers/dotnet/cs/Trellis.Core.Generator.dll) augments partial classes that inherit a Required*<TSelf> base type.

RequiredString<TSelf>

[JsonConverter(typeof(ParsableJsonConverter<TSelf>))]
public static Result<TSelf> TryCreate(string? value, string? fieldName = null)
public static TSelf Create(string? value, string? fieldName = null)
public static TSelf Parse(string s, IFormatProvider? provider)
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TSelf result)
public static explicit operator TSelf(string value)
static partial void ValidateAdditional(string value, string fieldName, ref string? errorMessage)
  • Built-in validation: null/empty/whitespace rejection, trimming, optional [StringLength] checks.

RequiredGuid<TSelf>

[JsonConverter(typeof(ParsableJsonConverter<TSelf>))]
public static TSelf NewUniqueV4()
public static TSelf NewUniqueV7()
public static Result<TSelf> TryCreate(Guid value, string? fieldName = null)
public static Result<TSelf> TryCreate(Guid? requiredGuidOrNothing, string? fieldName = null)
public static Result<TSelf> TryCreate(string? stringOrNull, string? fieldName = null)
public static new TSelf Create(Guid value)
public static TSelf Create(string stringValue)
public static TSelf Parse(string s, IFormatProvider? provider)
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TSelf result)
public static explicit operator TSelf(Guid value)
static partial void ValidateAdditional(Guid value, string fieldName, ref string? errorMessage)
  • Built-in validation: Guid.Empty rejection.

RequiredInt<TSelf>

[JsonConverter(typeof(ParsableJsonConverter<TSelf>))]
public static Result<TSelf> TryCreate(int value, string? fieldName = null)
public static Result<TSelf> TryCreate(int? valueOrNothing, string? fieldName = null)
public static Result<TSelf> TryCreate(string? stringOrNull, string? fieldName = null)
public static Result<TSelf> TryCreate(string? value, IFormatProvider? provider, string? fieldName = null)
public static new TSelf Create(int value)
public static TSelf Create(string stringValue)
public static TSelf Parse(string s, IFormatProvider? provider)
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TSelf result)
public static explicit operator TSelf(int value)
static partial void ValidateAdditional(int value, string fieldName, ref string? errorMessage)
  • Built-in validation: null rejection for nullable inputs, optional [Range(int, int)].

RequiredDecimal<TSelf>

[JsonConverter(typeof(ParsableJsonConverter<TSelf>))]
public static Result<TSelf> TryCreate(decimal value, string? fieldName = null)
public static Result<TSelf> TryCreate(decimal? valueOrNothing, string? fieldName = null)
public static Result<TSelf> TryCreate(string? stringOrNull, string? fieldName = null)
public static Result<TSelf> TryCreate(string? value, IFormatProvider? provider, string? fieldName = null)
public static new TSelf Create(decimal value)
public static TSelf Create(string stringValue)
public static TSelf Parse(string s, IFormatProvider? provider)
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TSelf result)
public static explicit operator TSelf(decimal value)
static partial void ValidateAdditional(decimal value, string fieldName, ref string? errorMessage)
  • Built-in validation: null rejection for nullable inputs, optional [Range(int, int)] or [Range(double, double)].

RequiredLong<TSelf>

[JsonConverter(typeof(ParsableJsonConverter<TSelf>))]
public static Result<TSelf> TryCreate(long value, string? fieldName = null)
public static Result<TSelf> TryCreate(long? valueOrNothing, string? fieldName = null)
public static Result<TSelf> TryCreate(string? stringOrNull, string? fieldName = null)
public static Result<TSelf> TryCreate(string? value, IFormatProvider? provider, string? fieldName = null)
public static new TSelf Create(long value)
public static TSelf Create(string stringValue)
public static TSelf Parse(string s, IFormatProvider? provider)
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TSelf result)
public static explicit operator TSelf(long value)
static partial void ValidateAdditional(long value, string fieldName, ref string? errorMessage)
  • Built-in validation: null rejection for nullable inputs, optional [Range(long, long)].

RequiredBool<TSelf>

[JsonConverter(typeof(ParsableJsonConverter<TSelf>))]
public static Result<TSelf> TryCreate(bool value, string? fieldName = null)
public static Result<TSelf> TryCreate(bool? valueOrNothing, string? fieldName = null)
public static Result<TSelf> TryCreate(string? stringOrNull, string? fieldName = null)
public static new TSelf Create(bool value)
public static TSelf Create(string stringValue)
public static TSelf Parse(string s, IFormatProvider? provider)
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TSelf result)
public static explicit operator TSelf(bool value)
static partial void ValidateAdditional(bool value, string fieldName, ref string? errorMessage)
  • Built-in validation: null rejection for nullable inputs; false is valid.

RequiredDateTime<TSelf>

[JsonConverter(typeof(ParsableJsonConverter<TSelf>))]
public static Result<TSelf> TryCreate(DateTime value, string? fieldName = null)
public static Result<TSelf> TryCreate(DateTime? valueOrNothing, string? fieldName = null)
public static Result<TSelf> TryCreate(string? stringOrNull, string? fieldName = null)
public static Result<TSelf> TryCreate(string? value, IFormatProvider? provider, string? fieldName = null)
public static new TSelf Create(DateTime value)
public static TSelf Create(string stringValue)
public static TSelf Parse(string s, IFormatProvider? provider)
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TSelf result)
public static explicit operator TSelf(DateTime value)
static partial void ValidateAdditional(DateTime value, string fieldName, ref string? errorMessage)
  • Built-in validation: DateTime.MinValue rejection.

RequiredEnum<TSelf>

[JsonConverter(typeof(RequiredEnumJsonConverter<TSelf>))]
public static Result<TSelf> TryCreate(string value)
public static Result<TSelf> TryCreate(string? value, string? fieldName = null)
public static TSelf Parse(string s, IFormatProvider? provider)
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TSelf result)
public static TSelf Create(string value)
  • Generated TryCreate delegates only to TryFromName.
  • The enum JSON converter also uses only TryFromName; there is no TryFromValue path.

Building your own primitive

using System.Globalization;
using Trellis;

namespace Demo;

[StringLength(50)]
public partial class CustomerName : RequiredString<CustomerName> { }

public partial class OrderId : RequiredGuid<OrderId> { }

[Range(1, 999)]
public partial class LineCount : RequiredInt<LineCount> { }

public partial class SubmittedAt : RequiredDateTime<SubmittedAt> { }

public partial class OrderState : RequiredEnum<OrderState>
{
    public static readonly OrderState Draft = new();

    [EnumValue("submitted")]
    public static readonly OrderState Submitted = new();
}

public static class Example
{
    public static void Run()
    {
        var orderId = OrderId.NewUniqueV7();
        var name = CustomerName.Create("Ada");
        var lines = LineCount.TryCreate("42", CultureInfo.InvariantCulture).TryGetValue(out var v) ? v : null!;
        var submittedAt = SubmittedAt.Parse("2026-01-15T12:00:00Z", CultureInfo.InvariantCulture);
        var state = OrderState.Create("submitted");

        _ = (orderId, name, lines, submittedAt, state);
    }
}

Extension methods

AggregateETagExtensions

public static Result<T> OptionalETag<T>(this Result<T> result, EntityTagValue[]? expectedETags) where T : IAggregate
public static Result<T> RequireETag<T>(this Result<T> result, EntityTagValue[]? expectedETags) where T : IAggregate
public static Task<Result<T>> OptionalETagAsync<T>(this Task<Result<T>> resultTask, EntityTagValue[]? expectedETags) where T : IAggregate
public static ValueTask<Result<T>> OptionalETagAsync<T>(this ValueTask<Result<T>> resultTask, EntityTagValue[]? expectedETags) where T : IAggregate
public static Task<Result<T>> RequireETagAsync<T>(this Task<Result<T>> resultTask, EntityTagValue[]? expectedETags) where T : IAggregate
public static ValueTask<Result<T>> RequireETagAsync<T>(this ValueTask<Result<T>> resultTask, EntityTagValue[]? expectedETags) where T : IAggregate

Notes:

  • Matching is always strong RFC 9110 comparison.
  • expectedETags is null means “no If-Match header supplied”.
  • expectedETags.Length == 0 fails with Error.PreconditionFailed because the header contained only weak ETags.
  • EntityTagValue.Wildcard() bypasses value comparison and succeeds immediately.

Internal types

  • AndSpecification<T>, OrSpecification<T>, and NotSpecification<T> are internal implementation types returned by the public combinators on Specification<T>.

Code examples

Aggregate, entity, and ETag validation

using System;
using Trellis;

public sealed class OrderId : ScalarValueObject<OrderId, Guid>, IScalarValue<OrderId, Guid>
{
    private OrderId(Guid value) : base(value) { }

    public static Result<OrderId> TryCreate(Guid value, string? fieldName = null) =>
        value == Guid.Empty
            ? Result.Fail<OrderId>(Error.UnprocessableContent.ForField(fieldName ?? "orderId", "required", "Order ID is required."))
            : Result.Ok(new OrderId(value));

    public static Result<OrderId> TryCreate(string? value, string? fieldName = null) =>
        Guid.TryParse(value, out var guid)
            ? TryCreate(guid, fieldName)
            : Result.Fail<OrderId>(Error.UnprocessableContent.ForField(fieldName ?? "orderId", "must_be_guid", "Order ID must be a GUID."));
}

public sealed record OrderPlaced(OrderId OrderId, DateTimeOffset OccurredAt) : IDomainEvent;

public sealed class Order : Aggregate<OrderId>
{
    public string Description { get; private set; }

    private Order(OrderId id, string description) : base(id) => Description = description;

    public static Result<Order> Create(string description)
    {
        var order = new Order(OrderId.Create(Guid.NewGuid()), description);
        order.DomainEvents.Add(new OrderPlaced(order.Id, DateTimeOffset.UtcNow));
        return Result.Ok(order);
    }
}

Result<Order> orderResult = Order.Create("starter-order");
if (orderResult.TryGetValue(out var order))
{
    var guarded = Result.Ok(order).OptionalETag(new[] { EntityTagValue.Strong(order.ETag) });
}

Specification composition

using System;
using System.Linq.Expressions;
using Trellis;

public sealed class Subscription
{
    public DateTimeOffset ExpiresAt { get; init; }
    public bool IsCancelled { get; init; }
}

public sealed class ExpiredSubscriptionSpec(DateTimeOffset now) : Specification<Subscription>
{
    public override Expression<Func<Subscription, bool>> ToExpression() =>
        subscription => subscription.ExpiresAt < now;
}

public sealed class ActiveSubscriptionSpec : Specification<Subscription>
{
    public override Expression<Func<Subscription, bool>> ToExpression() =>
        subscription => !subscription.IsCancelled;
}

var spec = new ExpiredSubscriptionSpec(DateTimeOffset.UtcNow)
    .And(new ActiveSubscriptionSpec());

Cross-references