Esox.SharpAndRusty 1.4.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Esox.SharpAndRusty --version 1.4.0
                    
NuGet\Install-Package Esox.SharpAndRusty -Version 1.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="Esox.SharpAndRusty" Version="1.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Esox.SharpAndRusty" Version="1.4.0" />
                    
Directory.Packages.props
<PackageReference Include="Esox.SharpAndRusty" />
                    
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 Esox.SharpAndRusty --version 1.4.0
                    
#r "nuget: Esox.SharpAndRusty, 1.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 Esox.SharpAndRusty@1.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=Esox.SharpAndRusty&version=1.4.0
                    
Install as a Cake Addin
#tool nuget:?package=Esox.SharpAndRusty&version=1.4.0
                    
Install as a Cake Tool

Esox.SharpAndRusty

A production-ready C# library that brings Rust-inspired patterns to .NET, including Result<T, E> for type-safe error handling and Option<T> for representing optional values without null references.

⚠️ Disclaimer

This library is provided "as is" without warranty of any kind, either express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.

Use at your own risk. While this library has been designed to be production-ready with comprehensive test coverage, it is your responsibility to evaluate its suitability for your specific use case and to test it thoroughly in your environment before deploying to production.

Features

  • Type-Safe Error Handling: Explicitly represent success and failure states in your type signatures
  • Option Type: Rust-inspired Option<T> for representing optional values without null references
  • Rust-Inspired API: Familiar patterns for developers coming from Rust or functional programming
  • Rich Error Type: Rust-inspired Error type with context chaining, metadata, and error categorization
  • Zero Overhead: Implemented as a readonly struct for optimal performance
  • Functional Composition: Chain operations with Map, Bind, MapError, and OrElse
  • Pattern Matching: Use the Match method for elegant success/failure handling
  • Full Equality Support: Implements IEquatable<T> with proper ==, !=, and GetHashCode()
  • Safe Value Extraction: TryGetValue, UnwrapOr, UnwrapOrElse, Expect, and Contains methods
  • Exception Handling Helpers: Built-in Try and TryAsync for wrapping operations
  • Inspection Methods: Execute side effects with Inspect, InspectErr, and Tap
  • LINQ Query Syntax: Full support for C# LINQ query comprehension with from, select, and more
  • Collection Operations: Combine and Partition for batch processing
  • Full Async Support: Complete async/await integration with MapAsync, BindAsync, TapAsync, and more
  • Cancellation Support: All async methods support CancellationToken for graceful operation cancellation
  • .NET 10 Compatible: Built for the latest .NET platform with C# 14
  • 🧪 Experimental: Mutex<T>: Rust-inspired mutual exclusion primitive with Result-based locking (works in both sync and async contexts)
  • 🧪 Experimental: RwLock<T>: Rust-inspired reader-writer lock for shared data access (works in both sync and async contexts)
  • Alternative Result DU: ExtendedResult<T, E> — a record-based discriminated union with pattern matching and LINQ support

Installation

# Clone the repository
git clone https://github.com/snoekiede/Esox.SharpAndRusty.git

# Build the project
dotnet build

# Run tests
dotnet test

Quick Start

using Esox.SharpAndRusty.Types;
using Esox.SharpAndRusty.Extensions;

// Create a successful result
var success = Result<int, string>.Ok(42);

// Create a failed result
var failure = Result<int, string>.Err("Something went wrong");

// Pattern match to handle both cases
var message = success.Match(
    success: value => $"Got value: {value}",
    failure: error => $"Got error: {error}"
);

// Or use LINQ query syntax!
var result = from x in Result<int, string>.Ok(10)
             from y in Result<int, string>.Ok(20)
             select x + y;
// Result: Ok(30)

// Use the Error type for rich error handling
var richResult = ErrorExtensions.Try(() => int.Parse("42"))
    .Context("Failed to parse user age")
    .WithMetadata("input", "42")
    .WithKind(ErrorKind.ParseError);

// Use Option<T> for optional values
Option<int> FindUser(int id) => id > 0 
    ? new Option<int>.Some(id) 
    : new Option<int>.None();

var userOption = FindUser(42);
var message = userOption switch
{
    Option<int>.Some(var id) => $"Found user {id}",
    Option<int>.None => "User not found",
    _ => "Unknown"
};

Usage Examples

