RZ.Foundation 8.4.0

dotnet add package RZ.Foundation --version 8.4.0
                    
NuGet\Install-Package RZ.Foundation -Version 8.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RZ.Foundation" Version="8.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RZ.Foundation" Version="8.4.0" />
                    
Directory.Packages.props
<PackageReference Include="RZ.Foundation" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RZ.Foundation --version 8.4.0
                    
#r "nuget: RZ.Foundation, 8.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RZ.Foundation@8.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RZ.Foundation&version=8.4.0
                    
Install as a Cake Addin
#tool nuget:?package=RZ.Foundation&version=8.4.0
                    
Install as a Cake Tool

RZ.Foundation

RZ.Foundation is a functional add-on to LanguageExt. Its centrepiece is Outcome<T>: a result type that brings Go's "errors are values" philosophy to C#. Instead of throwing exceptions and hoping someone, somewhere, remembers to catch, a function simply returns either its value or a structured ErrorInfo — and the compiler keeps you honest about which one you got.

// The signature tells the whole story: this can fail, and you must deal with it.
Outcome<User> FindUser(string id);

Why Outcome<T>?

In the exception model, the failure path is invisible. A method that returns User looks total, but may blow up the call stack at runtime. You only discover the failure modes by reading the implementation — or in production.

Outcome<T> makes failure a first-class, visible part of the return value:

  • Explicit — failure is in the type signature, not hidden in a comment.
  • Local — you handle (or deliberately forward) the error right where it happens.
  • Cheap — no stack unwinding on the expected-error path; exceptions are reserved for the genuinely exceptional and for system boundaries.

Go vs. C#, side by side

// Go
v, err := DoThing()
if err != nil {
    return err          // forward the error upward
}
use(v)
// C# with RZ.Foundation
if (Fail(DoThing(), out var e, out var v))
    return e.Trace();   // forward the error upward (and record this hop — see below)
Use(v);

Same shape, same intent. The C# side gets something extra for free: calling e.Trace() stamps the current file/method/line onto the error as it travels up, so by the time it surfaces you have a readable, application-level breadcrumb trail.

All examples below assume the project's global usings are in effect: using static RZ.Foundation.AOT.Prelude; and using static RZ.Foundation.StandardErrorCodes;. Outcome<T> lives in RZ.Foundation; ErrorInfo in RZ.Foundation.Types.

Outcome<T> in 60 seconds

An Outcome<T> is either a success carrying a T or a failure carrying an ErrorInfo. You rarely construct it explicitly — values and errors implicitly convert:

Outcome<int>  ok  = 42;                              // success, by implicit conversion
Outcome<int>  bad = ErrorInfo.New(INVALID_REQUEST, "Invalid parameter");  // failure, by a helper function with location attached

// Explicit factories when type inference needs a nudge:
var a = SuccessOutcome(42);            // Outcome<int> success
var b = FailedOutcome<int>(ErrorInfo._NotFound());
var u = UnitOutcome;                   // Outcome<Unit>, the "succeeded, nothing to return" case

A function returning Outcome<T> therefore reads naturally — just return value; or return error;:

Outcome<int> Divide(int a, int b)
    => b == 0 ? ErrorInfo.New(INVALID_REQUEST, "division by zero") : a / b;

ErrorInfo — the structured replacement for Exception

ErrorInfo is what Outcome<T> carries on failure. It is an immutable record designed to describe an error well enough that you never need an exception type per failure mode.

Field Purpose
Code Machine-readable category, e.g. "not-found"
Message Human-readable description
TraceId Distributed-trace id, auto-filled from Activity.Current
DebugInfo Extra developer detail (omit in release responses)
Data Serialized payload associated with the error
InnerError The underlying cause, when this error wraps another
SubErrors Sibling failures, when several things failed at once
Locations The application-side call trail accumulated by Trace()

Use the well-known codes from StandardErrorCodes (NOT_FOUND, INVALID_REQUEST, TIMEOUT, CANCELLED, DUPLICATION, HTTP_ERROR, UNHANDLED, VALIDATION_FAILED, …) rather than inventing strings. Every code also exists under a legacy PascalCase name (NotFound) — the same string, kept for compatibility. Prefer the UPPER_SNAKE_CASE form in new code:

