BindSharp 2.1.0

dotnet add package BindSharp --version 2.1.0
                    
NuGet\Install-Package BindSharp -Version 2.1.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="BindSharp" Version="2.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="BindSharp" Version="2.1.0" />
                    
Directory.Packages.props
<PackageReference Include="BindSharp" />
                    
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 BindSharp --version 2.1.0
                    
#r "nuget: BindSharp, 2.1.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 BindSharp@2.1.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=BindSharp&version=2.1.0
                    
Install as a Cake Addin
#tool nuget:?package=BindSharp&version=2.1.0
                    
Install as a Cake Tool

BindSharp

A lightweight, powerful functional programming library for .NET that makes error handling elegant and composable. Say goodbye to messy try-catch blocks and hello to Railway-Oriented Programming! 🚂

Why BindSharp?

Traditional error handling is messy:

try
{
    var data = await FetchDataAsync();
    
    try {
        var validated = ValidateData(data);
        var transformed = TransformData(validated);
        return await SaveAsync(transformed);
    }
    catch (ValidationException vex) {
        _logger.LogError(vex, "Validation failed");
        throw; // Loses context, can't compose further
    }
}
catch (HttpRequestException hex)
{
    _logger.LogError(hex, "Network error");
    throw; // Caller has to deal with it
}
catch (Exception ex)
{
    _logger.LogError(ex, "Unknown error");
    throw;
}

With BindSharp, it's clean and composable:

using BindSharp;
using BindSharp.Extensions;

return await Result.TryAsync(() => FetchDataAsync())
    .TapErrorAsync(ex => _logger.LogError(ex, "Fetch failed"))  // Sync logging in async pipeline
    .MapErrorAsync(ex => "Network error")    
    .BindAsync(ValidateDataAsync)
    .TapErrorAsync(err => _logger.LogError("Validation failed: {Error}", err))  // Sync logging    
    .BindIfAsync(  // Conditional processing
        data => data.RequiresTransformation,
        async data => await TransformDataAsync(data)
    )    
    .BindAsync(SaveAsync)
    .DoAsync(  // Combined side effects
        data => _logger.LogInfo("Saved: {Data}", data),
        error => _logger.LogError("Save failed: {Error}", error)
    )    
    .MatchAsync(
        success => $"✅ Saved: {success}",
        error => $"❌ Failed: {error}"
    );

✨ What's New in 2.0.0

Major Release - Breaking Changes 🚀

  • 🔥 Do/DoAsync - Execute different side effects for success/failure in one method (killer feature!)
  • Cleaner API - Result.Try() instead of ResultExtensions.Try()
  • Better Organization - Extension methods in BindSharp.Extensions namespace
  • ⚠️ Breaking Changes - See MIGRATION_V2.md for upgrade guide

Migration is simple:

  1. Add using BindSharp.Extensions; to files using extension methods
  2. Replace ResultExtensions.TryResult.Try
  3. (Optional) Refactor Tap + TapError pairs to Do for cleaner code

Previous Releases:

Features

Result<T, TError> - Explicit success/failure handling
🔥 Do/DoAsync - Dual side effects in one method (new in 2.0!)
Exception-First Try - Clean exception inspection and logging
Mixed Async/Sync Pipelines - Natural composition
BindIf - Conditional processing in pipelines
Equality Support - Compare Results, use in collections
Implicit Conversions - Clean, concise syntax
Unit Type - Represent "no value" in functional pipelines
Railway-Oriented Programming - Chain operations that can fail
Full Async/Await Support - Game-changing async composition
Exception Handling - Convert try/catch into functional Results
Validation Pipelines - Business rule checking without breaking flow
Side Effects - Tap into pipelines for logging and metrics
Resource Management - Guaranteed disposal with functional style
Type-Safe - Compiler catches your mistakes
Lightweight - Zero dependencies
Compatible - Works with .NET Framework 4.6.1+ and all modern .NET

Installation

dotnet add package BindSharp

Quick Start

Basic Success and Failure

using BindSharp;

// Create results - explicit style
var success = Result<int, string>.Success(42);
var failure = Result<int, string>.Failure("Something went wrong");

// Or use implicit conversions (new in 1.3.0!)
Result<int, string> success2 = 42;
Result<int, string> failure2 = "Error occurred";

// Check the result
if (success.IsSuccess)
    Console.WriteLine(success.Value); // 42

if (failure.IsFailure)
    Console.WriteLine(failure.Error); // "Something went wrong"

// Compare results (new in 1.3.0!)
if (success == success2)  // ✅ TRUE!
    Console.WriteLine("Results are equal!");

Equality Support

New in 1.3.0! Results now implement IEquatable<Result<T, TError>> for proper value equality:

using BindSharp;

var r1 = Result<int, string>.Success(42);
var r2 = Result<int, string>.Success(42);

// Equality comparison works!
Console.WriteLine(r1 == r2);  // TRUE ✅
Console.WriteLine(r1.Equals(r2));  // TRUE ✅

// Works in collections
var set = new HashSet<Result<int, string>>();
set.Add(Result<int, string>.Success(1));
set.Add(Result<int, string>.Success(1));  // Not added (duplicate)
Console.WriteLine(set.Count);  // 1 ✅

// Use as dictionary keys
var cache = new Dictionary<Result<int, string>, string>();
cache[Result<int, string>.Success(1)] = "one";

// Better debugging
Console.WriteLine(r1);  // "Success(42)"
Console.WriteLine(failure);  // "Failure(Something went wrong)"

In Tests

[Fact]
public void Divide_ReturnsCorrectResult()
{
    var result = Calculator.Divide(10, 2);
    var expected = Result<int, string>.Success(5);
    
    Assert.Equal(expected, result);  // ✅ Now works!
}

Implicit Conversions - Cleaner Syntax

New in 1.3.0! Return values and errors directly without wrapping them:

Simple Example

using BindSharp;

// Before: Verbose
public Result<int, string> ParseAge(string input)
{
    if (string.IsNullOrWhiteSpace(input))
        return Result<int, string>.Failure("Age is required");
    
    if (!int.TryParse(input, out int age))
        return Result<int, string>.Failure("Must be a number");
    
    if (age < 0 || age > 150)
        return Result<int, string>.Failure("Invalid age");
    
    return Result<int, string>.Success(age);
}

// After: Clean! (50% less code)
public Result<int, string> ParseAge(string input)
{
    if (string.IsNullOrWhiteSpace(input)) return "Age is required";
    if (!int.TryParse(input, out int age)) return "Must be a number";
    if (age < 0 || age > 150) return "Invalid age";
    
    return age;
}

