# Functional - UnambitiousFx > Result, Maybe, and railway-oriented error handling for .NET — zero-allocation and Native-AOT ready. This file contains all documentation content in a single document following the llmstxt.org standard. ## ASP.NET Core Integration `UnambitiousFx.Functional.AspNetCore` (v2.0.0) turns the functional types from `UnambitiousFx.Functional` into HTTP responses. Keep your domain and application services returning `Result`, `Result`, and `Maybe`, then adapt the outcome to HTTP at the API boundary — for both **Minimal APIs** (`IResult`) and **MVC controllers** (`IActionResult`). The conversion is driven by fluent builders. A single failure-to-status mapper (`IFailureHttpMapper`) centralizes how each failure type becomes an HTTP status code and body, so transport concerns stay out of your business logic. ## Install ```bash dotnet add package UnambitiousFx.Functional.AspNetCore ``` ```xml ``` ## How it works ```mermaid flowchart LR Handler["Service / handler"] Result["Result<T> / Maybe<T>"] Builder["AsHttpBuilder()\nAsActionResultBuilder()"] Mapper["IFailureHttpMapper"] Response["IResult / IActionResult"] Handler --> Result Result --> Builder Builder -- success --> Response Builder -- failure --> Mapper Mapper --> Response ``` - **Success** branches are mapped to a status code (200/201/202/204) with the value serialized as the body. - **Failure** branches are passed to the configured `IFailureHttpMapper`, which produces a status code and (by default) an RFC `ProblemDetails` body. ## Registration (optional) The builders work with no setup at all — they fall back to `DefaultFailureHttpMapper` when you don't pass a mapper. To register a shared mapper (and custom mappings) in DI, call `AddResultHttp`: ```csharp using UnambitiousFx.Functional.AspNetCore; builder.Services.AddResultHttp(); ``` `AddResultHttp` registers the resolved `IFailureHttpMapper` and a `ResultHttpAdapterPolicy` as singletons. Pass a configuration delegate to add custom mappings — see [Custom Mappers](./custom-mappers). Inject the mapper into endpoints/controllers and pass it to the builder when you want DI-managed behavior: ```csharp app.MapGet("/users/{id:guid}", async ( Guid id, IUserService service, IFailureHttpMapper mapper) => await service.GetUserAsync(id).AsHttpBuilder(mapper)); ``` ## Minimal API — quick look Namespace: `UnambitiousFx.Functional.AspNetCore.Http` ```csharp using UnambitiousFx.Functional.AspNetCore.Http; // Result → 200 OK with the value, failures mapped automatically app.MapGet("/users/{id:guid}", async (Guid id, IUserService service) => await service.GetUserAsync(id).AsHttpBuilder()); // Maybe → 200 on Some, 404 on None app.MapGet("/profiles/{id:guid}", async (Guid id, IProfileService service) => await service.FindProfileAsync(id).AsHttpBuilder()); // 201 Created with a Location header app.MapPost("/users", async (CreateUserRequest request, IUserService service) => await service.CreateAsync(request) .AsHttpBuilder() .AsCreated(user => $"/users/{user.Id}")); ``` The builder is awaitable directly — `await someResult.AsHttpBuilder()` yields the `IResult`. ## MVC — quick look Namespace: `UnambitiousFx.Functional.AspNetCore.Mvc` ```csharp using UnambitiousFx.Functional.AspNetCore.Mvc; [ApiController] [Route("api/users")] public sealed class UsersController : ControllerBase { [HttpGet("{id:guid}")] public async Task Get(Guid id, [FromServices] IUserService service) => await service.GetUserAsync(id).AsActionResultBuilder(); } ``` ## Default failure mapping `DefaultFailureHttpMapper` maps the standard failure types from `UnambitiousFx.Functional.Failures`: | Failure type | HTTP status | Body | | ------------------------ | ------------------------ | --------------- | | `ValidationFailure` | 400 Bad Request | `ProblemDetails`| | `BadRequestFailure` | 400 Bad Request | `ProblemDetails`| | `NotFoundFailure` | 404 Not Found | `ProblemDetails`| | `UnauthorizedFailure` | 401 Unauthorized | `ProblemDetails`| | `UnauthenticatedFailure` | 403 Forbidden | `ProblemDetails`| | `ConflictFailure` | 409 Conflict | `ProblemDetails`| | `ExceptionalFailure` | 500 Internal Server Error| `ProblemDetails`| | Any other failure | 500 Internal Server Error| `ProblemDetails`| :::note Naming vs. status The default mapper maps `UnauthorizedFailure` to **401** and `UnauthenticatedFailure` to **403**. If your API contract expects the conventional 401/403 split, override these with a [custom mapper](./custom-mappers). ::: ## Learn more - [HTTP Mapping (Minimal API)](./http-mapping) — the full builder API, status-code control, headers, Problem Details shape, and Minimal API patterns. - [MVC Patterns](./mvc-patterns) — controller usage and `IActionResult` builders. - [Custom Mappers](./custom-mappers) — customize failure-to-status mapping and adapter policy. ## See also - [Result](/docs/result/) - [Maybe](/docs/maybe/) - [Failures and Metadata](/docs/failures-and-metadata) --- ## Functional UnambitiousFx.Functional is a lightweight, performance-focused functional programming library for .NET. It makes failure handling and optional values elegant and type-safe through railway-oriented programming, so errors become part of your type signatures instead of control-flow exceptions. ## Key features - **`Result` / `Result`** — railway-oriented programming for failure handling without exceptions. - **`Maybe`** — type-safe optional values, no more null reference exceptions. - **Rich failure types** — `ValidationFailure`, `NotFoundFailure`, `ConflictFailure`, `UnauthorizedFailure`, and more, each with a machine-readable `Code`. - **Metadata** — attach contextual key/value information to any result. - **Async first** — full support for `Task` and `ValueTask` across every operator. - **ASP.NET Core integration** — convert results to HTTP responses with sensible status-code mapping. - **xUnit assertions** — fluent `ShouldBe()` assertions for functional types. - **Zero-allocation & AOT** — `readonly struct` value types, minimal heap pressure, NativeAOT-friendly. ## Railway-oriented flow A pipeline forms a "track": success values flow down the happy path through `Bind`/`Map`, while the first failure short-circuits everything that follows. ```mermaid graph LR Input["Input"] --> Bind["Bind / Map / Ensure"] Bind -->|success| Bind2["Bind / Map / Ensure"] Bind2 -->|success| Success["Success(value)"] Bind -.->|failure| Failure["Failure(error)"] Bind2 -.->|failure| Failure Success --> Match["Match"] Failure --> Match ``` ## Packages ```bash dotnet add package UnambitiousFx.Functional dotnet add package UnambitiousFx.Functional.AspNetCore # optional — web API integration dotnet add package UnambitiousFx.Functional.xunit # optional — test assertions ``` ```xml ``` | Package | Purpose | | ------------------------------------ | --------------------------------------------------------------- | | `UnambitiousFx.Functional` | Core types: `Result`, `Result`, `Maybe`, failures, metadata. | | `UnambitiousFx.Functional.AspNetCore`| Map results to `IActionResult` / `IResult` HTTP responses. | | `UnambitiousFx.Functional.xunit` | Fluent `ShouldBe()` assertions for functional types in tests. | Supported target frameworks: `net8.0`, `net9.0`, `net10.0`. The core package has no runtime dependencies. ## Quick start ```csharp using UnambitiousFx.Functional; using UnambitiousFx.Functional.Failures; static Result ParsePositive(string input) { if (!int.TryParse(input, out var value)) return Result.Failure(new ValidationFailure("Input is not a valid integer")); return value > 0 ? Result.Success(value) : Result.Failure(new ValidationFailure("Value must be positive")); } var result = ParsePositive("42") .Map(value => value * 2) // transform the success value .Ensure(value => value < 1000, new ValidationFailure("Too large")) // add a guard .Bind(value => Result.Success(value.ToString())); // chain another Result result.Match( success: text => Console.WriteLine($"Success: {text}"), failure: error => Console.WriteLine($"Failure: {error.Code} - {error.Message}")); ``` ## Design principles - **Errors are values** — failures are part of the type signature, not exceptions used for control flow. - **Zero-allocation** — core types are `readonly struct`s that minimize heap pressure on the success path. - **Composition over exceptions** — chain operations with `Bind`, `Map`, `Ensure`, `Recover`, and `Tap`. - **Modern C#** — file-scoped namespaces, records, pattern matching, and extension members throughout. ## Next steps Follow this path from fundamentals to advanced integration: 1. [Getting Started](./getting-started) — install, create your first `Result`, and chain operations. 2. [Result](./result/) — the full railway-oriented API surface. 3. [Maybe](./maybe/) — optional values without null. 4. [Failures and Metadata](./failures-and-metadata) — error modeling, failure codes, and contextual data. 5. [ASP.NET Core](./aspnetcore/) — convert results to HTTP responses. 6. [xUnit](./xunit/) — fluent assertions for functional types in tests. --- ## Maybe # `Maybe` `Maybe` models an **optional value**: it is either **Some** — a present value of type `T` — or **None**, the explicit absence of a value. It is a type-safe alternative to `null` that forces the absent case to be handled at compile time, and it composes through `Map`, `Bind`, `Filter`, and LINQ so you can describe a whole pipeline without scattering null checks. `Maybe` is a `readonly record struct`, so it allocates nothing on the heap for the wrapper itself. The constraint `where T : notnull` guarantees a Some never holds `null`. ## When to reach for `Maybe` - A value may legitimately be absent — cache misses, lookups, optional configuration. - A query may return no row — `FindById` returning `Maybe` instead of a nullable. - You want to chain steps that each may produce nothing, without `if (x is null)` at every hop. Use [`Result`](/docs/result/) instead when absence is an **error** that needs a reason. `Maybe` says "nothing here"; `Result` says "here is why it failed". Bridge from one to the other with [`ToResult`](#bridging-to-result). ## Creating a `Maybe` ```csharp using UnambitiousFx.Functional; // Static factories Maybe some = Maybe.Some(42); Maybe none = Maybe.None(); // Instance factories Maybe also = Maybe.Some(42); // Implicit conversion from a (nullable) value: null becomes None Maybe fromValue = "hello"; // Some("hello") Maybe fromNull = null; // None ``` ## Inspecting state ```csharp Maybe maybe = Maybe.Some(42); bool present = maybe.IsSome; // true bool absent = maybe.IsNone; // false // Case yields the value when Some, or default(T) when None. int? raw = maybe.Case; // 42 // Try-style extraction with a guaranteed-non-null out value. if (maybe.Some(out var value)) { Console.WriteLine(value); // 42 } ``` Prefer `Match`, `ValueOr`, or `Some(out …)` over reading `Case` directly — they make the None case explicit. ## Matching and switching `Match` collapses both branches into a single value; `Switch` (and the void `Match` overload) runs a side effect per branch. ```csharp string label = maybe.Match( some: value => $"Found {value}", none: () => "Nothing here"); maybe.Switch( some: value => Console.WriteLine($"Got {value}"), none: () => Console.WriteLine("Empty")); // Run an action only on one branch: maybe.IfSome(value => Console.WriteLine(value)); maybe.IfNone(() => Console.WriteLine("Empty")); ``` ## Transforming: Map, Bind, Filter - **`Map`** transforms the inner value, preserving None. - **`Bind`** transforms the value with a function that itself returns a `Maybe`, flattening the result (no `Maybe>`). - **`Filter`** keeps the value only when a predicate holds; otherwise it becomes None. ```csharp Maybe name = Maybe.Some(new User("Ada")) .Filter(user => user.IsActive) // Some(user) or None .Map(user => user.Name) // Some("Ada") or None .Bind(LookupDisplayName); // flattens Maybe Maybe LookupDisplayName(string name) => name.Length > 0 ? Maybe.Some(name.ToUpperInvariant()) : Maybe.None(); ``` ## Extracting a value: ValueOr and OrElse `ValueOr` unwraps to a plain `T`, supplying a fallback for None. `OrElse` stays in `Maybe` space, supplying an alternative `Maybe`. ```csharp int retries = Maybe.None().ValueOr(3); // 3 int lazy = Maybe.None().ValueOr(() => Compute()); // factory only runs on None Maybe user = primaryLookup .OrElse(secondaryLookup) // try another Maybe .OrElse(() => FallbackLookup()); // or a lazily-produced one ``` To read the raw inner value or `default(T)`, use the `Case` property (there is no separate `ValueOrDefault` on `Maybe`). ## Bridging to Result When an empty `Maybe` should become a failure, convert it with `ToResult`, supplying the failure to use for None. There are overloads for a ready-made `Failure`, a lazy `Func`, and a plain message string. ```csharp using UnambitiousFx.Functional.Failures; Result required = FindUser(id) .ToResult(new NotFoundFailure("User", id.ToString())); Result lazyFailure = FindUser(id) .ToResult(() => new NotFoundFailure("User", id.ToString())); Result messageFailure = FindUser(id) .ToResult("User not found"); ``` See [Failures and Metadata](/docs/failures-and-metadata) for the failure catalog. ## Side effects: the Tap family Tap operators run a side effect and return the original `Maybe` unchanged, so they slot into a pipeline without breaking the chain. - **`Tap`** / **`TapSome`** run only when Some. - **`TapNone`** runs only when None. ```csharp Maybe order = LookupOrder(id) .TapSome(o => _logger.LogInformation("Found order {Id}", o.Id)) .TapNone(() => _logger.LogWarning("Order {Id} missing", id)); ``` ## LINQ query syntax `Maybe` implements `Select`, `SelectMany`, and `Where`, so query syntax works directly. A `None` anywhere short-circuits the whole query to None. ```csharp Maybe profile = from user in GetUser(id) from settings in GetSettings(user.SettingsId) where settings.IsPublic select new PublicProfile(user.Name, settings); ``` `Select` maps, `SelectMany` binds (with an optional projector), and `where` filters. ## Async pipelines Every major operator has a `ValueTask>` extension, so you can `await` a chain without unwrapping at each step. Both synchronous and asynchronous delegate overloads are available (e.g. `Map(Func)` and `Map(Func>)`). ```csharp ValueTask> userTask = GetUserAsync(id); Maybe name = await userTask .Filter(u => u.IsActive) .Map(u => u.Name); Result profile = await userTask .Bind(u => GetProfileAsync(u.ProfileId)) // Func>> .ToResult(() => new NotFoundFailure("Profile", id.ToString())); ``` The async surface covers `Map`, `Bind`, `Filter`, `OrElse`, `ValueOr`, `Match`, `Switch`, `TapSome`, `TapNone`, and `ToResult` (including a `Func>` overload). ## Best practices - **Prefer `Maybe` over nullable** when the value flows through a chain — the operators keep the None case from leaking into every call site. - **Reserve `Maybe` for genuine optionality.** If the caller needs to know *why* a value is missing, use `Result`. - **Don't nest** — reach for `Bind` instead of `Map` whenever your function returns another `Maybe`. - **Avoid reading `Case` directly** in business logic; prefer `Match`, `ValueOr`, or `Some(out …)`. - **Use `ValueOr` for simple defaults** and `OrElse` when the fallback is itself optional. ## Testing with Functional.xunit `Functional.xunit` provides fluent assertions for `Maybe` via `ShouldBe()`. ```csharp using UnambitiousFx.Functional; [Fact] public void Map_WithSome_TransformsValue() { // Arrange (Given) var maybe = Maybe.Some(5); // Act (When) var mapped = maybe.Map(x => x * 2); // Assert (Then) mapped.ShouldBe() .Some() .And(value => Assert.Equal(10, value)); } [Fact] public void Filter_WhenPredicateFails_BecomesNone() { // Arrange (Given) var maybe = Maybe.Some(3); // Act (When) var filtered = maybe.Filter(x => x > 10); // Assert (Then) filtered.ShouldBe().None(); } ``` ## See also - [Maybe API Reference](./api-reference) — the full method surface, grouped by category. - [Result](/docs/result/) — for operations that can fail with a reason. - [Failures and Metadata](/docs/failures-and-metadata) — typed failures and contextual metadata. --- ## Result `UnambitiousFx.Functional` embraces the principle that **errors are values**. Instead of throwing exceptions to signal that an operation failed, a method returns a `Result` (success/failure) or `Result` (success-with-a-value/failure). The possibility of failure becomes part of the type, the compiler keeps you honest, and you compose operations into a pipeline that short-circuits the moment something goes wrong. This is **railway-oriented programming**: a chain of steps runs on the "success track" as long as each one succeeds. The first failure switches to the "failure track" and every subsequent step is skipped, carrying the failure straight through to the end. ## Why Result instead of exceptions - **Explicit** — the signature `Result` says the call can fail; `User` says it cannot. - **Composable** — `Bind`, `Map`, `Ensure`, and friends chain steps without nested `try`/`catch`. - **Cheap** — `Result` and `Result` are `readonly record struct`s, so the happy path allocates almost nothing. - **Typed failures** — failures carry a machine-readable `Code` and a human-readable `Message`, and you can pattern-match specific kinds (`NotFoundFailure`, `ValidationFailure`, …). Reserve exceptions for genuine programming errors (misconfiguration, bugs). Use `Result` for expected domain outcomes. ## Creating results ### Success ```csharp // Success without a value Result ok = Result.Success(); Result ok2 = Result.Ok(); // alias for Success() // Success with a value Result value = Result.Success(42); Result value2 = Result.Ok(42); // alias for Success(value) ``` `TValue` is constrained to `notnull`, so `Result` will not compile. Model the absence of a value with [`Maybe`](/docs/maybe/) instead. ### Failure ```csharp // From a message Result fail = Result.Failure("Something went wrong"); // From an exception (wrapped in an ExceptionalFailure) Result fail2 = Result.Failure(new InvalidOperationException("boom")); // From a Failure object Result fail3 = Result.Failure(new ValidationFailure("Email is required")); // From several failures (merged into an AggregateFailure) Result fail4 = Result.Failure(new[] { failureA, failureB }); // Typed failures use the generic overloads Result fail5 = Result.Failure("not found"); ``` `Failure` is also available under the `Fail` / `Fail` aliases. ### Semantic failure factories Prefer the specialized factories over a bare message — they attach the right `Code` and structured metadata, which downstream code (and the ASP.NET Core integration) can translate into the correct HTTP status: ```csharp return Result.FailNotFound("User", userId); // NOT_FOUND return Result.FailValidation("Email is required"); // VALIDATION return Result.FailUnauthenticated("Token expired"); // UNAUTHENTICATED return Result.FailUnauthorized("Admins only"); // UNAUTHORIZED return Result.FailConflict("Email already taken"); // CONFLICT return Result.FailBadRequest("Malformed payload"); // BAD_REQUEST ``` Each factory has a non-generic (`Result`) and a generic (`Result`) form. ### Implicit conversions A value converts implicitly to a success, and a `Failure` converts implicitly to a failure — so you rarely need to name the factory inside a method body: ```csharp public Result CreateUser(string name) { if (string.IsNullOrWhiteSpace(name)) return new ValidationFailure("Name is required"); // Failure → Result return new User(name); // User → Result } ``` ## Inspecting a result `Result` / `Result` deliberately expose **no** public `Value` or `Error` property — you must go through a method that forces you to handle both cases. ```csharp // Boolean guards if (result.IsSuccess) { /* ... */ } if (result.IsFailure) { /* ... */ } // out-variable style if (result.TryGet(out var user, out var error)) Use(user); else Log(error); // Just the value, or just the failure if (result.TryGetValue(out var user)) { /* ... */ } if (result.TryGetFailure(out var failure)) { /* ... */ } ``` ## Pattern matching with `Match` and `Switch` `Match` collapses both tracks into a single value; `Switch` runs side-effecting actions. ```csharp string message = result.Match( success: user => $"Welcome, {user.Name}", failure: error => $"Failed: {error.Message}"); result.Switch( success: user => Console.WriteLine(user.Name), failure: error => Console.WriteLine(error.Message)); ``` For the non-generic `Result`, the success branch takes no argument: ```csharp var http = result.Match( onSuccess: () => Results.NoContent(), onFailure: error => Results.Problem(error.Message)); ``` Matching on the **kind** of failure is a common pattern at boundaries: ```csharp return result.Match( success: user => Results.Ok(user), failure: error => error switch { NotFoundFailure => Results.NotFound(error.Message), ValidationFailure => Results.BadRequest(error.Message), UnauthorizedFailure => Results.Forbid(), _ => Results.Problem(error.Message) }); ``` ## Transforming with `Bind`, `Map`, `Then`, and `Flatten` These are the workhorses of the success track. - **`Map`** — transform the value with a plain function. `Result` → `Result`. - **`Bind`** — chain another operation that itself returns a `Result`. `Result` → `Result`. This is what keeps a pipeline flat instead of producing `Result`. - **`Then`** — run a follow-up step that keeps the same value type (handy for `Result` → `Result` validations or `Result` → `Result` checks that preserve the original value). - **`Flatten`** — collapse a `Result>` into a `Result`. ```csharp Result doubled = Result.Success(5).Map(x => x * 2); // Success(10) Result chained = Result.Success(5).Bind(x => Parse(x)); // runs Parse only on success Result unnested = Result.Success(Result.Success(5)).Flatten(); // Success(5) ``` ### A worked railway example Each step returns a `Result`; the first failure short-circuits the rest. ```csharp Result Checkout(CartRequest request) => LoadCart(request.CartId) // Result .Ensure(cart => cart.Items.Count > 0, _ => new ValidationFailure("Cart is empty")) .Bind(cart => ReserveStock(cart)) // Result .Bind(reservation => ChargeCard(request.PaymentToken, reservation)) .Map(payment => new Receipt(payment.Id)); // Result // If LoadCart fails, ReserveStock / ChargeCard / Map never run — // the original failure flows straight to the caller. ``` ## Validating with `Ensure` `Ensure` keeps the value on the success track when a predicate holds, and produces a failure (via your factory) when it does not: ```csharp Result email = Result.Success("a@b.com") .Ensure(e => e.Contains('@'), e => new ValidationFailure($"'{e}' is not a valid email")); ``` ## Recovering with `Recover` and `Compensate` `Recover` turns a failure back into a success by supplying a fallback value: ```csharp Result config = LoadConfig() .Recover(error => Config.Default); // from the failure // or simply Result config2 = LoadConfig().Recover(Config.Default); ``` `Compensate` runs a rollback when the result has failed — ideal for sagas and compensating transactions. If the rollback also fails, both failures are merged into an `AggregateFailure`: ```csharp Result result = ReserveInventory(productId, qty) .Bind(() => ChargePayment(amount)) .Compensate(error => ReleaseInventory(productId, qty)); ``` `Try` wraps a throwing delegate, converting any exception into a failure: ```csharp Result parsed = Result.Success("42").Try(s => int.Parse(s)); ``` ## Side effects with the `Tap` family `Tap` lets you observe a result (logging, metrics) without breaking the chain — it always returns the original result unchanged. ```csharp var result = GetUser(id) .Tap(user => _logger.LogInformation("Loaded {Id}", user.Id)) // on success .TapIf(user => user.IsAdmin, user => Audit(user)) // conditional .TapFailure(error => _logger.LogError("Failed: {Msg}", error.Message)) // on failure .Map(user => user.ToDto()); ``` | Method | Runs when | | ------------- | ---------------------------------- | | `Tap` | the result is a success | | `TapIf` | success **and** the predicate is true | | `TapFailure` | the result is a failure | ## Extracting values with `ValueOr*` When you finally need the raw value, choose how to handle the failure case: ```csharp int a = result.ValueOr(0); // fallback value int b = result.ValueOr(() => Compute()); // lazily-produced fallback int? c = result.ValueOrDefault(); // default(T) on failure int d = result.ValueOrThrow(); // throws the aggregated exception int e = result.ValueOrThrow(err => new MyException(err.Message)); // custom exception ``` ## LINQ query syntax `Select`, `SelectMany`, and `Where` let you compose results with C# query syntax. `Select` maps, `SelectMany` binds, and `Where` filters (producing a `ValidationFailure` when the predicate is false). ```csharp var result = from order in GetOrder(orderId) from customer in GetCustomer(order.CustomerId) // SelectMany / Bind where customer.IsActive // Where / Ensure from address in GetAddress(customer.AddressId) select new OrderDetails(order, customer, address); // Select / Map ``` ## Aggregating with `Combine` `Combine` folds many results into one. It succeeds only if every input succeeds; otherwise **all** failures are aggregated into a single `AggregateFailure` — perfect for collecting every validation error at once. ```csharp Result validation = new[] { ValidateName(input.Name), ValidateEmail(input.Email), ValidateAge(input.Age) }.Combine(); // Result collections combine into Result> Result> all = new[] { Result.Success(1), Result.Success(2) }.Combine(); ``` ## Attaching metadata Every result can carry contextual `Metadata` that flows through transformations. It does not change success/failure — it just travels alongside. ```csharp var result = GetUser(id) .WithMetadata("UserId", id) .WithMetadata(("Operation", "Fetch"), ("Timestamp", DateTime.UtcNow)) .WithMetadata(builder => { builder.Add("CorrelationId", correlationId); }) .Bind(user => Activate(user)); // metadata is preserved across the Bind ``` ## Async usage Most operators have `Task>` and `ValueTask>` overloads, so you can `await` an entire pipeline without unwrapping it at each step: ```csharp ValueTask> pipeline = LoadCartAsync(cartId) // ValueTask> .Ensure(cart => cart.Items.Count > 0, _ => new ValidationFailure("Cart is empty")) .Bind(cart => ReserveStockAsync(cart)) // async Bind .Map(reservation => new Receipt(reservation.Id)); Result receipt = await pipeline; ``` The async overloads accept both synchronous and asynchronous continuations, so you can freely mix `Map(x => ...)` and `Bind(x => SomethingAsync(x))` in the same chain. ## Testing results Use the `Functional.xunit` fluent assertions to make tests expressive and consistent: ```csharp [Fact] public void Map_WithSuccessResult_TransformsValue() { // Arrange (Given) var result = Result.Success(5); // Act (When) var mapped = result.Map(x => x * 2); // Assert (Then) mapped.ShouldBe() .Success() .And(value => Assert.Equal(10, value)); } [Fact] public void Ensure_WithFailingPredicate_ProducesValidationFailure() { // Arrange (Given) var result = Result.Success("invalid-email"); // Act (When) var ensured = result.Ensure(e => e.Contains('@'), _ => new ValidationFailure("Invalid email")); // Assert (Then) ensured.ShouldBe() .Failure() .AndMessage("Invalid email"); } ``` For failures you can chain `.AndMessage("...")` or `.AndCode("...")`; for successes use `.And(v => ...)` or `.Where(v => ...)`. ## Best practices 1. **Stay on the rails.** Compose with `Bind`/`Map`/`Ensure` instead of manually checking `IsFailure` after every step — let failures propagate. 2. **Prefer semantic factories.** `Result.FailNotFound`, `FailValidation`, etc. carry the right code and metadata; a bare `Result.Failure("...")` does not. 3. **Return failures, don't throw them.** Inside domain code, `return` a failure; reserve exceptions for misconfiguration and bugs. 4. **Convert at the boundary.** Keep `Result` flowing through your core; translate to HTTP responses or exceptions only at the edges, via `Match`. 5. **Use `Tap` for observation.** Logging and metrics belong in `Tap`/`TapFailure`, not in the transformation steps. 6. **Collect errors with `Combine`.** When validating several independent inputs, combine them so the caller sees every failure at once. 7. **Honor `notnull`.** `TValue` cannot be nullable — model optionality with `Maybe`. 8. **Lean on implicit conversions.** Returning a value or a `Failure` directly is clearer than wrapping it in a factory call. ## See also - [Result API Reference](./api-reference) — every method, signature, and failure factory. - [Maybe](/docs/maybe/) — for optional values. - [Failures and Metadata](/docs/failures-and-metadata) — failure types and metadata in depth. - [ASP.NET Core Integration](/docs/aspnetcore/) — map failures to HTTP responses. - [xUnit Integration](/docs/xunit/) — the `ShouldBe()` assertions used above. --- ## Functional.xunit `UnambitiousFx.Functional.xunit` adds fluent assertions for `Result`, `Result` and `Maybe` to your xUnit test suites. Instead of unpacking results by hand, you state the branch you expect and chain focused checks onto the value or failure. ## Why use it - **Express intent, not plumbing.** `result.ShouldBe().Success()` reads like the behavior under test, with no `TryGetValue`/`out` boilerplate. - **Rich failure messages.** A failing assertion reports the actual state, including the failure code and message, so a red test tells you what went wrong. - **Typed failures.** Narrow a failure to `ValidationFailure`, `NotFoundFailure`, `ConflictFailure`, `BadRequestFailure`, or any `IFailure` and assert its specific properties. - **Sync and async parity.** The same `ShouldBe()` chain works on `Task>` and `ValueTask>` (and their `Maybe` equivalents) without awaiting first. ## Install ```bash dotnet add package UnambitiousFx.Functional.xunit ``` ```xml ``` ## The `ShouldBe()` entry point Every assertion starts from `ShouldBe()`, then narrows to the expected case: - `result.ShouldBe()` returns a `ResultAssertion` (or `ResultAssertion` for `Result`). - `maybe.ShouldBe()` returns a `MaybeAssertion`. From there you pick a branch: | Source | Branch method | Returns | | ------------- | -------------------- | ----------------------------- | | `Result` | `.Success(because?)` | `SuccessAssertion` | | `Result` | `.Success(because?)` | `SuccessAssertion` | | `Result`/``| `.Failure(because?)` | `FailureAssertion` | | `Maybe` | `.Some(because?)` | `SomeAssertion` | | `Maybe` | `.None(because?)` | `NoneAssertion` | Each branch method takes an optional `because` reason that is woven into the failure message. ## Quick example — Result ```csharp using UnambitiousFx.Functional; using UnambitiousFx.Functional.Failures; using UnambitiousFx.Functional.xunit; using Xunit; [Fact] public void Parse_ValidValue_ReturnsSuccess() { // Arrange (Given) var input = "42"; // Act (When) var result = Parse(input); // Assert (Then) result.ShouldBe() .Success() .And(value => Assert.Equal(42, value)); } [Fact] public void Parse_InvalidValue_ReturnsValidationFailure() { // Arrange (Given) var input = "abc"; // Act (When) var result = Parse(input); // Assert (Then) result.ShouldBe() .Failure() .WhichIsValidationError() .WithFailureContaining("integer"); } ``` ## Quick example — Maybe ```csharp using UnambitiousFx.Functional; using UnambitiousFx.Functional.xunit; using Xunit; [Fact] public void FindUser_ExistingEmail_ReturnsSome() { // Arrange (Given) var email = "john@example.com"; // Act (When) var maybe = FindUser(email); // Assert (Then) maybe.ShouldBe() .Some() .And(user => Assert.Equal(email, user.Email)); } ``` ## Arrange-Act-Assert convention Tests in this family follow the **Arrange-Act-Assert (AAA)** pattern, which maps directly onto **Gherkin (Given-When-Then)**. Use comments to separate the three sections and name tests `MethodName_Scenario_ExpectedBehavior`. See [Test Organization](./test-organization) for the full convention. ## Explore the assertions - [Result Assertions](./result-assertions) — `.Success()`/`.Failure()`, chaining, message/code checks, and typed failures. - [Maybe Assertions](./maybe-assertions) — `.Some()`/`.None()` and value chaining. - [Async Assertions](./async-assertions) — assert directly on `Task>` / `ValueTask>` and async `Maybe`. - [Test Organization](./test-organization) — AAA/Gherkin, naming, theories, and an edge-case checklist. ## See also - [Result](/docs/result/) - [Maybe](/docs/maybe/) - [Failures and Metadata](/docs/failures-and-metadata) --- ## Getting Started This guide walks you through installing UnambitiousFx.Functional, creating your first `Result`, composing a railway-oriented pipeline, and handling failures. ## Install ```bash dotnet add package UnambitiousFx.Functional ``` ```xml ``` For web API projects, also install the ASP.NET Core integration layer: ```bash dotnet add package UnambitiousFx.Functional.AspNetCore ``` ```xml ``` ## Your first Result A `Result` represents an operation that either succeeds with a value or fails with a `Failure`. Create them with the static factory methods on `Result`: ```csharp using UnambitiousFx.Functional; using UnambitiousFx.Functional.Failures; // Success carrying a value Result ok = Result.Success(42); // Failure carrying a typed error Result failed = Result.Failure(new ValidationFailure("Value is required")); ``` Consume a result by pattern matching on it. `Match` forces you to handle both branches: ```csharp ok.Match( success: value => Console.WriteLine($"Got {value}"), failure: error => Console.WriteLine($"Failed: {error.Message}")); ``` :::tip `Result.Success(value)` and `Result.Failure(error)` have implicit conversions, so you can often `return value;` or `return new ValidationFailure("...");` directly where a `Result` is expected. ::: ## Chaining with Bind and Map Real workflows are a series of steps where each can fail. Compose them into a single pipeline: `Map` transforms a success value, `Bind` chains another operation that itself returns a `Result`, and `Ensure` adds a guard. The first failure short-circuits the rest. ```csharp using UnambitiousFx.Functional; using UnambitiousFx.Functional.Failures; public Result PlaceOrder(string rawQuantity) { return ParseQuantity(rawQuantity) // Result .Ensure(qty => qty > 0, new ValidationFailure("Quantity must be > 0")) // guard .Map(qty => new Order(qty)) // transform .Bind(SaveOrder) // chain a Result-returning step .Tap(order => Console.WriteLine($"Saved order {order.Id}")); // side effect on success } static Result ParseQuantity(string input) => int.TryParse(input, out var qty) ? Result.Success(qty) : Result.Failure(new ValidationFailure("Not a number")); static Result SaveOrder(Order order) { // ... persist ... return Result.Success(order); } ``` Everything works the same with `async` code — `Bind`, `Map`, `Tap`, and friends all have `Task` and `ValueTask` overloads: ```csharp public async Task> PlaceOrderAsync(string rawQuantity) { return await ParseQuantity(rawQuantity) .Bind(qty => SaveOrderAsync(new Order(qty))); } ``` ## Handling failures When you need the value or the error out of a result, you have several options. Extract both with `TryGet` (the value on success, the error on failure): ```csharp if (result.TryGet(out var value, out var error)) Console.WriteLine($"Value: {value}"); else Console.WriteLine($"Error: {error.Message}"); ``` Inspect the failure directly with `TryGetFailure`: ```csharp if (result.TryGetFailure(out var failure)) logger.LogWarning("Operation failed: {Code} {Message}", failure.Code, failure.Message); ``` Provide a fallback value with `ValueOr`, or recover into a new success with `Recover`: ```csharp // Substitute a default when the result failed var quantity = result.ValueOr(0); // Turn a failure back into a success Result recovered = result.Recover(error => 0); ``` ## Testing your code Add the `UnambitiousFx.Functional.xunit` package to assert on results fluently with `ShouldBe()`: ```csharp using UnambitiousFx.Functional.xunit; using Xunit; [Fact] public void PlaceOrder_WithValidQuantity_ReturnsSuccess() { // Arrange (Given) var input = "3"; // Act (When) var result = PlaceOrder(input); // Assert (Then) result.ShouldBe() .Success() .And(order => Assert.Equal(3, order.Quantity)); } ``` See [xUnit](./xunit/) for the full assertion API, including failure-type assertions and `Maybe` checks. ## Next steps - [Result](./result/) — the complete railway-oriented operator set. - [Maybe](./maybe/) — model optional values without null. --- ## Result API Reference The complete surface of `Result` and `Result` from `UnambitiousFx.Functional`. For concepts and worked examples, see the [Result guide](./). ## Core types ```csharp public readonly partial record struct Result : IResult; public readonly partial record struct Result : IResult where TValue : notnull; ``` `TValue` is constrained to `notnull`. Both types are immutable value types. ## Factory methods Each `Success`/`Ok` and `Failure`/`Fail` pair below are aliases. Generic overloads (``) produce a `Result`; non-generic overloads produce a `Result`. | Method | Signature | Description | | ------ | --------- | ----------- | | `Success` / `Ok` | `Result Success()` | A success with no value. | | `Success` / `Ok` | `Result Success(T value)` | A success carrying `value`. | | `Failure` / `Fail` | `Result Failure(string message)` | Failure from a message (wrapped in `ExceptionalFailure`). | | `Failure` / `Fail` | `Result Failure(Exception error)` | Failure wrapping an exception. | | `Failure` / `Fail` | `Result Failure(Failure failure)` | Failure from a `Failure` object. | | `Failure` / `Fail` | `Result Failure(params IEnumerable errors)` | Failure merging many into an `AggregateFailure`. | | `Failure` / `Fail` | `Result Failure(...)` | Generic forms of the four overloads above. | ### Static failure factories Extension factories that attach a semantic `Code` and structured metadata. Each has a non-generic (`Result`) and generic (`Result`) overload. | Method | Signature | Failure type / code | | ------ | --------- | ------------------- | | `FailNotFound` | `Result FailNotFound(string resource, string identifier, string? messageOverride = null)` | `NotFoundFailure` — `NOT_FOUND` | | `FailValidation` | `Result FailValidation(string message)` | `ValidationFailure` — `VALIDATION` | | `FailUnauthenticated` | `Result FailUnauthenticated(string? reason)` | `UnauthenticatedFailure` — `UNAUTHENTICATED` | | `FailUnauthorized` | `Result FailUnauthorized(string? reason)` | `UnauthorizedFailure` — `UNAUTHORIZED` | | `FailConflict` | `Result FailConflict(string message)` | `ConflictFailure` — `CONFLICT` | | `FailBadRequest` | `Result FailBadRequest(string message)` | `BadRequestFailure` — `BAD_REQUEST` | ## Inspection | Member | Signature | Description | | ------ | --------- | ----------- | | `IsSuccess` | `bool IsSuccess { get; }` | True when the operation succeeded. | | `IsFailure` | `bool IsFailure { get; }` | True when the operation failed. | | `Metadata` | `IReadOnlyMetadata Metadata { get; }` | Contextual metadata attached to the result. | | `TryGetValue` | `bool TryGetValue(out TValue? value)` | Extracts the value on success (`Result` only). | | `TryGetFailure` | `bool TryGetFailure(out Failure? error)` | Extracts the failure on failure. | | `TryGet` | `bool TryGet(out TValue? value, out Failure? error)` | Extracts both at once (`Result` only). | | `Deconstruct` | `void Deconstruct(out TValue? value, out Failure? error)` | Tuple deconstruction. | | `ToResult` | `Result ToResult()` | Drops the value, keeping state + metadata (`Result` only). | ## Matching | Method | Signature | Description | | ------ | --------- | ----------- | | `Match` | `TOut Match(Func success, Func failure)` | Collapse both branches to a value. | | `Match` | `void Match(Action success, Action failure)` | Side-effecting match. | | `Switch` | `void Switch(Action success, Action failure)` | Alias for the action-based `Match`. | | `IfSuccess` | `void IfSuccess(Action action)` | Run an action only on success. | | `IfFailure` | `void IfFailure(Action action)` | Run an action only on failure. | For the non-generic `Result`, the success delegate takes no argument (e.g. `Match(Func onSuccess, Func onFailure)`). ## Transformation | Method | Signature | Description | | ------ | --------- | ----------- | | `Map` | `Result Map(this Result, Func map)` | Transform the value with a plain function. | | `Bind` | `Result Bind(this Result, Func> bind)` | Chain another result-returning step. | | `Then` | `Result Then(this Result, Func> then)` | Follow-up step preserving the value type. | | `Then` | `Result Then(this Result, Func then)` | Follow-up check that keeps the original value. | | `Then` | `Result Then(this Result, Func func)` | Sequence two non-generic results. | | `Flatten` | `Result Flatten(this Result>)` | Collapse one level of nesting. | ## Failure handling | Method | Signature | Description | | ------ | --------- | ----------- | | `Ensure` | `Result Ensure(this Result, Func predicate, Func errorFactory)` | Fail when the predicate is false. | | `Recover` | `Result Recover(this Result, Func recoverFunc)` | Turn a failure into a success via a factory. | | `Recover` | `Result Recover(this Result, TValue fallback)` | Turn a failure into a success via a fallback value. | | `Compensate` | `Result Compensate(this Result, Func rollback)` | Run a rollback on failure; merges both failures if the rollback fails. | | `Compensate` | `Result Compensate(this Result, Func rollback)` | Non-generic compensation. | | `Try` | `Result Try(this Result, Func func)` | Run a throwing function, catching exceptions into a failure. | | `Try` | `Result Try(this Result, Action action)` | Run a throwing action, catching exceptions into a failure. | ## Side effects | Method | Signature | Description | | ------ | --------- | ----------- | | `Tap` | `Result Tap(this Result, Action tap)` | Observe the value on success; returns the result unchanged. | | `Tap` | `Result Tap(this Result, Action tap)` | Observe success without the value. | | `TapIf` | `Result TapIf(this Result, Func predicate, Action tap)` | Observe only when success and predicate hold. | | `TapFailure` | `Result TapFailure(this Result, Action tap)` | Observe the failure on the failure track. | `Tap`, `TapIf`, and `TapFailure` also exist on the non-generic `Result`. ## Extraction | Method | Signature | Description | | ------ | --------- | ----------- | | `ValueOr` | `TValue ValueOr(this Result, TValue fallback)` | Value, or a fallback on failure. | | `ValueOr` | `TValue ValueOr(this Result, Func fallbackFactory)` | Value, or a lazily-produced fallback. | | `ValueOrDefault` | `TValue? ValueOrDefault(this Result)` | Value, or `default(TValue)` on failure. | | `ValueOrThrow` | `TValue ValueOrThrow(this Result)` | Value, or throws the aggregated exception. | | `ValueOrThrow` | `TValue ValueOrThrow(this Result, Func exceptionFactory)` | Value, or throws a custom exception. | ## Metadata | Method | Signature | Description | | ------ | --------- | ----------- | | `WithMetadata` | `Result WithMetadata(string key, object? value)` | Add a single key/value pair. | | `WithMetadata` | `Result WithMetadata(params (string Key, object? Value)[] items)` | Add several pairs. | | `WithMetadata` | `Result WithMetadata(IReadOnlyMetadata metadata)` | Merge an existing metadata bag. | | `WithMetadata` | `Result WithMetadata(IEnumerable> metadata)` | Merge key/value pairs. | | `WithMetadata` | `Result WithMetadata(Action configure)` | Configure via a builder. | All `WithMetadata` overloads also exist on the non-generic `Result`. ## LINQ | Method | Signature | Description | | ------ | --------- | ----------- | | `Select` | `Result Select(this Result, Func selector)` | Query-syntax `Map`. | | `SelectMany` | `Result SelectMany(this Result, Func> binder)` | Query-syntax `Bind`. | | `SelectMany` | `Result SelectMany(this Result, Func> binder, Func projector)` | `from … from … select` form. | | `Where` | `Result Where(this Result, Func predicate)` | Filter; produces a `ValidationFailure` when false. | | `Combine` | `Result Combine(this IEnumerable)` | Succeeds only if all succeed; aggregates all failures. | | `Combine` | `Result> Combine(this IEnumerable>)` | Collect all values, or aggregate all failures. | ## Implicit conversions | Conversion | Direction | | ---------- | --------- | | `Result(TValue value)` | value → success | | `Result(Failure failure)` | failure → failed `Result` | | `Result(Failure failure)` | failure → failed `Result` | ## Async overloads Most operators have `Task>` and `ValueTask>` extension overloads — including `Map`, `Bind`, `Then`, `Flatten`, `Match`, `Switch`, `Ensure`, `Recover`, `Compensate`, `Try`, `Tap` / `TapFailure`, `ValueOr*`, `Combine`, and the LINQ operators. They accept both synchronous and asynchronous continuations, so a single awaited chain can freely mix `Map(x => …)` with `Bind(x => …Async(x))`: ```csharp Result receipt = await LoadCartAsync(cartId) .Bind(cart => ReserveStockAsync(cart)) .Map(reservation => new Receipt(reservation.Id)); ``` ## See also - [Result](./) — conceptual guide with worked examples. - [Failures and Metadata](/docs/failures-and-metadata) - [Maybe](/docs/maybe/) --- ## Maybe API Reference The full API surface for `Maybe` (`UnambitiousFx.Functional`). `T` is always constrained to `notnull`. Synchronous members live on `Maybe` itself or in `MaybeExtensions`; asynchronous members extend `ValueTask>` and are listed under [Async support](#async-support). ## Creation | Method | Signature | Description | | --- | --- | --- | | `Maybe.Some` | `Maybe Maybe.Some(T value)` | Wraps a non-null value as Some. | | `Maybe.None` | `Maybe Maybe.None()` | Creates an empty None. | | `Maybe.Some` | `static Maybe Some(T value)` | Instance-type factory for Some. | | `Maybe.None` | `static Maybe None()` | Instance-type factory for None. | | implicit operator | `implicit operator Maybe(T? value)` | Converts a value to Some, or `null` to None. | ## Inspection | Member | Signature | Description | | --- | --- | --- | | `IsSome` | `bool IsSome { get; }` | `true` when a value is present. | | `IsNone` | `bool IsNone { get; }` | `true` when no value is present. | | `Case` | `T? Case { get; }` | The value when Some, otherwise `default(T)`. | | `Some` | `bool Some(out T? value)` | Try-pattern: returns `true` and sets `value` (non-null) when Some. | ## Matching | Member | Signature | Description | | --- | --- | --- | | `Match` | `TOut Match(Func some, Func none)` | Collapses both branches to a single value. | | `Match` | `void Match(Action some, Action none)` | Runs an action per branch. | | `Switch` | `void Switch(Action some, Action none)` | Alias of the void `Match`; runs an action per branch. | | `IfSome` | `void IfSome(Action some)` | Runs the action only when Some. | | `IfNone` | `void IfNone(Action none)` | Runs the action only when None. | ## Transformation | Method | Signature | Description | | --- | --- | --- | | `Map` | `Maybe Map(this Maybe, Func)` | Transforms the value, preserving None. | | `Bind` | `Maybe Bind(this Maybe, Func>)` | Transforms with a `Maybe`-returning function, flattening the result. | | `Filter` | `Maybe Filter(this Maybe, Func)` | Keeps the value when the predicate holds; otherwise None. | ## Extraction | Method | Signature | Description | | --- | --- | --- | | `ValueOr` | `T ValueOr(this Maybe, T fallback)` | Returns the value, or the fallback when None. | | `ValueOr` | `T ValueOr(this Maybe, Func fallbackFactory)` | Returns the value, or a lazily-created fallback when None. | | `OrElse` | `Maybe OrElse(this Maybe, Maybe fallback)` | Returns this when Some; otherwise the fallback `Maybe`. | | `OrElse` | `Maybe OrElse(this Maybe, Func> fallbackFactory)` | Returns this when Some; otherwise a lazily-created fallback `Maybe`. | > To read the raw inner value or `default(T)`, use the `Case` property — there is no `ValueOrDefault` on `Maybe`. ## Bridging | Method | Signature | Description | | --- | --- | --- | | `ToResult` | `Result ToResult(Failure failure)` | Some becomes Success; None becomes Failure with the given failure. | | `ToResult` | `Result ToResult(Func errorFactory)` | Some becomes Success; None becomes Failure from the factory. | | `ToResult` | `Result ToResult(string message)` | Some becomes Success; None becomes Failure with the given message. | ## Side effects | Method | Signature | Description | | --- | --- | --- | | `Tap` | `Maybe Tap(this Maybe, Action)` | Runs a side effect when Some; returns the original `Maybe`. | | `TapSome` | `Maybe TapSome(Action)` | Runs a side effect when Some; returns the original `Maybe`. | | `TapNone` | `Maybe TapNone(Action)` | Runs a side effect when None; returns the original `Maybe`. | ## LINQ | Method | Signature | Description | | --- | --- | --- | | `Select` | `Maybe Select(this Maybe, Func)` | LINQ projection; delegates to `Map`. | | `SelectMany` | `Maybe SelectMany(this Maybe, Func>)` | LINQ bind; delegates to `Bind`. | | `SelectMany` | `Maybe SelectMany(this Maybe, Func>, Func)` | LINQ bind with a projection; enables multi-`from` queries. | | `Where` | `Maybe Where(this Maybe, Func)` | LINQ filter; delegates to `Filter`. | ## Async support Each method below extends `ValueTask>` and returns a `ValueTask<…>`, so chains can be `await`ed end to end. Where applicable, both sync-delegate and async-delegate (`Func<…, ValueTask<…>>`) overloads exist. | Method | Signatures | Description | | --- | --- | --- | | `Map` | `Map(Func)`, `Map(Func>)` | Async map, preserving None. | | `Bind` | `Bind(Func>)`, `Bind(Func>>)` | Async bind, flattening the result. | | `Filter` | `Filter(Func)`, `Filter(Func>)` | Async filter. | | `OrElse` | `OrElse(Maybe)`, `OrElse(Func>)`, `OrElse(Func>>)` | Async fallback `Maybe`. | | `ValueOr` | `ValueOr(T)`, `ValueOr(Func)`, `ValueOr(Func>)` | Async unwrap with fallback. | | `Match` | `Match(Func, Func)`, `Match(Func>, Func>)` | Async match. | | `Switch` | `Switch(Action, Action)`, `Switch(Func, Func)` | Async per-branch side effect. | | `TapSome` | `TapSome(Action)`, `TapSome(Func)` | Async side effect when Some. | | `TapNone` | `TapNone(Action)`, `TapNone(Func)` | Async side effect when None. | | `ToResult` | `ToResult(Failure)`, `ToResult(Func)`, `ToResult(string)`, `ToResult(Func>)` | Async bridge to `Result`. | ## See also - [Maybe](./) — concepts and usage guide. - [Result](/docs/result/) — for operations that can fail with a reason. - [Failures and Metadata](/docs/failures-and-metadata) — typed failures used by `ToResult`. --- ## Failures and Metadata `UnambitiousFx.Functional` models errors as **typed failure objects** rather than exceptions or bare strings. A failure carries a stable machine-readable `Code`, a human-readable `Message`, and an immutable `Metadata` bag of contextual key/value pairs. Failures flow through [`Result`](/docs/result/), so error handling stays explicit and composable. All failure types live in `UnambitiousFx.Functional.Failures`. ## The `IFailure` contract Every failure implements `IFailure`: ```csharp public interface IFailure { string Code { get; } // stable code, e.g. "VALIDATION", "NOT_FOUND" string Message { get; } // human-readable description Metadata Metadata { get; } // contextual key/value pairs (never null) } ``` The abstract record `FailureBase` provides the standard immutable, value-based implementation. `Failure` is the concrete general-purpose failure; the specialized types below all derive from it. ## Built-in failure types Each built-in failure sets a code from `FailureCodes`. Construct them directly and pass them to a `Result` factory. | Type | Code constant (value) | Constructor | Use when | | --- | --- | --- | --- | | `Failure` | `FailureCodes.Failure` (`"ERROR"`) | `Failure(string message)` or `Failure(string code, string message, IReadOnlyDictionary? metadata = null)` | A general failure with no more specific category. | | `ValidationFailure` | `FailureCodes.Validation` (`"VALIDATION"`) | `ValidationFailure(string validationMessage, …)` or `ValidationFailure(IReadOnlyList failures, …)` | Input fails field/business validation; collects one or more messages (exposed via `Failures`). | | `NotFoundFailure` | `FailureCodes.NotFound` (`"NOT_FOUND"`) | `NotFoundFailure(string resource, string identifier, string? messageOverride = null, …)` | A requested resource does not exist. Exposes `Resource` and `Identifier`. | | `ConflictFailure` | `FailureCodes.Conflict` (`"CONFLICT"`) | `ConflictFailure(string Message, …)` | A uniqueness/state conflict, e.g. a duplicate key. | | `UnauthorizedFailure` | `FailureCodes.Unauthorized` (`"UNAUTHORIZED"`) | `UnauthorizedFailure(string? reason = null, …)` | The caller is authenticated but not permitted. | | `UnauthenticatedFailure` | `FailureCodes.Unauthenticated` (`"UNAUTHENTICATED"`) | `UnauthenticatedFailure(string? reason = null, …)` | The caller is not authenticated. | | `BadRequestFailure` | `FailureCodes.BadRequest` (`"BAD_REQUEST"`) | `BadRequestFailure(string Message, …)` | The request is malformed beyond field validation. | | `TimeoutFailure` | `FailureCodes.Timeout` (`"TIMEOUT"`) | `TimeoutFailure(TimeSpan configuredTimeout, TimeSpan elapsed)` | An operation exceeded its allotted time. | | `ExceptionalFailure` | `FailureCodes.Exception` (`"EXCEPTION"`) | `ExceptionalFailure(Exception exception, string? messageOverride = null, …)` | Wrapping a caught exception while preserving it (via `Exception`). | | `AggregateFailure` | `FailureCodes.AggregateFailure` (`"AGGREGATE_ERROR"`) | `AggregateFailure(params IEnumerable errors)` | Reporting several failures at once (exposed via `Errors`). | Constructors that take an optional metadata argument accept an `IReadOnlyDictionary?`; the values are merged into the failure's `Metadata`. ## Producing a failed `Result` Use the `Result.Failure` / `Result.Fail` factories (the two are aliases). Overloads accept a `Failure`, an `Exception`, a plain message string, or a collection of failures. ```csharp using UnambitiousFx.Functional; using UnambitiousFx.Functional.Failures; Result ValidateEmail(string email) { if (string.IsNullOrWhiteSpace(email)) { return Result.Failure(new ValidationFailure("Email is required.")); } if (!email.Contains('@')) { return Result.Failure(new ValidationFailure("Email format is invalid.")); } return Result.Success(email); } // Other factories: Result notFound = Result.Fail(new NotFoundFailure("User", id.ToString())); Result fromMessage = Result.Fail("Something went wrong."); // wraps a Failure Result wrapped = Result.Fail(new InvalidOperationException("boom")); // -> ExceptionalFailure ``` ## Aggregating multiple failures `AggregateFailure` collects several failures so a caller sees them all at once — ideal for validation that reports every problem rather than stopping at the first. ```csharp var errors = new List(); if (string.IsNullOrWhiteSpace(dto.Name)) errors.Add(new ValidationFailure("Name is required.")); if (dto.Age < 0) errors.Add(new ValidationFailure("Age must be non-negative.")); Result result = errors.Count == 0 ? Result.Success(Map(dto)) : Result.Failure(new AggregateFailure(errors)); // Inspecting the aggregate: if (result.TryGetFailure(out var failure) && failure is AggregateFailure aggregate) { foreach (var inner in aggregate.Errors) { Console.WriteLine($"{inner.Code}: {inner.Message}"); } } ``` ## Wrapping exceptions `ExceptionalFailure` carries the original `Exception` so it can be logged or rethrown later, while still flowing through the `Result` pipeline. The wrapped exception's full type name is recorded under the `exceptionType` metadata key. ```csharp try { var data = ParseDocument(input); return Result.Success(data); } catch (FormatException ex) { return Result.Failure(new ExceptionalFailure(ex, "Document could not be parsed.")); } ``` You can also pass an `Exception` straight to `Result.Fail` / `Result.Failure`, which wraps it in an `ExceptionalFailure` for you. ## Metadata `Metadata` is an **immutable**, case-insensitive `string → object?` collection used to carry contextual diagnostics — correlation IDs, timing, domain context — alongside a result or failure. Use the static `Metadata.Empty` for the no-entries case. ### Attaching metadata to a result `Result` and `Result` expose `WithMetadata` overloads. Each call returns a **new** result instance — the original is never mutated. ```csharp var result = Result.Success("ok") .WithMetadata("requestId", "req-001") // single key/value .WithMetadata(("traceId", "trace-xyz"), ("region", "eu-west")) // tuples .WithMetadata(builder => builder // fluent builder .Add("feature", "checkout") .AddIf(isPremium, "tier", "premium")); ``` ### Reading metadata Read it back from `result.Metadata`, which returns an `IReadOnlyMetadata` (never null — defaults to `Metadata.Empty`). ```csharp if (result.Metadata.TryGetValue("requestId", out var requestId)) { Console.WriteLine(requestId); } foreach (var (key, value) in result.Metadata) { Console.WriteLine($"{key} = {value}"); } ``` ### Building metadata directly `MetadataBuilder` offers a fluent API and converts implicitly to `Metadata`. ```csharp Metadata metadata = new MetadataBuilder() .Add("requestId", "req-001") .AddIf(isRetry, "attempt", attemptNumber) .AddRange(("source", "api"), ("region", "eu-west")) .Build(); ``` You can also build a `Metadata` from a collection expression or tuples: ```csharp Metadata fromExpression = [ new("requestId", "req-001"), new("region", "eu-west") ]; Metadata fromTuples = Metadata.From(("requestId", "req-001"), ("region", "eu-west")); Metadata merged = Metadata.Merge(first, second); // later values win ``` ### Immutability and `Metadata.Empty` Metadata is treated as immutable in practice: builders and `WithMetadata` always return fresh instances, so attaching context never alters a shared object. When there is nothing to carry, use `Metadata.Empty` rather than `null` — failures and results default to it automatically. ## See also - [Result](/docs/result/) — the type that carries failures and metadata. - [Maybe](/docs/maybe/) — bridge an empty `Maybe` to a failed `Result` with `ToResult`. --- ## Custom Mappers The failure-to-status mapping is driven by a single contract, `IFailureHttpMapper`. The default mapping (see [HTTP Mapping](./http-mapping#failure-to-status-mapping)) covers the standard failure types, but you can add mappings for your own failure types, override the defaults, or replace the mapper entirely. ```csharp using UnambitiousFx.Functional.AspNetCore; using UnambitiousFx.Functional.AspNetCore.Mappers; ``` ## The contract ```csharp public interface IFailureHttpMapper { FailureHttpResponse? GetFailureResponse(IFailure failure); } ``` A mapper returns a `FailureHttpResponse` (status code, body, optional headers, optional content type) — or `null` to signal "I don't handle this failure", letting the next mapper in the chain try. ```csharp public sealed record FailureHttpResponse { public required int StatusCode { get; init; } public object? Body { get; init; } public IReadOnlyDictionary? Headers { get; init; } public string? ContentType { get; init; } } ``` ## Adding mappings via `AddResultHttp` `ResultHttpOptions` provides three `AddMapper` overloads. Custom mappers are evaluated **before** the default mapper, in the order added — the first non-null response wins (chain of responsibility). ### Map a failure type to a status code The simplest overload maps a failure type to a status code and produces a `ProblemDetails` body from the failure message: ```csharp builder.Services.AddResultHttp(options => { options.AddMapper(429); options.AddMapper(504); }); ``` ### Map with a custom response factory For full control over the body, headers, and content type, pass a factory: ```csharp builder.Services.AddResultHttp(options => { options.AddMapper(failure => new FailureHttpResponse { StatusCode = 402, Body = new { message = failure.Message }, Headers = new Dictionary { ["Retry-After"] = "3600" }, ContentType = "application/json", }); }); ``` ### Register an `IFailureHttpMapper` instance ```csharp builder.Services.AddResultHttp(options => { options.AddMapper(new MyFailureHttpMapper()); }); ``` `AddResultHttp` resolves the final mapper (a single mapper, or a `CompositeFailureHttpMapper` wrapping your custom mappers followed by the default) and registers it as a singleton, along with the `ResultHttpAdapterPolicy`. ## Writing a full custom mapper Implement `IFailureHttpMapper` when you want to map several failure types in one place. Return `null` for failures you don't handle so the chain can fall through to the default mapper: ```csharp using UnambitiousFx.Functional.AspNetCore.Mappers; using UnambitiousFx.Functional.Failures; public sealed class MyFailureHttpMapper : IFailureHttpMapper { public FailureHttpResponse? GetFailureResponse(IFailure failure) => failure switch { NotFoundFailure nf => new FailureHttpResponse { StatusCode = 404, Body = new { nf.Message } }, ValidationFailure vf => new FailureHttpResponse { StatusCode = 422, Body = new { vf.Message } }, _ => null, // let the next mapper / default handle it }; } ``` ```csharp builder.Services.AddResultHttp(options => options.AddMapper(new MyFailureHttpMapper())); ``` ## Overriding only a few default mappings `DefaultFailureHttpMapper.GetFailureResponse` is `virtual`. Subclass it to change specific cases and defer to the base for everything else: ```csharp using UnambitiousFx.Functional.AspNetCore.Mappers; using UnambitiousFx.Functional.Failures; public sealed class CustomDefaultMapper : DefaultFailureHttpMapper { public override FailureHttpResponse GetFailureResponse(IFailure failure) { // Map validation errors to 422 instead of the default 400 if (failure is ValidationFailure validation) { return new FailureHttpResponse { StatusCode = 422, Body = new { errors = validation.Failures }, }; } return base.GetFailureResponse(failure); } } ``` ```csharp builder.Services.AddResultHttp(options => options.AddMapper(new CustomDefaultMapper())); ``` :::tip Because the custom mapper always returns a non-null response for the cases it handles and delegates the rest to `base`, you don't need to re-declare every failure type. ::: ## Per-call mappers You don't have to register a mapper globally — pass one directly to a builder for a single endpoint: ```csharp await service.GetUserAsync(id).AsHttpBuilder(new MyFailureHttpMapper()); // Minimal API await service.GetUserAsync(id).AsActionResultBuilder(new MyFailureHttpMapper()); // MVC ``` When omitted, the builders use `DefaultFailureHttpMapper`. ## Composing mappers manually `CompositeFailureHttpMapper` chains mappers and returns the first non-null response. `AddResultHttp` builds one for you, but you can compose explicitly: ```csharp var mapper = new CompositeFailureHttpMapper( new MyFailureHttpMapper(), new DefaultFailureHttpMapper()); ``` ## Adapter policy `ResultHttpAdapterPolicy` (exposed via `options.Policy`) tunes default behavior and is registered as a singleton by `AddResultHttp`: ```csharp builder.Services.AddResultHttp(options => { options.IncludeExceptionDetails = builder.Environment.IsDevelopment(); options.Policy = options.Policy with { ResultSuccessBehavior = ResultSuccessHttpBehavior.Ok, // NoContent (default) | Ok MaybeNoneBehavior = MaybeNoneHttpBehavior.NoContent, // NotFound (default) | NoContent }; }); ``` - `ResultSuccessBehavior` — how a successful non-generic `Result` maps when no explicit success mapper is given (`NoContent` → 204, `Ok` → 200). - `MaybeNoneBehavior` — how `Maybe.None` maps when no `AsNone` mapper is given (`NotFound` → 404, `NoContent` → 204). - `IncludeExceptionDetails` — whether to include exception details (stack traces) in error responses; enable only in development. ## Composition guidelines - Keep domain failures transport-agnostic; map transport semantics in one place (the composition root). - Prefer typed failures over message parsing for deterministic mapping. - Reuse the same registered mapper across Minimal APIs and MVC for consistent error shapes. ## See also - [HTTP Mapping (Minimal API)](./http-mapping) — the default mapping table and Problem Details shape. - [Failures and Metadata](/docs/failures-and-metadata) — the failure types being mapped. --- ## HTTP Mapping (Minimal API) The `UnambitiousFx.Functional.AspNetCore.Http` namespace provides extension methods that turn `Result`, `Result`, and `Maybe` into ASP.NET Core `IResult` responses for Minimal APIs. Each extension returns an awaitable **builder** you can configure fluently, then `await` to produce the final `IResult`. ```csharp using UnambitiousFx.Functional.AspNetCore.Http; ``` ## The builder entry points `AsHttpBuilder` is the single entry point. It is overloaded for synchronous and asynchronous (`ValueTask`) values, and accepts an optional `IFailureHttpMapper` (falls back to `DefaultFailureHttpMapper` when omitted): ```csharp // Result (no value) → ResultHttpBuilder result.AsHttpBuilder(); valueTaskOfResult.AsHttpBuilder(); // Result → ResultHttpBuilder resultOfT.AsHttpBuilder(); valueTaskOfResultOfT.AsHttpBuilder(); // Maybe → MaybeHttpBuilder maybe.AsHttpBuilder(); valueTaskOfMaybe.AsHttpBuilder(); // Provide a specific mapper (e.g. from DI) resultOfT.AsHttpBuilder(myFailureMapper); ``` Every builder exposes `GetAwaiter()`, so you can `await` it directly in an endpoint handler: ```csharp app.MapGet("/users/{id:guid}", async (Guid id, IUserService service) => await service.GetUserAsync(id).AsHttpBuilder()); ``` ## Default success mapping | Source | Default success response | | ----------------------- | --------------------------------- | | `Result` (no value) | 204 No Content | | `Result` | 200 OK + value as JSON | | `Maybe` — `Some` | 200 OK + value as JSON | | `Maybe` — `None` | 404 Not Found | Failure branches are always routed through the failure mapper (see [Failure mapping](#failure-to-status-mapping) below). ## Configuring the response All builders are immutable — each method returns a new builder. ### Override the success status code ```csharp app.MapPost("/jobs", async (CreateJob cmd, IJobService service) => await service.QueueAsync(cmd) .AsHttpBuilder() .WithStatusCode(202)); // 202 Accepted ``` Recognized success codes map to typed results: `200` → `Ok`, `201` → `Created`, `202` → `Accepted`, `204` → `NoContent`. Any other code falls back to `Results.StatusCode(code)`. ### 201 Created with a Location `AsCreated` sets the status to 201 and builds a `Location` header from the success value: ```csharp app.MapPost("/users", async (CreateUserRequest request, IUserService service) => await service.CreateAsync(request) .AsHttpBuilder() .AsCreated(user => $"/users/{user.Id}")); ``` ### Add response headers ```csharp await service.GetUserAsync(id) .AsHttpBuilder() .WithHeader("Cache-Control", "no-store"); ``` ### Reshape the success body `WithResponseFormatter` transforms the success value before serialization (for example, mapping a domain entity to a DTO): ```csharp await service.GetUserAsync(id) .AsHttpBuilder() .WithResponseFormatter(user => new UserDto(user.Id, user.Name)); ``` ### Customize the `None` response (Maybe only) By default `Maybe.None` becomes 404. Override it with `AsNone`: ```csharp using HttpResults = Microsoft.AspNetCore.Http.Results; await service.FindDraftAsync(id) .AsHttpBuilder() .AsNone(() => HttpResults.NoContent()); ``` ## Minimal API patterns ### Query endpoint (read) ```csharp app.MapGet("/users/{id:guid}", async (Guid id, IUserService service) => await service.GetUserAsync(id).AsHttpBuilder()); // Found → 200 OK + user // NotFound → 404 Not Found (ProblemDetails) ``` ### Optional lookup with `Maybe` ```csharp app.MapGet("/profiles/{id:guid}", async (Guid id, IProfileService service) => await service.FindProfileAsync(id).AsHttpBuilder()); // Some → 200 OK + profile // None → 404 Not Found ``` ### Create endpoint (command) ```csharp app.MapPost("/users", async (CreateUserRequest request, IUserService service) => await service.CreateAsync(request) .AsHttpBuilder() .AsCreated(user => $"/users/{user.Id}")); // Success → 201 Created + Location: /users/{id} // Failure → mapped status (e.g. 400 for ValidationFailure) ``` ### Fire-and-forget command (no value) ```csharp app.MapDelete("/users/{id:guid}", async (Guid id, IUserService service) => await service.DeleteAsync(id).AsHttpBuilder()); // Success → 204 No Content // Failure → mapped status (e.g. 404) ``` ### Endpoint design tips - Keep validation and business logic in services that return `Result` / `Maybe`. - Keep handlers thin — binding plus transport adaptation only. - Use `WithResponseFormatter` to keep domain types out of your wire contract. - Reuse the same `IFailureHttpMapper` across all endpoints for consistent error shapes. ## Failure-to-status mapping When the outcome is a failure, the builder calls `IFailureHttpMapper.GetFailureResponse(failure)` and converts the resulting `FailureHttpResponse` into an `IResult`. The built-in `DefaultFailureHttpMapper` maps the standard failure types from `UnambitiousFx.Functional.Failures`: | Failure type | HTTP status | | ------------------------ | ------------------------- | | `ValidationFailure` | 400 Bad Request | | `BadRequestFailure` | 400 Bad Request | | `NotFoundFailure` | 404 Not Found | | `UnauthorizedFailure` | 401 Unauthorized | | `UnauthenticatedFailure` | 403 Forbidden | | `ConflictFailure` | 409 Conflict | | `ExceptionalFailure` | 500 Internal Server Error | | Any other failure | 500 Internal Server Error | To change these mappings, see [Custom Mappers](./custom-mappers). ### How a `FailureHttpResponse` becomes an `IResult` The builder inspects the mapped `FailureHttpResponse`: - `null` response → `Results.Problem(statusCode: 500)`. - No `Body` → `TypedResults.StatusCode(statusCode)`. - `Body` is a `ProblemDetails` → `TypedResults.Problem(problemDetails)`. - Otherwise, by status code: `400` → `BadRequest(body)`, `401` → `Unauthorized()`, `403` → `Forbid()`, `404` → `NotFound(body)`, `409` → `Conflict(body)`, `500` → `Problem(statusCode: 500)`, anything else → `StatusCode(code)`. Any `Headers` and `ContentType` on the `FailureHttpResponse` are applied to the response. ## Problem Details shape `DefaultFailureHttpMapper` produces an RFC `ProblemDetails` body. The fields are populated per failure type: ```json // 404 from a NotFoundFailure { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4", "title": "Not Found", "status": 404, "detail": "Resource 'User' with id '42' was not found.", "code": "NOT_FOUND", "resource": "User", "identifier": "42" } ``` ```json // 400 from a ValidationFailure { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "Validation Error", "status": 400, "detail": "Name is required; Email is invalid", "code": "VALIDATION", "failure[0]": "Name is required", "failure[1]": "Email is invalid" } ``` `NotFoundFailure` adds `code`, `resource`, and `identifier` extensions; `ValidationFailure` adds `code` plus one `failure[i]` extension per message. `Conflict`, `BadRequest`, `Unauthorized`, `Unauthenticated`, and `Exceptional` failures produce a `ProblemDetails` with `title`, `detail`, `status`, and `type`. ## See also - [MVC Patterns](./mvc-patterns) — the `IActionResult` equivalents. - [Custom Mappers](./custom-mappers) — override the failure mapping. - [Result](/docs/result/) · [Maybe](/docs/maybe/) --- ## MVC Patterns The `UnambitiousFx.Functional.AspNetCore.Mvc` namespace mirrors the Minimal API integration, but produces `IActionResult` instead of `IResult` so it fits naturally into MVC controllers. ```csharp using UnambitiousFx.Functional.AspNetCore.Mvc; ``` ## The builder entry points `AsActionResultBuilder` is the entry point, overloaded for synchronous and asynchronous (`ValueTask`) values, with an optional `IFailureHttpMapper` (defaults to `DefaultFailureHttpMapper`): ```csharp // Result (no value) → ResultMvcBuilder result.AsActionResultBuilder(); valueTaskOfResult.AsActionResultBuilder(); // Result → ResultMvcBuilder resultOfT.AsActionResultBuilder(); valueTaskOfResultOfT.AsActionResultBuilder(); // Maybe → MaybeMvcBuilder maybe.AsActionResultBuilder(); valueTaskOfMaybe.AsActionResultBuilder(); // With a specific mapper (e.g. injected from DI) resultOfT.AsActionResultBuilder(myFailureMapper); ``` Every builder exposes `GetAwaiter()`, so it can be `await`ed directly from an action method. ## Default success mapping | Source | Default success response | | ----------------------- | --------------------------------- | | `Result` (no value) | 204 No Content | | `Result` | 200 OK + value as JSON | | `Maybe` — `Some` | 200 OK + value as JSON | | `Maybe` — `None` | 404 Not Found | Failure branches go through the configured [failure mapper](./http-mapping#failure-to-status-mapping). When the mapped body is a `ProblemDetails`, the MVC builder returns an `ObjectResult` with the status code from the problem details; other bodies are wrapped in the matching typed result (`BadRequestObjectResult`, `NotFoundObjectResult`, `ConflictObjectResult`, etc.). ## Basic controller ```csharp using UnambitiousFx.Functional.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc; [ApiController] [Route("api/users")] public sealed class UsersController : ControllerBase { private readonly IUserService _service; public UsersController(IUserService service) => _service = service; [HttpGet("{id:guid}")] public async Task Get(Guid id) => await _service.GetUserAsync(id).AsActionResultBuilder(); // Found → 200 OK + user // NotFound → 404 Not Found (ProblemDetails) } ``` ## Create endpoint (201 Created) `AsCreated` sets the status to 201 and builds the `Location` from the success value: ```csharp [HttpPost] public async Task Create([FromBody] CreateUserRequest request) => await _service.CreateAsync(request) .AsActionResultBuilder() .AsCreated(user => $"/api/users/{user.Id}"); // Success → 201 Created + Location header // Failure → mapped status (e.g. 400 for ValidationFailure) ``` ## Command with no value ```csharp [HttpDelete("{id:guid}")] public async Task Delete(Guid id) => await _service.DeleteAsync(id).AsActionResultBuilder(); // Success → 204 No Content // Failure → mapped status ``` ## Customizing the response The MVC builders share the same fluent surface as the Minimal API builders: ```csharp [HttpGet("{id:guid}")] public async Task Get(Guid id) => await _service.GetUserAsync(id) .AsActionResultBuilder() .WithStatusCode(200) .WithHeader("Cache-Control", "no-store") .WithResponseFormatter(user => new UserDto(user.Id, user.Name)); ``` - `WithStatusCode(int)` — override the success status (200 → `OkObjectResult`, 201 → `CreatedResult`, 202 → `AcceptedResult`, 204 → `NoContentResult`, otherwise `StatusCodeResult`). - `WithHeader(key, value)` — add a response header. - `WithResponseFormatter(map)` — reshape the success value before serialization. - `AsCreated(locationFactory)` — 201 Created with a `Location`. - `AsNone(() => IActionResult)` — override the `Maybe.None` response (defaults to `NotFoundResult`). ### `Maybe.None` override ```csharp [HttpGet("{id:guid}/draft")] public async Task GetDraft(Guid id) => await _service.FindDraftAsync(id) .AsActionResultBuilder() .AsNone(() => NoContent()); ``` ## Injecting the mapper Register the mapper once with [`AddResultHttp`](./custom-mappers), then inject `IFailureHttpMapper` and hand it to the builder so every controller shares the same error contract: ```csharp [ApiController] [Route("api/orders")] public sealed class OrdersController : ControllerBase { private readonly IOrderService _service; private readonly IFailureHttpMapper _mapper; public OrdersController(IOrderService service, IFailureHttpMapper mapper) { _service = service; _mapper = mapper; } [HttpGet("{id:guid}")] public async Task Get(Guid id) => await _service.GetOrderAsync(id).AsActionResultBuilder(_mapper); } ``` ## Guidance - Return `IActionResult` and adapt from functional outcomes at the boundary. - Keep services free of MVC-specific types. - Reuse the same `IFailureHttpMapper` across MVC and Minimal API endpoints for consistent errors. ## See also - [HTTP Mapping (Minimal API)](./http-mapping) — the `IResult` equivalents and failure-mapping details. - [Custom Mappers](./custom-mappers) — customize failure-to-status mapping. --- ## Migration from v1 This guide helps you migrate existing `UnambitiousFx.Functional` code from v1 to v2. The migration is mostly mechanical. The pipeline APIs you already use — `Bind`, `Map`, `Ensure`, `Tap`, `Recover`, and `Match` — keep their familiar shapes, so most composition code carries over unchanged. ## 1. Update packages Bump every UnambitiousFx.Functional package to v2: ```bash dotnet add package UnambitiousFx.Functional --version 2.0.0 dotnet add package UnambitiousFx.Functional.AspNetCore --version 2.0.0 dotnet add package UnambitiousFx.Functional.xunit --version 2.0.0 ``` ```xml ``` If you pin versions centrally, update them in your `Directory.Packages.props`. ## 2. Target framework v2 supports `net8.0`, `net9.0`, and `net10.0`. If your project targets an older framework, retarget it before upgrading: ```xml net8.0 ``` ## 3. Failure types and the `Failures` namespace Failures live in the `UnambitiousFx.Functional.Failures` namespace. Make sure your files import it where you construct or inspect errors: ```csharp using UnambitiousFx.Functional; using UnambitiousFx.Functional.Failures; return Result.Failure(new ValidationFailure("Email is required")); ``` The built-in failure types are: - `Failure` — generic failure (code `ERROR`). - `ValidationFailure` — one or more validation messages (code `VALIDATION`). - `NotFoundFailure` — missing resource (code `NOT_FOUND`). - `ConflictFailure` — conflicting operation, e.g. duplicate key (code `CONFLICT`). - `UnauthorizedFailure` — access denied (code `UNAUTHORIZED`). - `UnauthenticatedFailure` — caller is not authenticated (code `UNAUTHENTICATED`). - `BadRequestFailure` — malformed request (code `BAD_REQUEST`). - `TimeoutFailure` — operation exceeded its time limit (code `TIMEOUT`). - `ExceptionalFailure` — wraps a caught `Exception` (code `EXCEPTION`). - `AggregateFailure` — bundles multiple failures (code `AGGREGATE_ERROR`). Each failure exposes a machine-readable `Code` (see `FailureCodes`) alongside its human-readable `Message`. ## 4. Result state access Check result state with `IsSuccess` and `IsFailure`: ```csharp if (result.IsFailure) { // handle the error } ``` To get the error out, prefer `TryGetFailure`, which returns `true` and a non-null `Failure` when the result failed: ```csharp if (result.TryGetFailure(out var failure)) { logger.LogWarning("{Code}: {Message}", failure.Code, failure.Message); } ``` For typed results, `TryGet` extracts the value on success or the error on failure in one call: ```csharp if (result.TryGet(out var value, out var error)) { Use(value); } else { Handle(error); } ``` ## 5. Keep your existing pipeline style Composition code can stay as-is: ```csharp return Validate(input) .Bind(DoWork) .Ensure(x => x.IsValid, new ValidationFailure("Invalid state")) .Tap(_ => logger.LogInformation("done")); ``` ## 6. ASP.NET Core mapping If you use `Functional.AspNetCore`, register your failure-to-status mappings and convert results at the edge: ```csharp services.AddResultHttp(options => { options.AddMapper(StatusCodes.Status400BadRequest); options.AddMapper(StatusCodes.Status404NotFound); options.AddMapper(StatusCodes.Status409Conflict); options.AddMapper(StatusCodes.Status401Unauthorized); }); ``` The default HTTP mapping follows standard semantics (400 / 401 / 404 / 409 / 500 …). ## 7. xUnit assertions The assertion style still starts with `ShouldBe()`. Confirm your typed assertions reference v2 failure names: ```csharp result.ShouldBe() .Failure() .WhichIsValidationError() .And(failure => Assert.Contains("email", failure.Message)); ``` ## Quick migration checklist - Update NuGet package versions to `2.0.0`. - Retarget to `net8.0`, `net9.0`, or `net10.0` if needed. - Ensure `using UnambitiousFx.Functional.Failures;` is present where you build or inspect failures. - Use `IsFailure` / `IsSuccess` and `TryGetFailure` / `TryGet` for state access. - Run your tests and confirm HTTP mapping expectations. ## See also - [Functional Overview](./) - [Result](./result/) - [Maybe](./maybe/) - [Failures and Metadata](./failures-and-metadata) --- ## Using with AI These docs are built to be consumed by AI coding assistants, not just humans. Every build publishes machine-readable bundles following the [llmstxt.org](https://llmstxt.org) standard, generated by [`docusaurus-plugin-llms`](https://www.npmjs.com/package/docusaurus-plugin-llms). ## llms.txt bundles | File | Contents | |------|----------| | [`/llms.txt`](https://functional.unambitiousfx.com/llms.txt) | Index of every documentation section with links. | | [`/llms-full.txt`](https://functional.unambitiousfx.com/llms-full.txt) | The entire documentation set concatenated into one file. | Use `/llms.txt` when you want an assistant to discover the structure and pull only the pages it needs, and `/llms-full.txt` when you want to drop the whole reference into context at once. ## How to use them - **Paste a URL.** Give your assistant the `/llms.txt` or `/llms-full.txt` URL and ask it to read the docs before answering. - **Let tools fetch automatically.** Assistants and editors that understand the `llms.txt` convention can discover and load these bundles on their own. - **Attach as context.** Download `/llms-full.txt` and add it to your project or prompt so answers stay grounded in the real API rather than guesses. :::tip Prefer `/llms.txt` for day-to-day questions to keep context small, and reach for `/llms-full.txt` only when you need the complete API surface — for example, when migrating a large codebase or generating new pipelines from scratch. ::: ## A note on accuracy The bundles are generated directly from these docs, which are written against the shipped v2.0.0 API (`Result`, `Result`, `Maybe`, the `UnambitiousFx.Functional.Failures` types, and metadata). Pointing an assistant at them is the most reliable way to get code that compiles against the current library instead of training-data approximations. --- ## Async Assertions Most code that returns `Result` or `Maybe` does so asynchronously. You can always `await` the operation and then assert on the materialized value, but `Functional.xunit` also lets you build the assertion chain **directly on the awaitable** and `await` once at the end. These async extensions live in the `UnambitiousFx.Functional.xunit.ValueTasks` namespace and cover both `Task<...>` and `ValueTask<...>`: ```csharp using UnambitiousFx.Functional.xunit; // sync ShouldBe + chaining using UnambitiousFx.Functional.xunit.ValueTasks; // Task / ValueTask overloads ``` The same `ShouldBe()` / `Success()` / `Failure()` / `Some()` / `None()` / `And()` / `Map()` / `AndMessage()` methods are available as awaitable overloads, so the chain reads identically to its synchronous counterpart — you just `await` the whole expression. ## Assert without awaiting first `Task>` and `ValueTask>` both have a `ShouldBe()` overload, so you can compose the entire chain and await once. ```csharp [Fact] public async Task CreateUserAsync_ValidInput_ReturnsSuccess() { // Arrange (Given) var email = "alice@example.com"; // Act + Assert (When / Then) await _service.CreateUserAsync(email) .ShouldBe() .Success() .And(user => Assert.Equal(email, user.Email)); } ``` `Map` also chains across the await boundary: ```csharp [Fact] public async Task GetOrderAsync_ExistingId_HasExpectedTotal() { // Arrange (Given) var orderId = "ORD-1"; // Act + Assert (When / Then) await _service.GetOrderAsync(orderId) .ShouldBe() .Success() .Map(order => order.Total) .And(total => Assert.Equal(99.50m, total)); } ``` ## Async failure assertions Failure chaining works the same way; `AndMessage` has an awaitable overload. ```csharp [Fact] public async Task WithdrawAsync_InsufficientFunds_ReturnsFailure() { // Arrange (Given) var account = new Account(balance: 10m); // Act + Assert (When / Then) await account.WithdrawAsync(50m) .ShouldBe() .Failure() .AndMessage("Insufficient funds to complete the withdrawal."); } ``` ## Async Maybe assertions `Task>` and `ValueTask>` expose `ShouldBe()`, then `Some()`/`None()` with the usual `And`/`Map` chaining. ```csharp [Fact] public async Task FindUserAsync_ExistingEmail_ReturnsSome() { // Arrange (Given) var email = "john@example.com"; // Act + Assert (When / Then) await _service.FindUserAsync(email) .ShouldBe() .Some() .And(user => Assert.Equal(email, user.Email)); } [Fact] public async Task FindUserAsync_UnknownEmail_ReturnsNone() { // Arrange (Given) var email = "missing@example.com"; // Act + Assert (When / Then) await _service.FindUserAsync(email) .ShouldBe() .None(); } ``` ## Await-first is still fine If you prefer, await the operation first and assert on the synchronous value — handy when you need the value for additional setup before asserting. ```csharp [Fact] public async Task CreateUserAsync_ValidInput_ReturnsActiveUser() { // Arrange (Given) var email = "alice@example.com"; // Act (When) var result = await _service.CreateUserAsync(email); // Assert (Then) result.ShouldBe() .Success() .Where(user => user.IsActive); } ``` :::note The typed-failure narrowers (`.WhichIsValidationError()`, `.WhichIs()`, etc.) are synchronous. To use them on an async result, await the `Failure()` chain first, then continue: ```csharp var failure = await GetOrderAsync("ORD-404").ShouldBe().Failure(); failure.WhichIsNotFoundError().WithResource("Order"); ``` ::: ## See also - [Result Assertions](./result-assertions) - [Maybe Assertions](./maybe-assertions) - [Test Organization](./test-organization) --- ## Maybe Assertions `maybe.ShouldBe()` returns a `MaybeAssertion`, the entry point for asserting optional values. From there, choose the branch you expect: - `.Some(string? because = null)` returns a `SomeAssertion` carrying the value. - `.None(string? because = null)` returns a `NoneAssertion`. Each method accepts an optional `because` reason that is included in the failure message when the assertion fails. The `None` failure message also reports the unexpected value when the `Maybe` was actually `Some`. ## Asserting `Some` `.Some()` exposes the value through `Value`/`Subject`, or you can chain assertions with `.And(...)`. ```csharp [Fact] public void FindUser_ExistingEmail_ReturnsSome() { // Arrange (Given) var email = "john@example.com"; // Act (When) var maybe = FindUser(email); // Assert (Then) maybe.ShouldBe() .Some() .And(user => Assert.Equal(email, user.Email)); } ``` ## Chaining on the value: `.And` and `.Where` `.And(Action)` runs xUnit assertions and returns the same `SomeAssertion` so several can be chained. `.Where(Func, because?)` fails the test if the predicate is not satisfied. ```csharp [Fact] public void FindActiveUser_ExistingEmail_ReturnsActiveUser() { // Arrange (Given) var email = "john@example.com"; // Act (When) var maybe = FindUser(email); // Assert (Then) maybe.ShouldBe() .Some() .Where(user => user.IsActive) .And(user => Assert.Equal(email, user.Email)); } ``` `SomeAssertion` also offers `BeEquivalentTo(expected)`, `NotBeNull()`, `BeOfType()`, `SatisfyAll(params Action[])`, `Map(...)`, and `Inspect(...)` for non-breaking debugging. ```csharp [Fact] public void ParseConfig_ValidJson_ReturnsExpectedPort() { // Arrange (Given) var json = "{ \"port\": 8080 }"; // Act (When) var maybe = ParseConfig(json); // Assert (Then) maybe.ShouldBe() .Some() .Map(config => config.Port) .And(port => Assert.Equal(8080, port)); } ``` ## Asserting `None` `.None()` asserts the empty branch. There is nothing to chain onto, so the call typically ends the assertion. ```csharp [Fact] public void FindUser_UnknownEmail_ReturnsNone() { // Arrange (Given) var email = "missing@example.com"; // Act (When) var maybe = FindUser(email); // Assert (Then) maybe.ShouldBe().None("no user is registered with that email"); } ``` ## See also - [Result Assertions](./result-assertions) - [Async Assertions](./async-assertions) - [Maybe](/docs/maybe/) --- ## Result Assertions `result.ShouldBe()` is the entry point for every `Result` assertion. From there you assert the branch you expect — `.Success()` or `.Failure()` — and chain focused checks onto the value or the failure. - `Result.ShouldBe()` returns a `ResultAssertion`. - `Result.ShouldBe()` returns a `ResultAssertion` whose `.Success()` exposes the contained value. Both `Success(string? because = null)` and `Failure(string? because = null)` accept an optional reason that is included in the failure message when the assertion fails. ## Asserting success For `Result`, `.Success()` returns a `SuccessAssertion` carrying the value. Read it via `Value`/`Subject`, or chain `.And(...)`. ```csharp [Fact] public void Parse_ValidValue_ReturnsSuccess() { // Arrange (Given) var input = "42"; // Act (When) var result = Parse(input); // Assert (Then) result.ShouldBe() .Success() .And(value => Assert.Equal(42, value)); } ``` For a non-generic `Result`, `.Success()` simply asserts the success branch: ```csharp [Fact] public void Save_ValidEntity_Succeeds() { // Arrange (Given) var entity = new Entity("valid"); // Act (When) Result result = Save(entity); // Assert (Then) result.ShouldBe().Success("the entity passed validation"); } ``` ## Chaining on the value: `.And` and `.Where` `.And(Action)` runs xUnit assertions against the value and returns the same assertion, so you can chain several. `.Where(Func, because?)` fails the test if the predicate is not satisfied. ```csharp [Fact] public void CreateUser_ValidInput_ReturnsPopulatedUser() { // Arrange (Given) var email = "alice@example.com"; // Act (When) var result = CreateUser(email); // Assert (Then) result.ShouldBe() .Success() .Where(user => user.IsActive) .And(user => Assert.Equal(email, user.Email)) .And(user => Assert.NotEqual(Guid.Empty, user.Id)); } ``` Additional value helpers on `SuccessAssertion` include `BeEquivalentTo(expected)`, `NotBeNull()`, `BeOfType()`, `SatisfyAll(params Action[])`, `Map(...)`, and `Inspect(...)` for non-breaking debugging. ## Asserting failure `.Failure()` returns a `FailureAssertion` exposing the underlying `IFailure` as `Failure`/`Subject`. ```csharp [Fact] public void Withdraw_InsufficientFunds_ReturnsFailure() { // Arrange (Given) var account = new Account(balance: 10m); // Act (When) var result = account.Withdraw(50m); // Assert (Then) result.ShouldBe() .Failure() .And(failure => Assert.NotNull(failure.Message)); } ``` ## Asserting the message and code: `.AndMessage` / `.AndCode` `AndMessage(expected)` asserts the exact `Failure.Message`; `AndCode(expected)` asserts the exact `Failure.Code`. Both return the `FailureAssertion` for further chaining. ```csharp [Fact] public void Withdraw_InsufficientFunds_ReturnsExpectedError() { // Arrange (Given) var account = new Account(balance: 10m); // Act (When) var result = account.Withdraw(50m); // Assert (Then) result.ShouldBe() .Failure() .AndCode("ACCOUNT_INSUFFICIENT_FUNDS") .AndMessage("Insufficient funds to complete the withdrawal."); } ``` For looser matching, `FailureAssertion` also offers `ContainMessage(substring)`, `StartWithMessage(prefix)`, `ContainCode(substring)`, and `Where(Func, because?)`. ## Typed failures: `.WhichIs...()` After `.Failure()`, narrow the assertion to a concrete failure type. The narrowing methods fail the test with a clear message if the actual failure type does not match, and otherwise return a strongly-typed assertion exposing that failure's properties. | Method | Returns | | ------------------------------- | ---------------------------------------- | | `.WhichIsValidationError()` | `ValidationFailureAssertion` | | `.WhichIsNotFoundError()` | `NotFoundFailureAssertion` | | `.WhichIsConflictError()` | `ConflictFailureAssertion` | | `.WhichIsBadRequestError()` | `BadRequestFailureAssertion` | | `.WhichIsTimeoutError()` | `TypedFailureAssertion` | | `.WhichIsUnauthenticatedError()`| `TypedFailureAssertion` | | `.WhichIsUnauthorizedError()` | `TypedFailureAssertion` | | `.WhichIs()` | `TypedFailureAssertion` (any `IFailure`) | ### Validation failures `ValidationFailureAssertion` adds `WithFailure(expected)`, `WithFailureContaining(text)`, and `WithFailureCount(count)` over the `Failures` collection, plus `And`, `AndMessage`, and `AndCode`. ```csharp [Fact] public void Register_BlankEmail_ReturnsValidationFailure() { // Arrange (Given) var request = new RegisterRequest(Email: ""); // Act (When) var result = Register(request); // Assert (Then) result.ShouldBe() .Failure() .WhichIsValidationError() .WithFailureCount(1) .WithFailureContaining("Email"); } ``` ### Not-found failures `NotFoundFailureAssertion` adds `WithResource(expected)` and `WithIdentifier(expected)`. ```csharp [Fact] public void GetOrder_UnknownId_ReturnsNotFound() { // Arrange (Given) var orderId = "ORD-404"; // Act (When) var result = GetOrder(orderId); // Assert (Then) result.ShouldBe() .Failure() .WhichIsNotFoundError() .WithResource("Order") .WithIdentifier(orderId); } ``` ### Conflict and bad-request failures `ConflictFailureAssertion` and `BadRequestFailureAssertion` expose `And`, `AndMessage`, and `AndCode`. ```csharp [Fact] public void Register_DuplicateEmail_ReturnsConflict() { // Arrange (Given) var request = new RegisterRequest(Email: "taken@example.com"); // Act (When) var result = Register(request); // Assert (Then) result.ShouldBe() .Failure() .WhichIsConflictError() .AndCode("USER_EMAIL_CONFLICT"); } ``` ### Any failure type via `.WhichIs()` Use the generic form for custom failures (any type implementing `IFailure`). It returns a `TypedFailureAssertion` supporting `And`, `Where`, `AndMessage`, and `AndCode`. ```csharp [Fact] public void Charge_ExpiredCard_ReturnsPaymentFailure() { // Arrange (Given) var card = new Card(isExpired: true); // Act (When) var result = Charge(card, amount: 25m); // Assert (Then) result.ShouldBe() .Failure() .WhichIs() .Where(f => f.Declined) .AndCode("CARD_EXPIRED"); } ``` ## See also - [Maybe Assertions](./maybe-assertions) - [Async Assertions](./async-assertions) - [Result](/docs/result/) - [Failures and Metadata](/docs/failures-and-metadata) --- ## Test Organization Fluent assertions are most valuable inside well-structured tests. This page captures the conventions used across the UnambitiousFx.Functional family so suites stay readable and diagnosable. ## Arrange-Act-Assert (Gherkin style) Every test follows the **Arrange-Act-Assert (AAA)** pattern, which maps directly onto **Gherkin (Given-When-Then)**. Use comments to separate the three sections: - **Arrange (Given)** — set up conditions, initialize objects, prepare input. - **Act (When)** — execute the single operation under test. - **Assert (Then)** — verify the outcome with `ShouldBe()` assertions. ```csharp [Fact] public void Map_WithSuccessResult_TransformsValue() { // Arrange (Given) var result = Result.Success(5); // Act (When) var mapped = result.Map(x => x * 2); // Assert (Then) mapped.ShouldBe() .Success() .And(value => Assert.Equal(10, value)); } ``` ## Descriptive test names Name tests `MethodName_Scenario_ExpectedBehavior`. The name should read as a sentence describing the behavior, so a failing test communicates intent without opening the body. ```csharp [Fact] public void Withdraw_InsufficientFunds_ReturnsValidationFailure() { /* ... */ } [Fact] public void FindUser_UnknownEmail_ReturnsNone() { /* ... */ } ``` ## Group by behavior Organize tests around behavior and expected outcome rather than implementation details: - Success flows - Validation failures - Not-found and authorization failures - Optional-value (`Maybe`) behavior Keep one main behavior per test. Use the fluent chain to assert several facets of a single outcome, but avoid testing unrelated concerns in the same method. ## Theories for variation Use `[Theory]` with `[InlineData]` to exercise the same logic across multiple inputs. This keeps coverage high without duplicating the AAA scaffold. ```csharp [Theory] [InlineData("1", 1)] [InlineData("42", 42)] [InlineData("-7", -7)] public void Parse_ValidInteger_ReturnsSuccess(string input, int expected) { // Arrange (Given) — input provided by InlineData // Act (When) var result = Parse(input); // Assert (Then) result.ShouldBe() .Success() .And(value => Assert.Equal(expected, value)); } [Theory] [InlineData("")] [InlineData("abc")] [InlineData("3.14")] public void Parse_NonInteger_ReturnsValidationFailure(string input) { // Act (When) var result = Parse(input); // Assert (Then) result.ShouldBe() .Failure() .WhichIsValidationError(); } ``` ## Edge-case checklist Always probe the boundaries, not just the happy path: - **Nulls** — null arguments, null fields, and `NotBeNull()` on values that must be present. - **Empty** — empty strings, empty collections, and zero counts. - **Boundaries** — min/max values, off-by-one limits, first/last elements. - **Error states** — each failure type the operation can produce (`ValidationFailure`, `NotFoundFailure`, `ConflictFailure`, `BadRequestFailure`, custom `IFailure`). Prefer asserting **concrete failure types** with the `.WhichIs...()` narrowers over checking only a message string — the test then documents which failure the code is contracted to return. ## Improve diagnostic quality - Pass a `because` reason to `Success`, `Failure`, `Some`, `None`, and `Where` so red tests explain the expectation. - Give domain failures meaningful messages and stable codes; assert them with `AndCode`/`AndMessage`. - Use `Inspect(...)` on a chain to log intermediate state while debugging without breaking the assertion flow. ## See also - [Result Assertions](./result-assertions) - [Maybe Assertions](./maybe-assertions) - [Failures and Metadata](/docs/failures-and-metadata)