var notFound = ErrorInfo.New(NOT_FOUND, "user 42 does not exist");
var invalid  = ErrorInfo.New(INVALID_REQUEST, "email is required");

// Inspect — Is() searches InnerError and SubErrors too, not just the top-level Code:
notFound.Is(NOT_FOUND);       // true
notFound.IsNotFound();        // true  — shorthand for the common case

// Re-tag a low-level error with higher-level context:
var wrapped = dbError.Wrap(SERVICE_ERROR, "failed to load user profile");

Synchronous flow

The everyday pattern is the guard: pull the value out, or short-circuit. Two mirror-image helpers cover it.

Success(...) is true on the happy path:

if (Success(FindUser(id), out var user, out var error))
    Render(user);
else
    Log(error);

Fail(...) is true on the failure path — perfect for early-return propagation:

Outcome<Invoice> BuildInvoice(string userId, string itemId) {
    if (Fail(FindUser(userId), out var e, out var user)) return e.Trace();
    if (Fail(FindItem(itemId), out e,  out var item)) return e.Trace();

    return new Invoice(user, item);   // implicit conversion to Outcome<Invoice>
}

Always forward with return e.Trace();, not bare return e;. Trace() appends the current file/method/line to the error's Locations. As the error bubbles through BuildInvoice → its caller → its caller, each hop leaves a marker, giving you an application-level trace that reads in your own terms — far more useful than a raw CLR stack trace, and it costs nothing on the success path.

There are convenient overloads. Take only what you need:

if (Fail(result, out var e)) return e.Trace();   // don't care about the value
if (Success(result, out var v)) Use(v);          // don't care about the error

Tuple results deconstruct in one step:

if (Fail(LoadPair(), out var e, out var first, out var second)) return e.Trace();
// first and second are in scope here

To collapse an Outcome<T> into a plain value, use Match or IfFail:

var label = FindUser(id).Match(u => u.Name, _ => "(unknown)");
var count = CountRows().IfFail(0);          // default on failure
var count2 = CountRows().IfFail(e => -1);   // compute a default from the error

Asynchronous flow

Async code returns ValueTask<Outcome<T>>. The pattern is identical — await first, then guard exactly as in the synchronous case:

async ValueTask<Outcome<Profile>> LoadProfile(string id) {
    if (Fail(await FetchUser(id), out var e, out var user)) return e.Trace();
    if (Fail(await FetchAvatar(user), out e, out var avatar)) return e.Trace();

    return new Profile(user, avatar);
}

When the work might throw (an HTTP call, a DB driver), wrap it with TryCatch to turn an exception into a failed Outcome instead of letting it escape (see Bridging with exceptions for the full set):

ValueTask<Outcome<string>> GetBody(Uri url) =>
    TryCatch(async () => await httpClient.GetStringAsync(url));
// a thrown HttpRequestException becomes a failed Outcome<string>

The NotFound convention

There is one error code the library treats specially: NotFound. It exists for a common situation — a lookup that may legitimately find nothing, where "nothing" is an ordinary outcome rather than a real failure.

You could model that with Outcome<Option<T>>: success-with-Some, success-with-None, or failure. It is precise, but every caller now has to unwrap two layers. The lighter alternative is to return a plain Outcome<T> and signal absence with a NotFound failure:

// Returns the user, or a NotFound failure if there is no such id.
Outcome<User> FindUser(string id)
    => TryLookup(id) is { } user ? user : ErrorInfo._NotFound(); // note, function form for attaching location

Callers that don't care about the distinction just forward like any other error. Callers that do care get purpose-built helpers so "missing" never gets mistaken for "broken":

// Forward real errors, but let "not found" fall through to a fallback.
if (FailButNotFound(FindUser(id), out var e, out var user)) return e.Trace();
var effective = user ?? User.Guest;       // not-found ⇒ user is null here

// Or substitute a fallback inline — only not-found is replaced; real errors pass through:
Outcome<User> u  = FindUser(id).IfNotFound(User.Guest);
Outcome<User> u2 = FindUser(id).IfNotFound(() => LoadDefault());

// Test it directly:
if (FindUser(id).IsNotFound()) ...

// When a caller genuinely wants the Option<T> shape back, recover it:
Outcome<Option<User>> maybe = FindUser(id).CheckNotFound();   // not-found ⇒ success(None)