Switch Expressions

public Result<decimal, string> GetDiscount(string code)
{
    return code.ToUpper() switch
    {
        "SAVE10" => 0.10m,  // ✨ Implicit Success
        "SAVE20" => 0.20m,  // ✨ Implicit Success
        "SAVE50" => 0.50m,  // ✨ Implicit Success
        _ => "Invalid coupon code"  // ✨ Implicit Failure
    };
}

Async Operations

using BindSharp;

public async Task<Result<User, string>> GetUserAsync(int id)
{
    if (id < 0) return "Invalid ID";  // ✨ Clean!
    
    var user = await _db.FindUserAsync(id);
    if (user == null) return "User not found";  // ✨ Clean!
    
    return user;  // ✨ Clean!
}

Real-World Example

public Result<User, string> CreateUser(CreateUserRequest request)
{
    if (request == null) return "Request is null";
    if (string.IsNullOrEmpty(request.Email)) return "Email is required";
    if (string.IsNullOrEmpty(request.Password)) return "Password is required";
    if (request.Password.Length < 8) return "Password too short";
    
    return new User(request);  // ✨ 53% less code than before!
}

⚠️ CRITICAL: Implicit Conversions Warning

NEVER use the same type for both T and TError - this creates ambiguity:

// ❌ NEVER DO THIS - Ambiguous!
public Result<string, string> GetValue()
{
    return "value";  // Is this Success or Failure? Compiler can't tell!
}

// ✅ ALWAYS DO THIS - Clear!
public Result<int, string> GetValue()
{
    if (error) return "Error message";  // Clear: string = Failure
    return 42;  // Clear: int = Success
}

// ✅ OR USE CUSTOM ERROR TYPE - Even Better!
public record ErrorInfo(string Message);

public Result<string, ErrorInfo> GetValue()
{
    if (error) return new ErrorInfo("Error");  // Clear: ErrorInfo = Failure
    return "Success value";  // Clear: string = Success
}

Best Practice: Define custom error types for your domain:

public record ValidationError(string Field, string Message);
public record NotFoundError(string EntityType, string Id);
public record UnauthorizedError(string Reason);

public Result<User, ValidationError> ValidateUser(UserInput input);
public Result<Order, NotFoundError> GetOrder(string orderId);
public Result<Resource, UnauthorizedError> AccessResource(string userId);

This approach:

  • ✅ Eliminates ambiguity completely
  • ✅ Makes errors type-safe
  • ✅ Enables better error handling
  • ✅ Improves code documentation

Unit Type - Representing "No Value"

Many operations succeed but don't produce a meaningful value. The Unit type lets you maintain consistent Result<T, TError> signatures in these cases:

The Problem

// ❌ Without Unit: inconsistent return types
public async Task DeleteUserAsync(int id);      // void? Task? bool?
public bool UpdateSettings(Settings s);         // What does 'true' mean?
public int InsertRecord(Record r);              // Returning affected rows... but do we care?

// These can't be composed in Result chains

The Solution

using BindSharp;

// ✅ With Unit: consistent Result<Unit, TError> signatures everywhere
public Task<Result<Unit, string>> DeleteUserAsync(int id) =>
    Result.TryAsync(
        operation: async () => {
            await _repository.DeleteAsync(id);
            return Unit.Value;  // T = Unit (success, no value)
        },
        errorFactory: ex => $"Delete failed: {ex.Message}"
    );

Real-World Example: CRUD Operations Chain

using BindSharp;
using BindSharp.Extensions;

// Every operation returns Result<Unit, string> for perfect composition
public async Task<Result<Unit, string>> CreateUserWorkflowAsync(CreateUserRequest request)
{
    return await ValidateRequest(request)                    // Result<CreateUserRequest, string>
        .BindAsync(r => InsertUserAsync(r))                  // Result<Unit, string>
        .TapAsync(_ => SendWelcomeEmailAsync(r.Email))       // Result<Unit, string> (unchanged)
        .BindAsync(_ => InitializePreferencesAsync(r.UserId)) // Result<Unit, string>
        .TapAsync(_ => _logger.LogInfo("User workflow completed"));
    
    // Clean chain - consistent Result<Unit, string> throughout
}

private async Task<Result<Unit, string>> InsertUserAsync(CreateUserRequest request) =>
    await Result.TryAsync(
        operation: async () => {
            await _database.ExecuteAsync("INSERT INTO Users ...", request);
            return Unit.Value;  // Implicit conversion works here too!
        },
        errorFactory: ex => $"Database error: {ex.Message}"
    );

When to Use Unit

Database operations - Inserts, updates, deletes that return void or row counts you don't need
Notifications - Sending emails, SMS, push notifications
Validation - Checks that produce no output, only pass/fail
Side effects - Logging, caching, metrics wrapped in Results
Void replacements - Any operation where success matters, not the return value

Performance

Unit.Value is a singleton with almost zero memory footprint. There's no performance cost to using it - perfect for high-throughput functional pipelines!

Core Operations

Map - Transform Success Values

Transform a value when the result is successful:

using BindSharp;

Result<int, string> GetAge() => 25;

var result = GetAge()
    .Map(age => age * 2)  // 50
    .Map(age => $"Age in months: {age * 12}");  // "Age in months: 600"

// If GetAge() returned a failure, Map would skip and propagate the error

Real-world example - API response transformation:

public Result<UserDto, string> GetUser(int id)
{
    var userResult = _database.FindUser(id); // Returns Result<User, string>
    
    return userResult.Map(user => new UserDto
    {
        Id = user.Id,
        FullName = $"{user.FirstName} {user.LastName}",
        Email = user.Email
    });
}

Bind - Chain Operations That Can Fail

Chain multiple operations where each can fail:

using BindSharp;
using BindSharp.Extensions;

public abstract record EmailError(string Message, string? Details = null, Exception? Exception = null);

public sealed record EmailValidationError(string? Details = null)
    : EmailError("Invalid email", Details);
    
public sealed record SendEmailError(string? Details = null, Exception? Exception = null)
    : EmailError("Failed to send email", Details, Exception);

Result<string, EmailError> ValidateEmail(string email) =>
    email.Contains("@")
        ? email
        : new EmailValidationError("Email must contain '@' character");