ExtendedResult<T, TE> — Record Alternative

ExtendedResult<T, TE> is an immutable discriminated union implemented as a record with two cases:

  • ExtendedResult<T, TE>.Success(T Value)
  • ExtendedResult<T, TE>.Failure(TE Error)

It offers ergonomic pattern matching, safe accessors, and a rich set of extension methods for functional composition and LINQ.

Creating and Matching
using Esox.SharpAndRusty.Types;

var ok = ExtendedResult<int, string>.Ok(42);
var err = ExtendedResult<int, string>.Err("Not found");

// Pattern matching with C#
var text = ok switch
{
    ExtendedResult<int, string>.Success s => $"Value: {s.Value}",
    ExtendedResult<int, string>.Failure f => $"Error: {f.Error}",
    _ => "Unknown"
};
Safe Value Extraction
if (ok.TryGetValue(out var value)) { /* use value */ }
if (err.TryGetError(out var error)) { /* use error */ }

var v1 = ok.UnwrapOr(0);                          // 42
var v2 = err.UnwrapOrElse(e => e.Length);         // computes from error
Functional Extensions (import namespace)
using Esox.SharpAndRusty.Extensions;

var mapped = ok.Map(x => x * 2);                  // Success(84)
var bound = ok.Bind(x => ExtendedResult<int, string>.Ok(x + 1));
var projected = ok.Select(x => x.ToString());     // LINQ select
var composed = from x in ok
               from y in ExtendedResult<int, string>.Ok(8)
               select x + y;                      // Success(50)

var changedError = err.MapError(e => e.Length);   // Failure(int)
var tapped = ok.Tap(v => Log(v), e => LogErr(e)); // side effects only
Collections
var list = new[]
{
    ExtendedResult<int, string>.Ok(1),
    ExtendedResult<int, string>.Ok(2),
    ExtendedResult<int, string>.Err("bad"),
};

var combined = list.Combine();                    // Failure("bad")
var (successes, failures) = list.Partition();     // ([1,2], ["bad"]) 
Thread Safety

ExtendedResult<T, TE> instances are immutable and safe to share across threads. Thread safety of values inside (T/TE) and delegates passed to Map/Bind/etc. depends on those objects.


Option<T> - Type-Safe Optional Values

The Option<T> type represents an optional value - either Some(value) or None. This provides a type-safe alternative to nullable reference types and eliminates null reference exceptions.

Creating Options
using Esox.SharpAndRusty.Types;

// Create Some with a value
var someOption = new Option<int>.Some(42);

// Create None (no value)
var noneOption = new Option<int>.None();

// Real-world example: Safe dictionary lookup
Option<string> GetConfigValue(Dictionary<string, string> config, string key)
{
    return config.TryGetValue(key, out var value)
        ? new Option<string>.Some(value)
        : new Option<string>.None();
}
Pattern Matching with Options
Option<User> FindUser(int userId) => /* ... */;

var user = FindUser(123);
var greeting = user switch
{
    Option<User>.Some(var u) => $"Hello, {u.Name}!",
    Option<User>.None => "User not found",
    _ => "Unknown"
};

// Or use if pattern
if (user is Option<User>.Some(var foundUser))
{
    Console.WriteLine($"Processing user: {foundUser.Name}");
}
Using Options in Collections
var users = new List<Option<User>>
{
    new Option<User>.Some(new User { Id = 1, Name = "Alice" }),
    new Option<User>.None(),
    new Option<User>.Some(new User { Id = 2, Name = "Bob" })
};

// Extract all valid users
var validUsers = users
    .OfType<Option<User>.Some>()
    .Select(opt => opt.Value)
    .ToList();
// Result: [User(Alice), User(Bob)]
Record Features
// Options are records, so you get equality by value
var opt1 = new Option<int>.Some(42);
var opt2 = new Option<int>.Some(42);
Console.WriteLine(opt1 == opt2); // True

// Use with expressions to create modified copies
var updated = opt1 with { Value = 43 };
Console.WriteLine(updated); // Some { Value = 43 }

// Use in dictionaries and hash sets
var dict = new Dictionary<Option<string>, int>
{
    { new Option<string>.Some("key"), 1 },
    { new Option<string>.None(), 0 }
};
Comparison with Nullable Types
// Traditional nullable approach - prone to null reference exceptions
string? GetName(int id) => /* might return null */;
var name = GetName(123);
Console.WriteLine(name.Length); // NullReferenceException if null!