// At an exception boundary, not-found becomes null instead of throwing:
User? user = await ThrowUnlessNotFound(FindUserAsync(id));

Pitfall — document the behaviour. Returning NotFound from an Outcome<T> is exactly like List.FindIndex returning -1: a useful shortcut that is invisible from the signature. If a function uses NotFound as a normal, expected result, say so in its doc comment — otherwise a caller will treat the absence as an error and propagate it. Reach for Outcome<Option<T>> instead when the distinction must be impossible to miss.

Streaming with IAsyncEnumerable<Outcome<T>>

A risky async stream can be turned into a stream of outcomes with TryCatch, which wraps each item and emits a trailing failure item if iteration throws:

IAsyncEnumerable<Outcome<Row>> rows = TryCatch(ReadRowsAsync(query));

To consume the whole stream into a list, use MakeList (read-only) or MakeMutableList (a List<T>). Both short-circuit on the first failure — you get the value list, or the first error, never a half-built list paired with a swallowed exception:

// Outcome<IReadOnlyList<Row>> — all rows, or the first error encountered
if (Fail(await rows.MakeList(), out var e, out var allRows)) return e.Trace();
Process(allRows);

// MakeMutableList when you need to keep appending afterwards:
if (Fail(await TryCatch(ReadRowsAsync(query)).MakeMutableList(), out var e2, out var list))
    return e2.Trace();
list.Add(extraRow);

Both have a selector overload that projects each item while collecting:

// Outcome<IReadOnlyList<string>>
var names = await TryCatch(ReadUsersAsync()).MakeList(u => u.Name);

Note: if the source is known to be empty, MakeList / MakeMutableList return ErrorInfo.NotFound rather than an empty list — handle it with FailButNotFound or IfNotFound if "no rows" is acceptable for your case.

The same extension set also offers First, Last, Average, and AverageBy, each returning an Outcome<T> that short-circuits on the first failing item.

Bridging with exceptions

Outcome<T> interoperates cleanly with exception-based code at both ends.

Pulling exceptions in. TryCatch (sync and async) converts a throwing call into an Outcome; Try returns a plain (Exception?, T) tuple you can convert later:

Outcome<int> parsed = TryCatch(() => int.Parse(input));      // sync

Pushing errors out. At an API boundary where callers expect exceptions, unwrap:

var user = FindUser(id).Unwrap();                 // throws ErrorInfoException if failed
var user2 = await ThrowIfError(LoadProfile(id));  // same, for ValueTask<Outcome<T>>
var maybe = await ThrowUnlessNotFound(Load(id));  // throws on real errors; null on not-found

Custom exceptions → codes. Tag an exception type with [ErrorInfo] and ErrorFrom will map it to the right Code automatically; otherwise it becomes Unhandled:

[ErrorInfo(DUPLICATION)]
sealed class DuplicateKeyException(string msg) : Exception(msg);

// Anywhere a caught exception needs to become an ErrorInfo:
ErrorInfo info = ErrorFrom.Exception(caughtException);   // honours [ErrorInfo]
ErrorInfo prog = ErrorFrom.Program("invalid state");     // quick InvalidRequest with caller name

Cancellation and timeouts get their own codes rather than being lumped into Unhandled, so a caller can tell "the user walked away" apart from "the service broke":

Caught exception Code
TaskCanceledException wrapping a TimeoutException TIMEOUT
any other OperationCanceledException CANCELLED
TimeoutException TIMEOUT

(The first row is the shape HttpClient produces when its own request timeout elapses.)

if (Fail(await CallService(ct), out var e, out var v)) {
    if (e.Is(CANCELLED)) return;          // caller gave up — nothing to report
    if (e.Is(TIMEOUT))   return e.Trace(); // worth retrying / alerting
    return e.Trace();
}

An AggregateException — several operations failing together — keeps all of its failures: each inner exception is mapped by the same rules and collected into SubErrors, so nothing is silently dropped. Is(code) searches those sub-errors as well.

ErrorInfoException is the bridge type: it carries Code and DebugInfo, and round-trips via .ToErrorInfo().

Deprecated: assigning a LanguageExt.Common.Error straight to an Outcome<T> still compiles but is now marked obsolete, as the library moves away from depending on LanguageExt types in its public surface. Convert explicitly instead — ErrorFrom.Exception(error.ToException()) — or build the ErrorInfo you actually want.