Result<string, EmailError> SendEmail(string email) =>
    Result.Try(() =>
    {
        if (email.EndsWith("@blocked.com"))
            throw new InvalidOperationException("Domain is blocked");
        
        if (string.IsNullOrWhiteSpace(email))
            throw new ArgumentException("Email cannot be empty", nameof(email));
        
        // Simulate actual sending...
        Console.WriteLine($"📧 Sending email to {email}...");
        return $"Sent to {email}";
    })
    .TapError(ex => 
    {
        Console.WriteLine($"🔴 Email send error: {ex.GetType().Name} - {ex.Message}");
        if (ex.StackTrace != null)
            Console.WriteLine($"   Stack: {ex.StackTrace.Split('\n')[0]}");
    })
    .MapError(ex => ex switch
    {
        InvalidOperationException => new SendEmailError("Domain is blocked", ex),
        ArgumentException => new SendEmailError("Invalid email format", ex),
        _ => new SendEmailError("Unexpected error occurred", ex)
    });

// ✅ Success case
var successResult = ValidateEmail("user@example.com")
    .Bind(SendEmail);

Console.WriteLine(successResult.Match(
    success => $"✅ {success}",
    error => $"❌ {error.Message}" + (!string.IsNullOrEmpty(error.Details) ? $": {error.Details}" : string.Empty)
));
// Output: 
// 📧 Sending email to user@example.com...
// ✅ Sent to user@example.com

// ❌ Validation failure case
var validationFailure = ValidateEmail("invalid-email")
    .Bind(SendEmail);  // Never executes - validation failed

Console.WriteLine(validationFailure.Match(
    success => $"✅ {success}",
    error => $"❌ {error.Message}" + (!string.IsNullOrEmpty(error.Details) ? $": {error.Details}" : string.Empty)
));
// Output: ❌ Invalid email: Email must contain '@' character

// ❌ Send failure case - shows exception logging!
var sendFailure = ValidateEmail("user@blocked.com")
    .Bind(SendEmail);

Console.WriteLine(sendFailure.Match(
    success => $"✅ {success}",
    error => $"❌ {error.Message}" + (error.Details != null ? $": {error.Details}" : "")
));
// Output:
// 🔴 Email send error: InvalidOperationException - Domain is blocked
//    Stack: at SendEmail(String email) in ...
// ❌ Failed to send email: Domain is blocked

Real-world example - User registration flow:

public Result<User, string> RegisterUser(string email, string password)
{
    return ValidateEmail(email)
        .Bind(validEmail => ValidatePassword(password)
            .Map(_ => validEmail))  // Keep the email, discard password validation result
        .Bind(validEmail => CreateUser(validEmail, password))
        .Bind(user => SendWelcomeEmail(user));
}

// Each step only runs if the previous succeeded!
// First failure stops the chain and returns the error

Match - Handle Both Cases

Extract a value by handling both success and failure:

var result = DivideNumbers(10, 2);

var message = result.Match(
    success => $"Result: {success}",
    error => $"Error: {error}"
);

Console.WriteLine(message); // "Result: 5" or "Error: Division by zero"

Real-world example - API response:

public IActionResult GetProduct(int id)
{
    var result = _productService.FindProduct(id);
    
    return result.Match(
        product => Ok(product),           // 200 OK with product
        error => NotFound(new { error })  // 404 Not Found with error message
    );
}

MapError - Transform Error Values

Change the error type while preserving success:

using BindSharp;

Result<int, string> result = "404";

var transformed = result.MapError(errorCode => new 
{
    Code = int.Parse(errorCode),
    Message = "Resource not found"
});
// Result<int, { Code, Message }>

Real-world example - Error localization:

public Result<Order, LocalizedError> GetOrder(int id)
{
    return _orderRepository.FindOrder(id)  // Returns Result<Order, string>
        .MapError(errorCode => new LocalizedError
        {
            Code = errorCode,
            Message = _localizer.GetString(errorCode),
            Timestamp = DateTime.UtcNow
        });
}

BindIf - Conditional Processing

New in 1.4.1! Execute operations conditionally based on a predicate:

using BindSharp;

// Process data only if it needs processing
var result = GetData()
    .BindIf(
        data => data.RequiresProcessing,  // If TRUE
        data => ProcessData(data)         // Then execute
    );
// If predicate is FALSE, returns data unchanged

How it works:

  • Predicate returns TRUE → Continuation executes
  • Predicate returns FALSE → Original result returned unchanged (short-circuit)
  • Result is already failed → Error propagates without evaluating predicate

Real-world example - Conditional enrichment:

using BindSharp;
using BindSharp.Extensions;

public async Task<Result<User, string>> GetUserAsync(int id)
{
    return await FetchUserAsync(id)
        .BindIfAsync(
            user => !user.IsComplete,  // If incomplete (TRUE)
            async user => await EnrichFromDatabaseAsync(user)  // Then enrich
        )
        .TapAsync(async user => await CacheUserAsync(user));
}

Example - JSON extraction:

// Extract JSON only if it's NOT already in JSON format
var result = GetPayload()
    .Map(p => p.TrimStart())
    .BindIf(
        p => !(p.StartsWith("{") || p.StartsWith("[")),  // If NOT JSON (TRUE)
        p => ExtractJsonAfterPrefix(p)                    // Then extract
    );

With async predicates (database checks):

using BindSharp;
using BindSharp.Extensions;

public async Task<Result<Order, string>> ProcessOrderAsync(Order order)
{
    return await Result<Order, string>.Success(order)
        .BindIfAsync(
            async o => await RequiresValidationAsync(o.Id),  // Async check
            async o => await ValidateOrderAsync(o)           // Then validate
        );
}

Key Difference from Ensure:

  • Ensure - Validates and fails if condition is false
  • BindIf - Executes continuation if condition is true, skips if false

🚀 Async Support - The Game Changer!

This is where BindSharp really shines! Handle async operations with the same elegant composition.

MapAsync - Async Transformations

Three overloads for every scenario:

using BindSharp;
using BindSharp.Extensions;

// 1. Task<Result> + sync function
Task<Result<int, string>> asyncResult = GetUserIdAsync();
var user = await asyncResult.MapAsync(id => GetUserFromCache(id));

// 2. Result + async function
Result<int, string> userId = 42;
var user = await userId.MapAsync(async id => await FetchUserAsync(id));

// 3. Task<Result> + async function (most common!)
Task<Result<int, string>> asyncResult = GetUserIdAsync();
var user = await asyncResult.MapAsync(async id => await FetchUserAsync(id));