// Option approach - compiler forces you to handle None case
Option<string> GetNameSafe(int id) => /* returns Some or None */;
var nameOption = GetNameSafe(123);
var length = nameOption switch
{
    Option<string>.Some(var n) => n.Length,
    Option<string>.None => 0,
    _ => 0
};
// No risk of NullReferenceException!

Result<T, E> - Basic Operations

// Creating results
var success = Result<int, string>.Ok(42);
var failure = Result<int, string>.Err("Not found");

// Checking state
if (success.IsSuccess)
{
    Console.WriteLine("Operation succeeded!");
}

if (failure.IsFailure)
{
    Console.WriteLine("Operation failed!");
}

// Equality comparison
var result1 = Result<int, string>.Ok(42);
var result2 = Result<int, string>.Ok(42);
Console.WriteLine(result1 == result2); // True

// String representation for debugging
Console.WriteLine(success); // Output: Ok(42)
Console.WriteLine(failure); // Output: Err(Not found)

Safe Value Extraction

var result = GetUserAge();

// Option 1: Try pattern
if (result.TryGetValue(out var age))
{
    Console.WriteLine($"User is {age} years old");
}

// Option 2: Provide default value
var age = result.UnwrapOr(0);

// Option 3: Compute default based on error
var age = result.UnwrapOrElse(error => 
{
    Logger.Warn($"Failed to get age: {error}");
    return 0;
});

// Option 4: Try to get error
if (result.TryGetError(out var error))
{
    Console.WriteLine($"Error occurred: {error}");
}

// Option 5: Expect a value or throw
var age = result.Expect("Age not available");
// Throws InvalidOperationException with message "Age not available" if error

// Option 6: Check if result contains a value
if (result.Contains(42))
{
    Console.WriteLine("User is 42 years old");
}

Pattern Matching

public string ProcessResult(Result<User, string> result)
{
    return result.Match(
        success: user => $"Welcome, {user.Name}!",
        failure: error => $"Error: {error}"
    );
}

// Convert to different type
public int GetLengthOrZero(Result<string, int> result)
{
    return result.Match(
        success: text => text.Length,
        failure: errorCode => 0
    );
}

Functional Composition with Map

Transform success values while automatically propagating errors:

Result<int, string> GetUserAge() => Result<int, string>.Ok(25);

// Transform the success value
var result = GetUserAge()
    .Map<int, string, string>(age => $"User is {age} years old");
// Result: Ok("User is 25 years old")

// Errors propagate automatically
Result<int, string> failed = Result<int, string>.Err("User not found");
var mappedFailed = failed.Map<int, string, string>(age => $"User is {age} years old");
// Result: Err("User not found")

Chaining Operations with Bind

Chain multiple operations that can fail, stopping at the first error:

Result<int, string> ParseInt(string input)
{
    if (int.TryParse(input, out int value))
        return Result<int, string>.Ok(value);
    return Result<int, string>.Err($"Cannot parse '{input}' as integer");
}

Result<int, string> Divide(int numerator, int denominator)
{
    if (denominator == 0)
        return Result<int, string>.Err("Division by zero");
    return Result<int, string>.Ok(numerator / denominator);
}

Result<int, string> ValidatePositive(int value)
{
    if (value <= 0)
        return Result<int, string>.Err("Result must be positive");
    return Result<int, string>.Ok(value);
}

// Chain operations - stops at first error
var result = ParseInt("100")
    .Bind(value => Divide(value, 5))
    .Bind(value => ValidatePositive(value));
// Result: Ok(20)

var failedResult = ParseInt("100")
    .Bind(value => Divide(value, 0))
    .Bind(value => ValidatePositive(value));
// Result: Err("Division by zero") - ValidatePositive never executes

LINQ Query Syntax

Use familiar C# LINQ query syntax for elegant error handling:

// Simple query
var result = from x in ParseInt("10")
             from y in ParseInt("20")
             select x + y;
// Result: Ok(30)

// Complex query with validation
var result = from input in ParseInt("100")
             from divisor in ParseInt("5")
             from quotient in Divide(input, divisor)
             from validated in ValidatePositive(quotient)
             select $"Result: {validated}";
// Result: Ok("Result: 20")