Composing outcomes (Experimental)

Outcome<T> supports the usual functional combinators:

var len = FindUser(id).Map(u => u.Name.Length);
var inv = FindUser(id).Bind(u => BuildInvoice(u.Id, itemId));

It also offers LINQ query syntax (from … from … select), including an async form over ValueTask<Outcome<T>>:

var result = from u in FindUser(id)
             from i in FindItem(itemId)
             select new Invoice(u, i);

⚠️ Caution: the LINQ form is not reliable for Outcome<T> — prefer the guard style. The query syntax compiles into nested SelectMany/Select closures and, in the async case, a generated state machine. In practice this has broken under code that participates in ambient, flow-sensitive state — for example operations running inside a MongoDB transaction, where the LINQ-generated state machine appears to disrupt the transaction context and the call fails. Until this is understood and fixed, treat LINQ over Outcome<T> as experimental: use the Success/Fail guard pattern (above) for anything inside a transaction or other context-sensitive scope.

Helper cheat-sheet

Need Use
Make a success / failure SuccessOutcome(v) / FailedOutcome<T>(e) (or just return v; / return ErrorInfo.New(code, msg);)
Happy-path guard if (Success(x, out var v, out var e))
Forward an error early if (Fail(x, out var e, out var v)) return e.Trace();
Treat not-found as non-error FailButNotFound(x, out var e, out var v)
Turn a throwing call into an Outcome TryCatch(() => ...) / TryCatch(async ...)
Collapse to a value x.Match(onOk, onErr) / x.IfFail(default)
Exit to exception-based code x.Unwrap() / await ThrowIfError(task)
Collect an async stream await TryCatch(stream).MakeList() / .MakeMutableList()
Build a structured error ErrorInfo.New(code, message) / StandardErrorCodes
Add app-level trace while propagating error.Trace()
Wrap a low-level error in context error.Wrap(code, message)

HTTP helpers

RZ.Foundation.Extensions puts the same railway over HttpClient. Every verb returns ValueTask<Outcome<HttpResponseMessage>> instead of throwing, and each accepts either a string URL or a System.Uri:

using RZ.Foundation.Extensions;

var r1 = await http.Get("https://api.example.com/users/42");
var r2 = await http.Get(new Uri("https://api.example.com/users/42"));   // Uri overload
var r3 = await http.Post(uri, content);   // also Put, Patch, Delete
var r4 = await http.TrySend(request);     // named TrySend: HttpClient already has an instance Send

Reading the response is a single chained call — no intermediate variable, and the response is closed for you:

if (Fail(await http.Get(uri).ReadString(), out var e, out var body)) return e.Trace();
var bytes = await http.Get(uri).ReadByteArray();

A non-2xx status is a failure, carrying the HttpError code with the status code and reason phrase recorded in Data — so a 404 and a 500 are distinguishable without re-reading the response.

JSON over HTTP

// Send
await http.PostJson(uri, dto).MustSucceed();     // asserts 2xx AND an empty body
await http.PutJson(uri, dto).CheckSucceed();     // asserts 2xx, ignores the body

// Receive — chained straight off the verb
if (Fail(await http.Get(uri).DeserializedJson<User>(), out var e, out var user)) return e.Trace();

// ExpectJson / MustSucceed / CheckSucceed also exist on a response you already hold:
if (Fail(await http.Get(uri), out var e2, out var response)) return e2.Trace();
var u2 = await response.ExpectJson<User>();   // 2xx + body, else the server's own error

All of these default to RzRecommendedJsonOptions, so what you send and what you receive agree. Pass your own JsonSerializerOptions to override.

When the server answers with an error, ExpectJson / MustSucceed / CheckSucceed first try to read the body as a serialized ErrorInfo — so a failure raised on the server arrives at the client with its original Code and Message intact. If the body is anything else (HTML, plain text), you get an HttpError carrying the status and the raw body.

Streaming

ReadStream hands you the body as a Stream. The response stays alive as long as the stream does, and is disposed with it — so dispose the stream, and pass ResponseHeadersRead to avoid buffering the whole body first:

var streamed = await http.Get(uri, HttpCompletionOption.ResponseHeadersRead).ReadStream();
if (Fail(streamed, out var e, out var stream)) return e.Trace();
await using (stream)                       // closes the response too
    await stream.CopyToAsync(destination);