Real-world example - API call chain:

using BindSharp;
using BindSharp.Extensions;

public async Task<Result<OrderSummary, string>> GetOrderSummaryAsync(int orderId)
{
    return await FetchOrderAsync(orderId)
        .MapAsync(async order => await EnrichWithCustomerDataAsync(order))
        .MapAsync(async order => await CalculateTotalsAsync(order))
        .MapAsync(order => new OrderSummary(order));
    
    // Clean, readable, and each step only runs if previous succeeded!
}

BindAsync - Chain Async Operations

using BindSharp;
using BindSharp.Extensions;

public async Task<Result<Receipt, string>> ProcessPaymentAsync(PaymentRequest request)
{
    return await ValidatePaymentRequest(request)  // Result<PaymentRequest, string>
        .BindAsync(async req => await ChargeCardAsync(req))  // Task<Result<Transaction, string>>
        .BindAsync(async tx => await SaveTransactionAsync(tx))  // Task<Result<Transaction, string>>
        .MapAsync(async tx => await GenerateReceiptAsync(tx));  // Task<Result<Receipt, string>>
}

// Beautiful async composition! No nested try-catch, no ugly error handling.

Real-world example - Multi-step async workflow:

public async Task<Result<ShipmentConfirmation, string>> FulfillOrderAsync(int orderId)
{
    return await GetOrderAsync(orderId)
        .BindAsync(async order => await ValidateInventoryAsync(order))
        .BindAsync(async order => await ReserveItemsAsync(order))
        .BindAsync(async order => await CreateShipmentAsync(order))
        .BindAsync(async shipment => await NotifyCustomerAsync(shipment))
        .MapAsync(shipment => new ShipmentConfirmation(shipment));
    
    // Each async operation runs in sequence
    // First failure stops the chain immediately
    // Error is automatically propagated
}

MatchAsync - Async Result Handling

Handle async results with async handlers:

var result = await FetchDataAsync();

var output = await result.MatchAsync(
    async data => await ProcessSuccessAsync(data),
    async error => await LogErrorAsync(error)
);

Real-world example - Complete async flow:

using BindSharp;
using BindSharp.Extensions;

public async Task<IActionResult> CreateUserAsync(CreateUserRequest request)
{
    var result = await ValidateUserRequest(request)
        .BindAsync(async req => await CheckEmailAvailabilityAsync(req))
        .BindAsync(async req => await CreateUserAccountAsync(req))
        .BindAsync(async user => await SendVerificationEmailAsync(user));
    
    return await result.MatchAsync(
        async user => {
            await _auditLog.LogUserCreatedAsync(user);
            return Created($"/users/{user.Id}", user);
        },
        async error => {
            await _auditLog.LogErrorAsync(error);
            return BadRequest(new { error });
        }
    );
}

Result Utilities - Utilities for the Real World

BindSharp provides Result static class with practical utilities that handle common real-world scenarios beyond pure functional operations.

Try / TryAsync - Exception Handling

Convert exception-based code into Results:

using BindSharp;

// Synchronous - with custom error
var result = Result.Try(
    () => int.Parse(userInput),
    ex => $"Invalid number: {ex.Message}"
);

// Asynchronous - with custom error
var data = await Result.TryAsync(
    async () => await httpClient.GetStringAsync(url),
    ex => $"HTTP request failed: {ex.Message}"
);

Real-world example - API integration:

using BindSharp;
using BindSharp.Extensions;

public async Task<Result<WeatherData, string>> GetWeatherAsync(string city)
{
    return await Result.TryAsync(
            async () => await _weatherApi.GetWeatherAsync(city),
            ex => $"Failed to fetch weather for {city}: {ex.Message}"
        )
        .BindAsync(json => Result.Try(
            () => JsonSerializer.Deserialize<WeatherData>(json),
            ex => $"Invalid weather data format: {ex.Message}"
        ))
        .EnsureNotNullAsync("Weather data was null")
        .TapAsync(async weather => await _cache.SetAsync(city, weather));
}

Use custom error types:

public record ApiError(string Code, string Message, Exception? InnerException);

var result = Result.Try(
    () => ProcessData(input),
    ex => new ApiError("PROCESS_FAILED", "Data processing failed", ex)
);
// Result<Data, ApiError>

Exception-First Try - Clean Exception Handling

New in 1.6.0! Returns Result<T, Exception> for clean exception inspection before transformation:

using BindSharp;
using BindSharp.Extensions;

// Exception-first - inspect then transform
var result = Result.Try(() => int.Parse("invalid"))
    .TapError(ex => _logger.LogError(ex, "Parse failed"))  // Log with full context
    .MapError(ex => "Invalid number");  // Then transform to custom error

The Pattern: TapError → MapError

using BindSharp;
using BindSharp.Extensions;

// Clean separation: logging vs transformation
var result = Result.Try(() => File.ReadAllText("file.txt"))
    .TapError(ex => _logger.LogError(ex, "Read failed"))  // ✅ Logging (side effect)
    .MapError(ex => "Failed to read file");  // ✅ Transformation (error conversion)

// Compare to mixing concerns (avoid this):
var result = Result.Try(
    () => File.ReadAllText("file.txt"),
    ex => {
        _logger.LogError(ex, "Read failed");  // ❌ Mixed with transformation
        return "Failed to read file";
    }
);

Real-world example - Pattern matching on exception types:

using BindSharp;
using BindSharp.Extensions;

public async Task<Result<Data, string>> FetchDataAsync(string url)
{
    return await Result.TryAsync(async () => 
            await _httpClient.GetStringAsync(url))
        .TapErrorAsync(ex => {
            // Pattern match and log with full exception context
            switch (ex)
            {
                case HttpRequestException http:
                    _logger.LogWarning(http, "HTTP error for {Url}: {Status}", 
                        url, http.StatusCode);
                    break;
                case TaskCanceledException timeout:
                    _logger.LogWarning("Request timeout for {Url}", url);
                    break;
                default:
                    _logger.LogError(ex, "Unexpected error for {Url}", url);
                    break;
            }
        })
        .MapErrorAsync(ex => ex switch {
            HttpRequestException => "Network error",
            TaskCanceledException => "Request timeout",
            _ => "Failed to fetch data"
        });
}

Example - File operations with specific exception handling:

public async Task<Result<string, string>> ReadConfigFileAsync(string path)
{
    return await Result.TryAsync(async () => 
            await File.ReadAllTextAsync(path))
        .TapErrorAsync(ex => {
            if (ex is FileNotFoundException fnf)
                _logger.LogWarning("Config file missing: {FileName}", fnf.FileName);
            else if (ex is UnauthorizedAccessException)
                _logger.LogError(ex, "Permission denied reading config");
            else
                _logger.LogError(ex, "Failed to read config file");
        })
        .MapErrorAsync(ex => ex switch {
            FileNotFoundException => "Configuration file not found",
            UnauthorizedAccessException => "Permission denied",
            IOException => "Failed to read configuration",
            _ => "Configuration error"
        });
}

When to use Exception-First Try:

  • ✅ You need to log exceptions with full context (stack traces, types)
  • ✅ Different exception types require different handling
  • ✅ You want to separate logging from error transformation
  • ✅ Metrics or alerting need to inspect the raw exception

When to use Original Try:

  • ✅ You don't need exception details
  • ✅ Simple transformation to custom error is sufficient

Ensure / EnsureAsync - Validation

Add validation checks without breaking your pipeline:

using BindSharp;
using BindSharp.Extensions;

var result = GetUserAge()
    .Ensure(age => age >= 18, "Must be 18 or older")
    .Ensure(age => age <= 120, "Invalid age")
    .Map(age => new User(age));

Real-world example - Business rule validation:

using BindSharp;
using BindSharp.Extensions;

public Result<Order, string> ValidateOrder(OrderRequest request)
{
    return request.ToResult("Order request is required")
        .Ensure(r => r.Items.Any(), "Order must contain at least one item")
        .Ensure(r => r.Items.All(i => i.Quantity > 0), "All quantities must be positive")
        .Ensure(r => r.Total > 0, "Order total must be greater than zero")
        .Ensure(r => !string.IsNullOrEmpty(r.CustomerId), "Customer ID is required")
        .Map(r => new Order(r));
}

Async validation:

public async Task<Result<Account, string>> CreateAccountAsync(string email)
{
    return await ValidateEmail(email)
        .EnsureAsync(e => !await _db.EmailExistsAsync(e), "Email already registered")
        .BindAsync(async e => await CreateAccountRecordAsync(e));
}

EnsureNotNull - Null Safety

Convert nullable checks into Results:

using BindSharp;
using BindSharp.Extensions;

Result<User?, string> maybeUser = FindUser(id);
Result<User, string> user = maybeUser.EnsureNotNull("User not found");

// Or in a pipeline
var result = await GetUserFromCacheAsync(id)
    .EnsureNotNullAsync("User not in cache")
    .TapAsync(async u => await LogCacheHitAsync(u))
    .MapAsync(u => u.ToDto());

ToResult - Nullable Conversion

Convert nullable values to Results:

using BindSharp;
using BindSharp.Extensions;

string? cached = _cache.Get("key");
var result = cached.ToResult("Value not found in cache");

// In a pipeline
var processed = _cache.Get("user:42")
    .ToResult("User not cached")
    .Bind(json => DeserializeUser(json))
    .Map(user => ProcessUser(user));

Real-world example:

using BindSharp;
using BindSharp.Extensions;

public Result<Product, string> GetProductFromSession(HttpContext context)
{
    return context.Session.GetString("current_product")
        .ToResult("No product in session")
        .Bind(json => Result.Try(
            () => JsonSerializer.Deserialize<Product>(json),
            ex => "Invalid product data in session"
        ))
        .EnsureNotNull("Product was null")
        .Ensure(p => !p.IsDeleted, "Product has been deleted");
}

Tap / TapAsync - Side Effects

Execute side effects (logging, metrics, notifications) without modifying the Result:

using BindSharp;
using BindSharp.Extensions;

var result = await ProcessOrderAsync(order)
    .TapAsync(async o => await _logger.LogInfoAsync($"Order {o.Id} processed"))
    .TapAsync(async o => await _metrics.IncrementAsync("orders.processed"))
    .TapAsync(async o => await _notifications.NotifyAsync(o.CustomerId))
    .MapAsync(o => o.ToDto());

// The Result flows through unchanged, but side effects are executed on success

New in 1.6.0 - Sync actions in async pipelines:

// Before: Awkward Task.FromResult wrapping
await GetDataAsync()
    .TapAsync(x => Task.FromResult(Console.WriteLine(x)));  // ❌ Ugly!

// After: Natural sync actions
await GetDataAsync()
    .TapAsync(x => Console.WriteLine(x));  // ✨ Clean!

Real-world example - Audit logging:

using BindSharp;
using BindSharp.Extensions;

public async Task<Result<User, string>> UpdateUserAsync(int id, UpdateUserRequest request)
{
    return await GetUserAsync(id)
        .Tap(user => _auditLog.LogAccess(user.Id, "Update attempted"))  // ✨ Sync!
        .BindAsync(user => ValidateUpdateAsync(user, request))
        .BindAsync(async user => await ApplyChangesAsync(user, request))
        .TapAsync(user => _auditLog.LogSuccess(user.Id, "Updated"))  // ✨ Sync!
        .TapAsync(async user => await _cache.InvalidateAsync($"user:{user.Id}"))
        .BindAsync(async user => await SaveChangesAsync(user));
}

TapError / TapErrorAsync - Error-Specific Side Effects

New in 1.5.0! Execute side effects specifically on errors without modifying the result:

using BindSharp;
using BindSharp.Extensions;

var result = await ProcessDataAsync()
    .TapAsync(data => _logger.LogInfoAsync($"Processing {data.Id}"))
    .TapErrorAsync(error => _logger.LogErrorAsync(error));  // Only on failure!

New in 1.6.0 - Sync actions in async pipelines:

// Before: Awkward Task.FromResult wrapping
await GetDataAsync()
    .TapErrorAsync(ex => Task.FromResult(_logger.LogError(ex, "Failed")));  // ❌ Ugly!

// After: Natural sync actions
await GetDataAsync()
    .TapErrorAsync(ex => _logger.LogError(ex, "Failed"));  // ✨ Clean!

How it works:

  • Result is failure → Action executes with error value
  • Result is success → Action is skipped
  • Always returns the original result unchanged

Real-world example - Complete observability:

using BindSharp;
using BindSharp.Extensions;