// Error propagation - stops at first error
var result = from x in ParseInt("10")
             from y in ParseInt("abc") // Parse fails here
             from z in ParseInt("30")  // Never executes
             select x + y + z;
// Result: Err("Cannot parse 'abc' as integer")

// Works with different types
var result = from name in GetUserName(userId)
             from age in GetUserAge(userId)
             select $"{name} is {age} years old";

Note on Validation: LINQ where clauses are not supported for Result types because predicates cannot provide meaningful error messages. Instead, use Bind with explicit validation:

// ❌ where is not available (by design)
// var result = from x in GetValue()
//              where x > 5  // Cannot provide error message
//              select x * 2;

// ✅ Use Bind with explicit validation instead
var result = GetValue()
    .Bind(x => x > 5 
        ? Result<int, string>.Ok(x) 
        : Result<int, string>.Err("Value must be greater than 5"))
    .Map(x => x * 2);

// ✅ Or use validation helper functions in LINQ queries
Result<int, string> ValidatePositive(int value) =>
    value > 0
        ? Result<int, string>.Ok(value)
        : Result<int, string>.Err("Value must be positive");

var result = from x in GetValue()
             from validated in ValidatePositive(x)
             select validated * 2;

Combining Map and Bind

var result = ParseInt("42")
    .Map<int, string, int>(x => x * 2)              // Transform value: 42 -> 84
    .Bind(x => Divide(x, 2))                         // Chain operation: 84 / 2 = 42
    .Map<int, string, string>(x => $"Result: {x}"); // Transform to string
// Result: Ok("Result: 42")

Fallback with OrElse

Provide alternative results when operations fail:

Result<User, string> GetUserFromCache(int id) => 
    Result<User, string>.Err("Not in cache");

Result<User, string> GetUserFromDatabase(int id) => 
    Result<User, string>.Ok(new User { Id = id, Name = "John" });

// Try cache first, fallback to database
var user = GetUserFromCache(123)
    .OrElse(error => 
    {
        Logger.Info($"Cache miss: {error}. Trying database...");
        return GetUserFromDatabase(123);
    });
// Result: Ok(User { Id = 123, Name = "John" })

Side Effects with Inspect

Execute side effects without transforming the result:

var result = GetUser(userId)
    .Inspect(user => Logger.Info($"Found user: {user.Name}"))
    .InspectErr(error => Logger.Error($"User lookup failed: {error}"))
    .Map<User, string, string>(user => user.Email);

// Logs are written, but result is transformed only on success

Exception Handling

Wrap operations that might throw exceptions:

// Synchronous operation
var result = Result<int, string>.Try(
    operation: () => int.Parse("42"),
    errorHandler: ex => $"Parse failed: {ex.Message}"
);
// Result: Ok(42)

// Async operation
var asyncResult = await Result<User, string>.TryAsync(
    operation: async () => await httpClient.GetUserAsync(userId),
    errorHandler: ex => $"HTTP request failed: {ex.Message}"
);

// Real-world example: File operations
var fileContent = Result<string, string>.Try(
    operation: () => File.ReadAllText("config.json"),
    errorHandler: ex => $"Failed to read config: {ex.Message}"
);

// Using the Error type for automatic exception conversion
var richResult = ErrorExtensions.Try(() => int.Parse("42"));
// Automatically converts exceptions to Error with appropriate ErrorKind

var asyncRichResult = await ErrorExtensions.TryAsync(
    async () => await File.ReadAllTextAsync("config.json"));
// Returns Result<string, Error>

Rich Error Handling with Error Type

The library includes a Rust-inspired Error type for rich error handling with production-grade optimizations:

using Esox.SharpAndRusty.Types;
using Esox.SharpAndRusty.Extensions;

// Automatic exception conversion with error kinds
var result = ErrorExtensions.Try(() => File.ReadAllText("config.json"))
    .Context("Failed to load configuration")
    .WithMetadata("path", "config.json")
    .WithKind(ErrorKind.NotFound);

// Type-safe metadata with compile-time safety
var error = Error.New("Operation failed")
    .WithMetadata("userId", 123)           // Type-safe: int
    .WithMetadata("timestamp", DateTime.UtcNow)  // Type-safe: DateTime
    .WithMetadata("isRetryable", true);    // Type-safe: bool

// Type-safe metadata retrieval
if (error.TryGetMetadata("userId", out int userId))
{
    Console.WriteLine($"Failed for user: {userId}");
}

