BindSharp 1.4.1
See the version list below for details.
dotnet add package BindSharp --version 1.4.1
NuGet\Install-Package BindSharp -Version 1.4.1
<PackageReference Include="BindSharp" Version="1.4.1" />
<PackageVersion Include="BindSharp" Version="1.4.1" />
<PackageReference Include="BindSharp" />
paket add BindSharp --version 1.4.1
#r "nuget: BindSharp, 1.4.1"
#:package BindSharp@1.4.1
#addin nuget:?package=BindSharp&version=1.4.1
#tool nuget:?package=BindSharp&version=1.4.1
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.4.1
Conditional Processing - Execute operations based on conditions in functional pipelines:
- ✅ BindIf / BindIfAsync - Standard if-then logic in Railway-Oriented Programming!
- ✅ 7 Async Overloads - Full async support including async predicates
- ✅ Database-Backed Conditions - Async predicates for I/O operations
- 🎯 Intuitive Behavior - Works like standard
ifstatements
Example:
// If user is incomplete, then enrich
var result = await GetUserAsync(id)
.BindIfAsync(
user => !user.IsComplete, // If TRUE
async user => await EnrichUserAsync(user) // Then execute
);
See the BindIf section below!
Previous Releases:
- Version 1.3.0 added Equality Support & Implicit Conversions
- Version 1.2.0 added the Unit Type
- Version 1.1.0 added ResultExtensions
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 falseBindIf- 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));
}
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 (new in 1.4.1!)
.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 (with implicit conversions!)
.MapAsync(order => new OrderConfirmation(order))
.TapAsync(async conf => await _auditLog.LogOrderCreatedAsync(conf));
}
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
- Use descriptive error types -
Result<User, UserError>is better thanResult<User, string> - Keep operations small - Each function should do one thing
- Use Bind for chaining - When next operation depends on previous result
- Use Map for transforming - When just changing the value
- Match at boundaries - Convert Result to concrete types at API/UI boundaries
- Async all the way - If any operation is async, make the whole chain async
Implicit Conversions (NEW in 1.3.0)
- Use different types for T and TError - NEVER use
Result<string, string> - Define custom error types - Makes code clearer and type-safer
- Mix styles freely - Implicit and explicit can coexist
- Use for guard clauses - Perfect for early returns
ResultExtensions
- Use Try for exception-based APIs - Convert legacy code to Results
- Use Ensure for business rules - Keep validation in the pipeline
- Use Tap for side effects - Logging, metrics, notifications
- Use Using for resources - Database connections, file streams, transactions
- Use ToResult for nullables - Convert optional values to Results
BindIf (NEW in 1.4.1)
- Use BindIf for conditional execution - "If condition, then execute"
- Keep predicates side-effect free - Predicates should be pure functions
- Use async predicates for I/O - Database checks, cache lookups, etc.
- Remember: TRUE executes, FALSE skips - Standard if-then behavior
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 statesUnit- Represents "no value" (useUnit.Value)
FunctionalResult (Core Operations)
Map<T1, T2, TError>- Transform success valueBind<T1, T2, TError>- Chain operations that can failBindIf<T, TError>- Conditional processing (new in 1.4.1!)MapError<T, TError, TNewError>- Transform error valueMatch<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 handlingEnsure<T, TError>/EnsureAsync<T, TError>- ValidationEnsureNotNull<T, TError>/EnsureNotNullAsync<T, TError>- Null safetyToResult<T, TError>- Convert nullable to ResultTap<T, TError>/TapAsync<T, TError>- Side effects (3 overloads)Using<TResource, TResult, TError>/UsingAsync- Resource managementAsTask<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 | Versions 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. |
-
.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 1.4.1 adds conditional branching to functional pipelines:
- Added BindIf/BindIfAsync for conditional processing without breaking chains
- Full async support with 7 overloads covering all async scenarios
- Async predicate support for database-backed conditional logic
- Enables clean short-circuit patterns in Railway-Oriented Programming
All changes are backwards compatible. No breaking changes.
See RELEASE_NOTES_1.4.0.md for full details.