Without ResponseHeadersRead, HttpClient buffers the entire body before the call returns, which defeats the point of streaming a large download.

Encryption (AES-GCM)

RZ.Foundation.Helpers.Encryption provides authenticated symmetric encryption built on AES-GCM. Encrypt and Decrypt return Outcome<byte[]>, so failures — a wrong key, a malformed or tampered payload, or a platform without AES-GCM — flow through the railway instead of throwing.

AES-GCM gives two guarantees for free:

  • Confidentiality — a fresh random 12-byte nonce is generated on every Encrypt, so encrypting the same data twice produces different output.
  • Integrity / authenticity — every payload carries an authentication tag. If the payload is modified, or decrypted with the wrong key, Decrypt fails with the Encryption.TAMPER_ERROR code rather than returning corrupt bytes.

The produced payload is self-describing, laid out as [nonce (12 bytes)][tag (16 bytes)][ciphertext], so the only thing you need to keep is the key.

Getting a key

A key must be 16, 24 or 32 bytes (AES-128/192/256).

using RZ.Foundation.Helpers;

// A true random 256-bit key (store it somewhere safe):
byte[] key = Encryption.RandomAesKey();

To derive the key deterministically from a string — so it can be re-created on demand and never stored — pick the method that matches your input. The name makes the security choice explicit:

// For a human passphrase / low-entropy text — PBKDF2-HMAC-SHA256 (slow, adds brute-force resistance):
byte[] key = Encryption.CreateAesKeyFromWeakText("correct horse battery staple").Unwrap();

// For text that is ALREADY high-entropy (e.g. a generated / base64 secret) — HKDF-SHA256 (fast):
byte[] key = Encryption.CreateAesKeyFromStrongText(mySecretToken).Unwrap();

// Both default to a 32-byte (AES-256) key; pass n = 16 or 24 for AES-128 / AES-192.

Deterministic derivation is only as strong as the entropy of the input string. For a key that does not need to be reproducible from text, prefer RandomAesKey().

Encrypting and decrypting

using System.Text;
using RZ.Foundation;          // Outcome<T>
using RZ.Foundation.Helpers;

byte[] key  = Encryption.RandomAesKey();
byte[] data = Encoding.UTF8.GetBytes("secret message");

Outcome<byte[]> roundTrip =
    Encryption.Encrypt(key, data)
              .Bind(payload => Encryption.Decrypt(key, payload));

string text = roundTrip.Match(
    plain => Encoding.UTF8.GetString(plain),
    error => $"failed: {error.Message}");

Detecting tampering

var payload = Encryption.Encrypt(key, data).Unwrap();
payload[^1] ^= 0xFF;   // flip a byte of the ciphertext

var result = Encryption.Decrypt(key, payload);
if (result.IfFail(out var error) && error.Is(Encryption.TAMPER_ERROR))
    Console.WriteLine("Payload was tampered with (or the key is wrong).");
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (14)

Showing the top 5 NuGet packages that depend on RZ.Foundation:

Package Downloads
RZ.Foundation.NewtonsoftJson

RZ.Foundation extension for Newtonsoft.JSON

TiraxTech.Uri

Customizable, Immutable URI record

RZ.Foundation.Blazor

Package Description

RZ.Linq.RelationalDatabase

Transform LINQ to SQL text

RZ.AspNet.Bootstrapper

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
8.4.0 88 7/26/2026
8.3.0 158 6/29/2026
8.2.15 137 4/26/2026
8.2.14 157 3/29/2026
8.2.13 131 3/28/2026
8.2.12 131 3/27/2026
8.2.11 143 3/24/2026
8.2.10 117 3/23/2026
8.2.9 483 2/7/2026
8.2.6 269 1/30/2026
8.1.0 224 1/28/2026
7.0.5 542 6/12/2025
7.0.4 300 4/24/2025
7.0.2 369 3/9/2025
7.0.0 291 2/4/2025
7.0.0-beta.26 132 10/15/2024
7.0.0-beta.12 125 9/25/2024
7.0.0-beta.11 138 9/22/2024
7.0.0-beta.3 154 6/23/2024
7.0.0-alpha.3 151 6/22/2024
Loading failed

Embrace LanguageExt lib.