// Context chaining for error propagation
Result<Config, Error> LoadConfig(string path)
{
    return ReadFile(path)
        .Context($"Failed to load config from {path}")
        .Bind(content => ParseConfig(content)
            .Context("Failed to parse configuration")
            .WithKind(ErrorKind.ParseError));
}

// Full error chain display with metadata
if (result.TryGetError(out var error))
{
    Console.Error.WriteLine(error.GetFullMessage());
    // Output:
    // "NotFound: Failed to load configuration"
    //   [path=/etc/app/config.json]
    //   [attemptCount=3]
    //   Caused by: "Io: File not found: config.json"
}

// Error categorization with ErrorKind
if (error.Kind == ErrorKind.NotFound)
{
    // Handle not found specifically
}

// Optional stack trace capture (performance-aware)
var errorWithTrace = error.CaptureStackTrace(includeFileInfo: false);  // Fast
var detailedError = error.CaptureStackTrace(includeFileInfo: true);    // Detailed

Production Features:

  • ImmutableDictionary - Efficient metadata storage with structural sharing
  • Type-safe metadata API - Generic overloads for compile-time type safety
  • Metadata type validation - Validates at addition time, not serialization
  • Depth Limiting - Error chains truncated at 50 levels (prevents stack overflow)
  • Circular Reference Detection - HashSet-based cycle detection
  • Expanded Exception Mapping - 11 common exception types automatically mapped
  • Configurable Stack Traces - Optional file info for performance tuning
  • Metadata Type Validation - Validates types at addition time, not serialization

Error Kind Categories:

  • NotFound - Entity not found
  • InvalidInput - Invalid data
  • PermissionDenied - Insufficient privileges
  • Timeout - Operation timed out
  • Interrupted - Operation cancelled/interrupted
  • ParseError - Parsing failed
  • Io - I/O error
  • ResourceExhausted - Out of memory, disk full, etc.
  • InvalidOperation - Operation invalid for current state
  • And more... (14 categories total)

Exception to ErrorKind Mapping:

  • FileNotFoundException, DirectoryNotFoundExceptionNotFound
  • TaskCanceledException, OperationCanceledExceptionInterrupted
  • FormatExceptionParseError
  • OutOfMemoryExceptionResourceExhausted
  • TimeoutExceptionTimeout
  • UnauthorizedAccessExceptionPermissionDenied
  • And more...

See ERROR_TYPE.md for comprehensive Error type documentation. See ERROR_TYPE_PRODUCTION_IMPROVEMENTS.md for detailed production optimization information.

API Reference

Option<T> Type

A type-safe way to represent optional values, eliminating null reference exceptions.

Creating Options
  • new Option<T>.Some(T value) - Creates an option containing a value
  • new Option<T>.None() - Creates an empty option
Pattern Matching
var result = option switch
{
    Option<T>.Some(var value) => /* handle value */,
    Option<T>.None => /* handle absence */,
    _ => /* fallback */
};
Type Checks
  • option is Option<T>.Some - Check if option contains a value
  • option is Option<T>.None - Check if option is empty
  • option is Option<T>.Some(var value) - Extract value with pattern matching
Record Features
  • Equality: Options support value-based equality
  • Hash Code: Safe to use in collections (HashSet, Dictionary)
  • With Expressions: Create modified copies with with { Value = newValue }
  • ToString: Automatically formatted as "Some { Value = ... }" or "None { }"
Example Usage
// Type-safe dictionary lookup
Option<string> GetConfig(string key)
{
    return config.TryGetValue(key, out var value)
        ? new Option<string>.Some(value)
        : new Option<string>.None();
}

// Extract values from collections
var validValues = options
    .OfType<Option<T>.Some>()
    .Select(opt => opt.Value)
    .ToList();

Result<T, E> Type

Properties
  • bool IsSuccess - Returns true if the result represents success
  • bool IsFailure - Returns true if the result represents failure
Static Factory Methods
  • Result<T, E> Ok(T value) - Creates a successful result
  • Result<T, E> Err(E error) - Creates a failed result
  • Result<T, E> Try(Func<T> operation, Func<Exception, E> errorHandler) - Execute operation with exception handling
  • Task<Result<T, E>> TryAsync(Func<Task<T>> operation, Func<Exception, E> errorHandler) - Async version of Try
