BindSharp 1.5.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package BindSharp --version 1.5.0
                    
NuGet\Install-Package BindSharp -Version 1.5.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="1.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="BindSharp" Version="1.5.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 1.5.0
                    
#r "nuget: BindSharp, 1.5.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@1.5.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=1.5.0
                    
Install as a Cake Addin
#tool nuget:?package=BindSharp&version=1.5.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();
    var validated = ValidateData(data);
    var transformed = TransformData(validated);
    return await SaveAsync(transformed);
}
catch (Exception ex) {
    // What failed? Where? How do we recover?
    return null; // 😢
}

With BindSharp, it's clean and composable:

return await FetchDataAsync()
    .BindAsync(ValidateDataAsync)
    .MapAsync(TransformData)
    .BindAsync(SaveAsync)
    .MatchAsync(
        success => $"Saved: {success}",
        error => $"Failed: {error}"
    );

✨ What's New in 1.5.0

Error-Specific Side Effects - Clean logging and metrics for failures:

  • TapError / TapErrorAsync - Execute side effects on errors without transformation!
  • 3 Async Overloads - Full async support matching Tap pattern
  • Symmetric Design - Tap for success, TapError for failure
  • 🎯 Better Observability - Log, track, and alert on errors cleanly

Example:

// Clean error logging without transforming the error
var result = await GetUserAsync(id)
    .TapAsync(user => _logger.LogInfoAsync($"User {user.Id} loaded"))
    .TapErrorAsync(error => _logger.LogErrorAsync(error));  // New!

See the TapError section below!

Previous Releases:

Features

Result<T, TError> - Explicit success/failure handling
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;  // ✨ Implicit!
Result<int, string> failure2 = "Error occurred";  // ✨ Implicit!

// 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:

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

// 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! (53% 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;  // ✨ So clean!
}

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

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

// ✅ With Unit: consistent Result<Unit, TError> signatures everywhere
public Task<Result<Unit, string>> DeleteUserAsync(int id) =>
    ResultExtensions.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

// 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 ResultExtensions.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 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:

Result<int, string> GetAge() => 25;  // ✨ Implicit conversion!

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:

Result<string, string> ValidateEmail(string email) =>
    email.Contains("@")
        ? email  // ✨ Implicit Success!
        : "Invalid email";  // ✨ Implicit Failure!

Result<string, string> SendEmail(string email) =>
    /* send email logic */
    $"Sent to {email}";  // ✨ Implicit Success!

var result = ValidateEmail("user@example.com")
    .Bind(SendEmail);  // Only runs if validation succeeds

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:

Result<int, string> result = "404";  // ✨ Implicit Failure!

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:

// 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:

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):

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:

// 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;  // ✨ Implicit!
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:

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

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:

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 });
        }
    );
}

ResultExtensions - Utilities for the Real World

BindSharp 1.1.0 adds ResultExtensions - practical utilities that handle common real-world scenarios beyond pure functional operations.

Try / TryAsync - Exception Handling

Convert exception-based code into Results:

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

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

Real-world example - API integration:

public async Task<Result<WeatherData, string>> GetWeatherAsync(string city)
{
    return await ResultExtensions.TryAsync(
            async () => await _weatherApi.GetWeatherAsync(city),
            ex => $"Failed to fetch weather for {city}: {ex.Message}"
        )
        .BindAsync(json => ResultExtensions.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 = ResultExtensions.Try(
    () => ProcessData(input),
    ex => new ApiError("PROCESS_FAILED", "Data processing failed", ex)
);
// Result<Data, ApiError>

Ensure / EnsureAsync - Validation

Add validation checks without breaking your pipeline:

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:

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:

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:

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:

public Result<Product, string> GetProductFromSession(HttpContext context)
{
    return context.Session.GetString("current_product")
        .ToResult("No product in session")
        .Bind(json => ResultExtensions.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:

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

Real-world example - Audit logging:

public async Task<Result<User, string>> UpdateUserAsync(int id, UpdateUserRequest request)
{
    return await GetUserAsync(id)
        .Tap(user => _auditLog.LogAccess(user.Id, "Update attempted"))
        .BindAsync(user => ValidateUpdateAsync(user, request))
        .BindAsync(async user => await ApplyChangesAsync(user, request))
        .TapAsync(async user => await _auditLog.LogSuccessAsync(user.Id, "Updated"))
        .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:

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

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:

public async Task<Result<Order, string>> ProcessOrderAsync(Order order)
{
    return await ValidateOrder(order)
        .TapAsync(_ => _logger.LogInfoAsync("Validation passed"))
        .BindAsync(async o => await SaveOrderAsync(o))
        .TapAsync(async o => {
            await _logger.LogInfoAsync($"Order {o.Id} saved");
            await _metrics.IncrementAsync("orders.success");
        })
        .TapErrorAsync(async error => {
            await _logger.LogErrorAsync($"Order failed: {error}");
            await _metrics.IncrementAsync("orders.failed");
            await _alerting.NotifyAdminAsync(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

Using / UsingAsync - Resource Management

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

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:

public async Task<Result<Order, string>> CreateOrderWithTransactionAsync(CreateOrderRequest request)
{
    return await ResultExtensions.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;  // ✨ Implicit Success!
                })
                .MapErrorAsync(async error => {
                    await transaction.RollbackAsync();
                    return error;
                })
        );
    // transaction is automatically disposed
}

AsTask - Sync to Async Conversion

Convert synchronous Results to Task-wrapped Results:

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:

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
            .BindAsync(async c => await ResultExtensions.TryAsync(
                async () => await _inventory.CheckStockAsync(c.Items),
                ex => OrderError.InventoryError(ex.Message)
            ))
            
            // Business logic with logging
            .TapAsync(async c => await _logger.LogInfoAsync($"Processing cart {c.Id}"))
            .BindAsync(async c => await CalculatePriceAsync(c))
            .TapAsync(async c => await _metrics.IncrementAsync("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(async order => await _logger.LogInfoAsync($"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 audit
            .MapAsync(order => new OrderConfirmation(order))
            .TapAsync(async conf => await _auditLog.LogOrderCreatedAsync(conf))
            
            // Error handling (new in 1.5.0!)
            .TapErrorAsync(async error => {
                await _logger.LogErrorAsync($"Order failed: {error}");
                await _metrics.IncrementAsync("orders.failed");
                await _alerting.NotifyAdminAsync($"Order processing failure: {error}");
            });
    }
    
    private async Task<Result<Order, OrderError>> ProcessPaymentWithTransactionAsync(Order order)
    {
        return await ResultExtensions.TryAsync(
                async () => await _paymentGateway.BeginTransactionAsync(),
                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

ResultExtensions

  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

Error Handling Strategy

// ✅ 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");  // ✨ Implicit!
    
    return new Order(request);  // ✨ Implicit!
}

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

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:

// 😊 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)

ResultExtensions (Utilities)

  • Try<T, TError> / TryAsync<T, TError> - Exception handling
  • Ensure<T, TError> / EnsureAsync<T, TError> - Validation
  • EnsureNotNull<T, TError> / EnsureNotNullAsync<T, TError> - Null safety
  • ToResult<T, TError> - Convert nullable to Result
  • Tap<T, TError> / TapAsync<T, TError> - Success side effects (3 overloads)
  • TapError<T, TError> / TapErrorAsync<T, TError> - Error side effects (3 overloads - new in 1.5.0!)
  • Using<TResource, TResult, TError> / UsingAsync - Resource management
  • AsTask<T, TError> - Convert Result to Task

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

LGPL-3.0-or-later

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 147 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

Version 1.5.0 adds error-specific side effects to functional pipelines:
           - Added TapError/TapErrorAsync for clean error logging and metrics without transformation
           - Full async support with 3 overloads matching Tap pattern
           - Symmetric design: Tap for success side effects, TapError for error side effects
           - Enables better observability and error handling in Railway-Oriented Programming
           All changes are backwards compatible. No breaking changes.
           See RELEASE_NOTES_1.5.0.md for full details.