public async Task<Result<Order, string>> ProcessOrderAsync(Order order)
{
    return await ValidateOrder(order)
        .TapAsync(o => _logger.LogInfo("Validation passed"))  // ✨ Sync!
        .BindAsync(async o => await SaveOrderAsync(o))
        .TapAsync(o => {  // ✨ Sync!
            _logger.LogInfo($"Order {o.Id} saved");
            _metrics.Increment("orders.success");
        })
        .TapErrorAsync(error => {  // ✨ Sync!
            _logger.LogError($"Order failed: {error}");
            _metrics.Increment("orders.failed");
            _alerting.NotifyAdmin(error);
        });
}

Key Difference from MapError:

  • MapError - Transforms the error value (returns different error)
  • TapError - Executes side effects only (returns same error)

Symmetric Design:

// Tap and TapError are symmetric - one for success, one for failure
result
    .Tap(value => Console.WriteLine($"Success: {value}"))      // Only on success
    .TapError(error => Console.WriteLine($"Error: {error}"));  // Only on failure

Do / DoAsync - Dual Side Effects

New in 2.0! 🔥 Execute different side effects for success and failure in a single method call.

Often you need to perform different actions based on whether an operation succeeds or fails - logging, metrics, notifications, etc. Previously, this required two separate method calls:

// Old way (v1.x) - Still works but verbose
var result = await ProcessDataAsync()
    .TapAsync(data => _logger.LogInfo($"Processing succeeded: {data}"))
    .TapErrorAsync(error => _logger.LogError($"Processing failed: {error}"));

New way (v2.0) - Cleaner and more intentional:

using BindSharp;
using BindSharp.Extensions;

var result = await ProcessDataAsync()
    .DoAsync(
        data => _logger.LogInfo($"Processing succeeded: {data}"),
        error => _logger.LogError($"Processing failed: {error}")
    );

Benefits:

  • ✅ Single method call
  • ✅ Success and failure handling grouped together
  • ✅ Clear intent: "Do this on success, do that on failure"
  • ✅ Harder to forget one path
  • ✅ Cleaner, more maintainable code

How it works:

Do/DoAsync executes one of two actions based on the Result's state, then returns the original Result unchanged (just like Tap).

var result = Result<int, string>.Success(42);

// Both actions provided, only success executes
var sameResult = result.Do(
    onSuccess: value => Console.WriteLine($"Success: {value}"),  // ✅ Executes
    onFailure: error => Console.WriteLine($"Error: {error}")     // ❌ Skipped
);

// sameResult == result (unchanged Success(42))

Key Difference from Match:

Method Purpose Returns
Match Transform the result into a new value Different type (e.g., string, IActionResult)
Do Execute side effects only Same Result<T, TError>
// Match - Transform result into a message
string message = result.Match(
    value => $"Got value: {value}",   // Returns string
    error => $"Got error: {error}"    // Returns string
);
// Type: string ✅

// Do - Execute side effects, keep result
Result<int, string> sameResult = result.Do(
    value => Console.WriteLine(value),  // Side effect only
    error => Console.WriteLine(error)   // Side effect only
);
// Type: Result<int, string> ✅

Async Combinations:

DoAsync supports all combinations of sync/async handlers:

using BindSharp;
using BindSharp.Extensions;

// 1. Both sync
result.Do(
    value => Log(value),
    error => Log(error)
);

// 2. Async success, sync failure
await result.DoAsync(
    async value => await LogToDbAsync(value),
    error => Console.WriteLine(error)
);

// 3. Sync success, async failure
await result.DoAsync(
    value => Console.WriteLine(value),
    async error => await AlertAdminAsync(error)
);

// 4. Both async
await result.DoAsync(
    async value => await LogSuccessAsync(value),
    async error => await LogErrorAsync(error)
);

// Works with Task<Result> too!
await GetDataAsync()
    .DoAsync(
        data => Console.WriteLine(data),
        error => Console.WriteLine(error)
    );

Real-World Examples:

using BindSharp;
using BindSharp.Extensions;

// Example 1: Complete Observability
public async Task<Result<Order, string>> ProcessOrderAsync(Order order)
{
    return await ValidateOrder(order)
        .DoAsync(
            o => _logger.LogInfo($"Validation passed for order {o.Id}"),
            error => _logger.LogWarning($"Validation failed: {error}")
        )
        .BindAsync(async o => await SaveOrderAsync(o))
        .DoAsync(
            o => {
                _logger.LogInfo($"Order {o.Id} saved");
                _metrics.Increment("orders.success");
            },
            error => {
                _logger.LogError($"Save failed: {error}");
                _metrics.Increment("orders.failed");
                _alerting.NotifyAdmin(error);
            }
        );
}

// Example 2: API Response with Logging
public async Task<IActionResult> GetUserAsync(int id)
{
    var result = await _userService.FetchUserAsync(id)
        .DoAsync(
            async user => await _auditLog.LogAccessAsync(user.Id, "Viewed"),
            async error => await _auditLog.LogAccessDeniedAsync(id, error)
        );

    return result.Match(
        user => Ok(user),
        error => NotFound(new { error })
    );
}

// Example 3: Metrics and Alerting
public async Task<Result<Data, string>> ImportDataAsync(string source)
{
    var stopwatch = Stopwatch.StartNew();

    return await FetchDataAsync(source)
        .DoAsync(
            async data => {
                stopwatch.Stop();
                await _metrics.RecordLatencyAsync("import.success", stopwatch.Elapsed);
                await _metrics.IncrementAsync("import.count");
            },
            async error => {
                stopwatch.Stop();
                await _metrics.RecordLatencyAsync("import.failure", stopwatch.Elapsed);
                await _alerting.NotifyAsync($"Import failed from {source}: {error}");
            }
        )
        .BindAsync(async data => await TransformDataAsync(data));
}

When to Use Do vs. Tap + TapError:

Both patterns are valid! Choose based on your preference:

Use Do when:

  • ✅ Success and failure are related (same logging concern)
  • ✅ You want handlers visually grouped
  • ✅ Cleaner, more concise code matters

Use Tap + TapError when:

  • ✅ Success and failure are unrelated concerns
  • ✅ Handlers are complex and better separated
  • ✅ You might add one handler later
// Related logging - Do is clearer
.DoAsync(
    data => _logger.LogInfo($"Success: {data}"),
    error => _logger.LogError($"Failed: {error}")
)

// Unrelated concerns - Tap + TapError is fine
.TapAsync(data => _cache.SetAsync("key", data))  // Caching (success only)
.TapErrorAsync(error => _retry.ScheduleAsync())  // Retry (failure only)

Using / UsingAsync - Resource Management

Safe resource management with guaranteed disposal (the "bracket" pattern):