Instance Methods
  • R Match<R>(Func<T, R> success, Func<E, R> failure) - Pattern match on the result
  • bool TryGetValue(out T value) - Try to get the success value
  • bool TryGetError(out E error) - Try to get the error value
  • T UnwrapOr(T defaultValue) - Get value or return default
  • T UnwrapOrElse(Func<E, T> defaultFactory) - Get value or compute default
  • Result<T, E> OrElse(Func<E, Result<T, E>> alternative) - Provide alternative on failure
  • Result<T, E> Inspect(Action<T> action) - Execute action on success value
  • Result<T, E> InspectErr(Action<E> action) - Execute action on error value
Equality Methods
  • bool Equals(Result<T, E> other) - Check equality
  • int GetHashCode() - Get hash code
  • bool operator ==(Result<T, E> left, Result<T, E> right) - Equality operator
  • bool operator !=(Result<T, E> left, Result<T, E> right) - Inequality operator
  • string ToString() - Returns "Ok(value)" or "Err(error)"

Extension Methods (ResultExtensions)

Map<T, E, U>

Transforms the success value while propagating errors:

Result<U, E> Map<T, E, U>(this Result<T, E> result, Func<T, U> mapper)

Example:

var result = Result<int, string>.Ok(5);
var mapped = result.Map<int, string, string>(x => $"Value: {x}");
// Result: Ok("Value: 5")
Bind<T, E, U>

Chains operations that return results (also known as flatMap or andThen):

Result<U, E> Bind<T, E, U>(this Result<T, E> result, Func<T, Result<U, E>> binder)

Example:

var result = Result<int, string>.Ok(10)
    .Bind(x => x > 0 
        ? Result<int, string>.Ok(x * 2) 
        : Result<int, string>.Err("Must be positive"));
// Result: Ok(20)
Select<U> (LINQ Support)

Projects the success value (enables select in LINQ queries):

Result<U, E> Select<U>(this Result<T, E> result, Func<T, U> selector)

Example:

var result = from x in Result<int, string>.Ok(10)
             select x * 2;
             // Result: Ok(20)
SelectMany<U> (LINQ Support)

Chains results (enables from in LINQ queries):

Result<U, E> SelectMany<U>(this Result<T, E> result, Func<T, Result<U, E>> selector)

Example:

var result = from x in ParseInt("10")
             from y in ParseInt("20")
             select x + y;
// Result: Ok(30)
Unwrap<T, E>

Extracts the success value or throws an exception (use with caution):

T Unwrap<T, E>(this Result<T, E> result)

Example:

var result = Result<int, string>.Ok(42);
var value = result.Unwrap(); // Returns 42

var failed = Result<int, string>.Err("Error");
var willThrow = failed.Unwrap(); // Throws InvalidOperationException

ExtendedResult<T, TE> Type (API)

Static Methods
  • ExtendedResult<T, TE> Ok(T value) - Creates a successful result
  • ExtendedResult<T, TE> Err(TE error) - Creates a failed result
  • ExtendedResult<T, TE> Try(Func<T> operation, Func<Exception, TE> errorHandler) - Execute operation with exception handling
  • Task<ExtendedResult<T, TE>> TryAsync(Func<Task<T>> operation, Func<Exception, TE> errorHandler) - Async version of Try
Instance Methods
  • TR Match<TR>(Func<T, TR> success, Func<TE, TR> failure) - Pattern match on the result
  • bool TryGetValue(out T value) - Try to get the success value
  • bool TryGetError(out TE error) - Try to get the error value
  • T UnwrapOr(T defaultValue) - Get value or return default
  • T UnwrapOrElse(Func<TE, T> defaultFactory) - Get value or compute default from error
  • ExtendedResult<T, TE> OrElse(Func<TE, ExtendedResult<T, TE>> alternative) - Provide alternative on failure
  • ExtendedResult<T, TE> Inspect(Action<T> action) - Execute action on success value
  • ExtendedResult<T, TE> InspectErr(Action<TE> action) - Execute action on error value
