BindSharp 1.3.0
See the version list below for details.
dotnet add package BindSharp --version 1.3.0
NuGet\Install-Package BindSharp -Version 1.3.0
<PackageReference Include="BindSharp" Version="1.3.0" />
<PackageVersion Include="BindSharp" Version="1.3.0" />
<PackageReference Include="BindSharp" />
paket add BindSharp --version 1.3.0
#r "nuget: BindSharp, 1.3.0"
#:package BindSharp@1.3.0
#addin nuget:?package=BindSharp&version=1.3.0
#tool nuget:?package=BindSharp&version=1.3.0
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.3.0
Equality & Implicit Conversions - Write cleaner, more maintainable code:
- ✅ Equality Support - Results can now be compared and used in collections!
- ✨ Implicit Conversions - Return values directly without wrapping (40-50% less code!)
- 🐛 Better Debugging - ToString() override shows "Success(value)" or "Failure(error)"
Before:
public Result<int, string> Divide(int a, int b)
{
if (b == 0)
return Result<int, string>.Failure("Division by zero");
return Result<int, string>.Success(a / b);
}
After:
public Result<int, string> Divide(int a, int b)
{
if (b == 0) return "Division by zero"; // ✨ Implicit!
return a / b; // ✨ Implicit!
}
See the Implicit Conversions and Equality Support sections below!
⚠️ CRITICAL: Read the implicit conversions warning to avoid ambiguity issues.
Previous Releases:
- Version 1.2.0 added the Unit Type for functional result handling
- Version 1.1.0 added ResultExtensions with exception handling, validation, and more
Features
✅ Result<T, TError> - Explicit success/failure handling
✅ 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
});
}
🚀 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 new 1.3.0 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"))
// 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
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 failMapError<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)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.3.0 adds major improvements:
- Implemented IEquatable for value equality (Results work in collections!)
- Added implicit conversions for cleaner syntax (40-50% less boilerplate!)
- Improved ToString() for better debugging
All changes are backwards compatible. No breaking changes.
See RELEASE_NOTES_1.3.0.md for full details.