using BindSharp;
using BindSharp.Extensions;

var data = OpenFile("data.txt")
    .Using(stream => ReadAllData(stream));
// stream is automatically disposed, even if ReadAllData fails

// Async version
var data = await OpenDatabaseConnectionAsync()
    .UsingAsync(async connection =>
        await QueryDataAsync(connection)
            .BindAsync(async data => await ValidateDataAsync(data))
            .MapAsync(data => TransformData(data))
    );
// connection is automatically disposed

Real-world example - Database transaction:

using BindSharp;
using BindSharp.Extensions;

public async Task<Result<Order, string>> CreateOrderWithTransactionAsync(CreateOrderRequest request)
{
    return await Result.TryAsync(
            async () => await _dbContext.Database.BeginTransactionAsync(),
            ex => $"Failed to begin transaction: {ex.Message}"
        )
        .UsingAsync(async transaction =>
            await ValidateOrderRequest(request)
                .BindAsync(async req => await CreateOrderEntityAsync(req))
                .TapAsync(async order => await UpdateInventoryAsync(order))
                .TapAsync(async order => await CreateOrderHistoryAsync(order))
                .BindAsync(async order => {
                    await transaction.CommitAsync();
                    return order;
                })
                .MapErrorAsync(async error => {
                    await transaction.RollbackAsync();
                    return error;
                })
        );
    // transaction is automatically disposed
}

AsTask - Sync to Async Conversion

Convert synchronous Results to Task-wrapped Results:

using BindSharp;
using BindSharp.Extensions;

Result<int, string> syncResult = Validate(value);
Task<Result<int, string>> asyncResult = syncResult.AsTask();

// Useful for matching method signatures
public Task<Result<User, string>> GetUserAsync(int id)
{
    // Fast path: check cache
    var cached = _cache.Get<User>(id);
    if (cached != null)
        return cached.AsTask();  // ✨ Implicit conversion + AsTask!
    
    // Slow path: fetch from database
    return FetchUserFromDatabaseAsync(id);
}

Complete Real-World Example

Here's how everything comes together with all the features:

using BindSharp;
using BindSharp.Extensions;

public class OrderService
{
    public async Task<Result<OrderConfirmation, OrderError>> PlaceOrderAsync(Cart cart)
    {
        return await cart.ToResult(OrderError.InvalidCart("Cart is null"))
            // Validation
            .Ensure(c => c.Items.Any(), OrderError.EmptyCart())
            .Ensure(c => c.CustomerId != null, OrderError.MissingCustomer())
            
            // Exception handling with logging (NEW in 1.6.0!)
            .BindAsync(async c => await Result.TryAsync(
                async () => await _inventory.CheckStockAsync(c.Items))
                .TapErrorAsync(ex => {  // ✨ Log exception with full context
                    _logger.LogError(ex, "Inventory check failed");
                    _metrics.RecordException(ex);
                })
                .MapErrorAsync(ex => OrderError.InventoryError(ex.Message))
            )
            
            // Business logic with dual side effects (NEW in 2.0!)
            .DoAsync(
                c => _logger.LogInfo($"Processing cart {c.Id}"),
                error => _logger.LogError($"Validation failed: {error}")
            )
            .BindAsync(async c => await CalculatePriceAsync(c))
            .TapAsync(c => _metrics.Increment("orders.pricing.completed"))
            
            // Conditional processing
            .BindIfAsync(
                async order => await RequiresSpecialHandlingAsync(order),
                async order => await ApplySpecialHandlingAsync(order)
            )
            
            // Payment with resource management
            .BindAsync(async order => await ProcessPaymentWithTransactionAsync(order))
            .TapAsync(order => _logger.LogInfo($"Payment processed for order {order.Id}"))
            
            // Finalization
            .BindAsync(async order => await CreateOrderRecordAsync(order))
            .BindAsync(async order => await ReserveInventoryAsync(order))
            
            // Notifications
            .TapAsync(async order => await _emailService.SendConfirmationAsync(order))
            .TapAsync(async order => await _sms.SendNotificationAsync(order.CustomerId))
            
            // Transform and dual side effects (NEW in 2.0!)
            .MapAsync(order => new OrderConfirmation(order))
            .DoAsync(
                conf => _auditLog.LogOrderCreated(conf),
                error => {
                    _logger.LogError($"Order failed: {error}");
                    _metrics.Increment("orders.failed");
                    _alerting.NotifyAdmin($"Order processing failure: {error}");
                }
            );
    }
    
    private async Task<Result<Order, OrderError>> ProcessPaymentWithTransactionAsync(Order order)
    {
        return await Result.TryAsync(
                async () => await _paymentGateway.BeginTransactionAsync())
            .TapErrorAsync(ex => _logger.LogError(ex, "Payment transaction failed"))
            .MapErrorAsync(ex => OrderError.PaymentError($"Transaction failed: {ex.Message}"))
            .UsingAsync(async transaction =>
                await ChargeCustomerAsync(order)
                    .TapAsync(async charge => await transaction.CommitAsync())
                    .MapErrorAsync(async error => {
                        await transaction.RollbackAsync();
                        return error;
                    })
            );
    }
}

Tips & Best Practices

General

  1. Use descriptive error types - Result<User, UserError> is better than Result<User, string>
  2. Keep operations small - Each function should do one thing
  3. Use Bind for chaining - When next operation depends on previous result
  4. Use Map for transforming - When just changing the value
  5. Match at boundaries - Convert Result to concrete types at API/UI boundaries
  6. Async all the way - If any operation is async, make the whole chain async

Implicit Conversions (NEW in 1.3.0)

  1. Use different types for T and TError - NEVER use Result<string, string>
  2. Define custom error types - Makes code clearer and type-safer
  3. Mix styles freely - Implicit and explicit can coexist
  4. Use for guard clauses - Perfect for early returns

Result Utilities (v2.0)

  1. Use Try for exception-based APIs - Convert legacy code to Results
  2. Use Ensure for business rules - Keep validation in the pipeline
  3. Use Tap for side effects - Logging, metrics, notifications
  4. Use Using for resources - Database connections, file streams, transactions
  5. Use ToResult for nullables - Convert optional values to Results

BindIf (NEW in 1.4.1)

  1. Use BindIf for conditional execution - "If condition, then execute"
  2. Keep predicates side-effect free - Predicates should be pure functions
  3. Use async predicates for I/O - Database checks, cache lookups, etc.
  4. Remember: TRUE executes, FALSE skips - Standard if-then behavior