Extension Methods (ExtendedResultExtensions)
  • T Unwrap() - Extract value or throw (use with caution)
  • T Expect(string message) - Extract value or throw with custom message
  • ExtendedResult<U, TE> Map<U>(Func<T, U> mapper) - Transform success value
  • ExtendedResult<U, TE> Bind<U>(Func<T, ExtendedResult<U, TE>> binder) - Chain operations (flatMap)
  • ExtendedResult<T, TE2> MapError<TE2>(Func<TE, TE2> errorMapper) - Transform error value
  • ExtendedResult<T, TE> Tap(Action<T> onSuccess, Action<TE> onFailure) - Side effects for both branches
  • ExtendedResult<U, TE> Select<U>(Func<T, U> selector) - LINQ projection support
  • ExtendedResult<U, TE> SelectMany<U>(Func<T, ExtendedResult<U, TE>> selector) - LINQ monadic bind
  • ExtendedResult<IEnumerable<T>, TE> Combine() (on IEnumerable<ExtendedResult<T, TE>>) - Aggregate results
  • (List<T> successes, List<TE> failures) Partition() (on IEnumerable<ExtendedResult<T, TE>>) - Split successes/failures
Equality & Hash Code
  • Supports value-based equality for Success and Failure cases
  • Null-safe hash code computation
  • Implements Equals, GetHashCode, ==, !=
  • ToString() returns "Ok(value)" or "Err(error)"

Why Use Result Types?

Traditional Exception-Based Approach

public User GetUser(int id)
{
    var user = database.FindUser(id);
    if (user == null)
        throw new NotFoundException($"User {id} not found");
    return user;
}

// Caller has no indication this method can throw
User user = GetUser(123); // Might throw at runtime!

Result-Based Approach

public Result<User, string> GetUser(int id)
{
    var user = database.FindUser(id);
    if (user == null)
        return Result<User, string>.Err($"User {id} not found");
    return Result<User, string>.Ok(user);
}

// Failure is explicit in the type signature
Result<User, string> result = GetUser(123);
var message = result.Match(
    success: user => $"Found: {user.Name}",
    failure: error => $"Error: {error}"
);

Benefits

  • Explicit Error Handling: Method signatures clearly communicate potential failures
  • Type-Safe Optional Values: Option<T> eliminates null reference exceptions
  • Type Safety: Compile-time guarantees about error handling
  • Rich Error Context: Chain error context as failures propagate up the call stack
  • Error Categorization: 14 predefined error kinds for appropriate handling
  • Performance: Avoid exception overhead for expected failure cases
  • Composability: Easily chain operations with functional combinators
  • Testability: Easier to test both success and failure paths
  • No Null References: Use Option<T> and Result<T, E> to avoid NullReferenceException
  • Better Code Flow: Failures don't break the natural flow of your code
  • Pattern Matching: Leverage C# pattern matching for elegant value handling
  • LINQ Integration: Use familiar C# query syntax for error handling workflows
  • Async/Await Support: Full integration with async patterns including cancellation
  • Cancellable Operations: Graceful cancellation of long-running async operations
  • Debugging Support: Metadata attachment and full error chain display for debugging

Testing

The library includes comprehensive test coverage with 417 unit tests covering:

  • Result<T, E> (260 tests)
    • Basic creation and inspection
    • Pattern matching
    • Equality and hash code
    • Map and Bind operations
    • LINQ query syntax integration (SelectMany, Select, from/select)
    • Advanced features (MapError, Expect, Tap, Contains)
    • Collection operations (Combine, Partition)
    • Full async support (MapAsync, BindAsync, TapAsync, OrElseAsync, CombineAsync)
    • Cancellation token support (all async methods with cancellation scenarios)
  • ExtendedResult<T, TE> (19 tests)
    • Basic creation and value/error extraction
    • Pattern matching with records
    • Instance methods (UnwrapOr, UnwrapOrElse, Inspect, InspectErr, OrElse)
    • Extension methods (Map, Bind, MapError, Tap, Unwrap, Expect)
    • LINQ support (Select, SelectMany)
    • Collection operations (Combine, Partition)
    • Static helpers (Try, TryAsync)
    • Edge cases (null values, null errors)
    • Equality and hash code
  • Option<T> (43 tests)
    • Creation and value access
    • Pattern matching with switch expressions
    • Equality and hash code
    • Record functionality (with expressions, ToString)
    • Collection integration (List, HashSet, Dictionary, LINQ)
    • Edge cases (nested options, tuples, null handling)
  • Error type (64 comprehensive tests)
    • Context chaining and error propagation
    • Type-safe metadata with generics
    • Metadata type validation
    • Exception conversion with 11 exception types
    • Error kind modification
    • Stack trace capture (configurable)
    • Depth limiting (50 levels)
    • Circular reference detection
    • Full error chain formatting
    • Equality and hash code
  • 🧪 Experimental Mutex<T> (36 tests)
    • Lock acquisition and release
    • Try-lock and timeout variants
    • Async locking with cancellation
    • Concurrency stress tests
    • RAII guard management
  • Exception handling (Try/TryAsync)
  • Side effects (Inspect/InspectErr)
  • Value extraction methods
  • Null handling for nullable types

Experimental Features

🧪 Mutex<T> & RwLock<T> - Thread-Safe Synchronization Primitives

Status: Experimental - API may change in future versions

Rust-inspired synchronization primitives for protecting shared data, suitable for both synchronous and asynchronous contexts:

Mutex<T> - Mutual Exclusion
using Esox.SharpAndRusty.Sync;
using Esox.SharpAndRusty.Types;

// Create a mutex protecting shared data
var mutex = new Mutex<int>(0);

// Synchronous locking
var result = mutex.Lock();
if (result.TryGetValue(out var guard))
{
    using (guard)
    {
        guard.Value++;  // Safe mutation
    } // Lock automatically released
}

// Non-blocking try
var tryResult = mutex.TryLock();

// Async locking with cancellation
var asyncResult = await mutex.LockAsync(cancellationToken);
RwLock<T> - Reader-Writer Lock
using Esox.SharpAndRusty.Sync;
using Esox.SharpAndRusty.Types;

// Create a reader-writer lock
var rwlock = new RwLock<int>(42);

// Multiple readers can access simultaneously
var readResult = rwlock.Read();
if (readResult.TryGetValue(out var readGuard))
{
    using (readGuard)
    {
        Console.WriteLine(readGuard.Value); // Read-only access
    }
}

// Exclusive writer access
var writeResult = rwlock.Write();
if (writeResult.TryGetValue(out var writeGuard))
{
    using (writeGuard)
    {
        writeGuard.Value = 100; // Exclusive write access
    }
}

Key Features:

  • Result-Based Locking - All lock operations return Result<Guard<T>, Error>
  • RAII Lock Management - Automatic lock release via IDisposable
  • Multiple Lock Strategies - Blocking, try-lock, and timeout variants
  • Type-Safe - Compile-time guarantees for protected data access
  • Sync & Async Support - Works in both synchronous and asynchronous contexts
  • Reader-Writer Optimization - RwLock<T> allows concurrent readers

Mutex<T> Methods:

  • Lock() - Blocking lock acquisition (sync)
  • TryLock() - Non-blocking attempt
  • TryLockTimeout(TimeSpan) - Lock with timeout
  • LockAsync(CancellationToken) - Async lock
  • LockAsyncTimeout(TimeSpan, CancellationToken) - Async lock with timeout

RwLock<T> Methods:

  • Read() - Acquire read lock (allows concurrent readers)
  • TryRead() - Non-blocking read attempt
  • TryReadTimeout(TimeSpan) - Read with timeout
  • Write() - Acquire exclusive write lock
  • TryWrite() - Non-blocking write attempt
  • TryWriteTimeout(TimeSpan) - Write with timeout

⚠️ Experimental Notice:

The Mutex<T> and RwLock<T> APIs are currently experimental and may undergo changes based on user feedback and real-world usage patterns. While fully tested, we recommend:

  • Using them in non-critical paths initially
  • Providing feedback on the API design
  • Testing thoroughly in your specific use cases
  • Being prepared for potential API changes in minor version updates

See MUTEX_DOCUMENTATION.md for complete Mutex<T> documentation and usage examples.

Why Use Result Types?

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.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.5.0 93 2/14/2026
1.4.4 88 2/8/2026
1.4.3 96 2/8/2026
1.4.1 91 1/28/2026
1.4.0 93 1/28/2026
1.3.0 92 1/18/2026
1.2.7 255 12/19/2025
1.2.6 270 12/18/2025
1.2.4 162 12/13/2025
1.2.3 427 12/10/2025
1.2.2 433 12/10/2025
1.2.1 116 12/6/2025
1.2.0 344 11/30/2025
1.1.0 137 11/28/2025
1.0.0 178 11/26/2025