TapError (NEW in 1.5.0)

  1. Use TapError for error logging - Log errors without transforming them
  2. Use Tap + TapError for observability - Complete success/failure tracking
  3. Keep error actions simple - Just logging, metrics, alerts
  4. Don't transform in TapError - Use MapError if you need to change the error

Do/DoAsync (NEW in 2.0)

  1. Use Do for related concerns - When success and failure handling are coupled
  2. Keep handlers simple - Do is for side effects, not complex logic
  3. Use for cross-cutting concerns - Logging, metrics, caching, notifications
  4. Combine with other methods naturally - Do fits seamlessly in pipelines

Exception-First Try (NEW in 1.6.0)

  1. Use exception-first Try for logging - Log exceptions before transforming
  2. Pattern match on exception types - Handle different exceptions differently
  3. Separate logging from transformation - TapError then MapError
  4. Use for metrics and alerting - Inspect raw exceptions for monitoring

Mixed Async/Sync Pipelines (NEW in 1.6.0)

  1. Use sync actions when possible - Simpler than wrapping in Task.FromResult
  2. Keep side effects lightweight - Heavy operations should be async
  3. Natural composition - Let the compiler choose the right overload

Error Handling Strategy

using BindSharp;

// ✅ Good: Specific error types
public record OrderError(string Code, string Message);

// ✅ Good: Enables implicit conversions without ambiguity
public Result<Order, OrderError> CreateOrder(OrderRequest request)
{
    if (request.Items.Count == 0)
        return new OrderError("EMPTY_CART", "Cart is empty");
    
    return new Order(request);
}

// ❌ Avoid: Same type for T and TError (ambiguous with implicit conversions!)
public Result<string, string> GetValue() { ... }  // DON'T DO THIS!

Exception Handling Patterns (NEW in 1.6.0)

using BindSharp;
using BindSharp.Extensions;

// ✅ Good: Separate logging from transformation
Result.Try(() => operation())
    .TapError(ex => _logger.LogError(ex, "Failed"))  // ✅ Logging
    .MapError(ex => "User-friendly message");  // ✅ Transformation

// ❌ Avoid: Mixing concerns
Result.Try(
    () => operation(),
    ex => {
        _logger.LogError(ex, "Failed");  // ❌ Side effect mixed with transformation
        return "User-friendly message";
    }
);

Why Async Support is a Game-Changer

Traditional async error handling gets messy fast:

// 😢 The old way
try {
    var user = await GetUserAsync(id);
    try {
        var orders = await GetOrdersAsync(user.Id);
        try {
            var enriched = await EnrichOrdersAsync(orders);
            return await FormatResponseAsync(enriched);
        } catch (Exception ex3) { /* handle */ }
    } catch (Exception ex2) { /* handle */ }
} catch (Exception ex1) { /* handle */ }

With BindSharp:

using BindSharp;
using BindSharp.Extensions;

// 😊 The new way
return await GetUserAsync(id)
    .BindAsync(user => GetOrdersAsync(user.Id))
    .BindAsync(orders => EnrichOrdersAsync(orders))
    .MapAsync(enriched => FormatResponseAsync(enriched))
    .MatchAsync(
        success => Ok(success),
        error => BadRequest(error)
    );

Clean. Composable. Elegant. Powerful. 🚀

API Reference

Core Types

  • Result<T, TError> - Result type with Success/Failure states
  • Unit - Represents "no value" (use Unit.Value)

FunctionalResult (Core Operations)

  • Map<T1, T2, TError> - Transform success value
  • Bind<T1, T2, TError> - Chain operations that can fail
  • BindIf<T, TError> - Conditional processing (new in 1.4.1!)
  • MapError<T, TError, TNewError> - Transform error value
  • Match<T, TError, TResult> - Handle both success and failure

AsyncFunctionalResult (Async Core)

  • MapAsync<T1, T2, TError> - Async transformations (3 overloads)
  • BindAsync<T1, T2, TError> - Async chaining (3 overloads)
  • BindIfAsync<T, TError> - Async conditional processing (7 overloads - new in 1.4.1!)
  • MapErrorAsync<T, TError, TNewError> - Async error transformation (3 overloads)
  • MatchAsync<T, TError, TResult> - Async result handling (7 overloads)

Result (Static Utilities)

  • Try<T, TError> - Exception handling with custom error factory
  • Try<T> - Exception-first (returns Result<T, Exception>) (new in 1.6.0!)
  • TryAsync<T, TError> - Async exception handling with custom error factory
  • TryAsync<T> - Async exception-first (new in 1.6.0!)

BindSharp.Extensions

Require using BindSharp.Extensions;

Validation:

  • Ensure<T, TError> / EnsureAsync<T, TError> - Validation
  • EnsureNotNull<T, TError> / EnsureNotNullAsync<T, TError> - Null safety

Conversion:

  • ToResult<T, TError> - Convert nullable to Result
  • AsTask<T, TError> - Convert Result to Task

Side Effects:

  • Tap<T, TError> / TapAsync<T, TError> - Success side effects (4 overloads, +1 in 1.6.0)
  • TapError<T, TError> / TapErrorAsync<T, TError> - Error side effects (4 overloads, +1 in 1.6.0)
  • Do<T, TError> / DoAsync<T, TError> - Dual side effects (8 overloads - new in 2.0!)

Resource Management:

  • Using<TResource, TResult, TError> / UsingAsync - Resource management

Acknowledgments

Special thanks to Zoran Horvat from the YouTube channel "Zoran on C#" for his excellent tutorials on functional programming in C#. His teaching on Railway-Oriented Programming and Result patterns made this library possible.

If you want to learn advanced C# techniques and functional programming concepts, check out his channel - he deserves way more views! We can all learn a thing or two from him. 🙏

License

MPL-2.0

Contributing

Contributions are welcome! Feel free to open issues or submit pull requests.


Built with ❤️ for the .NET community

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.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
2.1.0 146 1/20/2026
2.0.0 122 1/9/2026
1.6.0 258 12/6/2025
1.5.0 229 12/3/2025
1.4.1 387 11/30/2025
1.3.0 222 11/24/2025
1.2.1 181 11/8/2025
1.1.0 215 11/7/2025
1.0.0 232 11/5/2025

## v2.1.0

           ### License Change
           - Changed license from LGPL-3.0-or-later to MPL-2.0

           See MIGRATION_V2.md for v2.0.0 changes.