BindSharp 1.3.0
BindSharp 1.3.0 Release Notes
🎉 What's New
Version 1.3.0 introduces two major improvements that significantly enhance the developer experience while maintaining 100% backwards compatibility.
New Features
1. Equality Implementation (IEquatable<T>)
Result<T, TError> now implements IEquatable<Result<T, TError>>, providing proper value equality instead of reference equality.
Before:
var r1 = Result<int, string>.Success(42);
var r2 = Result<int, string>.Success(42);
Console.WriteLine(r1 == r2); // FALSE ❌After:
var r1 = Result<int, string>.Success(42);
var r2 = Result<int, string>.Success(42);
Console.WriteLine(r1 == r2); // TRUE ✅Benefits:
- ✅ Results can now be compared for equality
- ✅ Works in collections (
HashSet,Dictionary) - ✅ Proper hash code implementation
- ✅ Better debugging with
ToString()override
Usage:
// Works in HashSet
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 ✅
// Works as Dictionary key
var dict = new Dictionary<Result<int, string>, string>();
dict[Result<int, string>.Success(1)] = "one";
// Better debugging
var result = Result<int, string>.Success(42);
Console.WriteLine(result); // "Success(42)"2. Implicit Conversions
Added implicit conversion operators that allow returning values and errors directly without wrapping them in Result.Success() or Result.Failure().
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!
}Benefits:
- ✨ 40-50% less boilerplate code
- ✨ More readable and maintainable
- ✨ Cleaner guard clauses
- ✅ Fully type-safe
- ⚡ Zero performance overhead
Examples:
// Example 1: Validation
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 range";
return age; // ✨ Clean!
}
// Example 2: Switch expressions
public Result<decimal, string> GetDiscount(string code)
{
return code.ToUpper() switch
{
"SAVE10" => 0.10m,
"SAVE20" => 0.20m,
"SAVE50" => 0.50m,
_ => "Invalid coupon code"
};
}
// Example 3: Async operations
public async Task<Result<User, string>> GetUserAsync(int id)
{
if (id < 0) return "Invalid ID";
var user = await _db.FindUserAsync(id);
if (user == null) return "User not found";
return user;
}⚠️ Important: Implicit Conversions Warning
CRITICAL: When using implicit conversions, 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 error? Compiler can't tell!
}
// ✅ ALWAYS DO THIS - Clear!
public Result<int, string> GetValue()
{
if (error) return "Error"; // Clear: error
return 42; // Clear: success
}
// ✅ OR USE CUSTOM ERROR TYPE
public record ErrorInfo(string Message);
public Result<string, ErrorInfo> GetValue()
{
if (error) return new ErrorInfo("Error"); // Clear: error
return "Success"; // Clear: success
}Best Practice: Define custom error types for your domain to avoid ambiguity and improve type safety.
📦 Installation
dotnet add package BindSharp --version 1.3.0Or update your .csproj:
<PackageReference Include="BindSharp" Version="1.3.0" />🔄 Migration from 1.2.x
No migration needed! All changes are backwards compatible.
What Continues to Work:
// Explicit style still works
return Result<int, string>.Success(42);
return Result<int, string>.Failure("Error");
// All existing code unchanged
var result = GetData()
.Map(x => x * 2)
.Bind(Validate)
.MapAsync(async x => await ProcessAsync(x));What You Can Now Do:
// Use implicit conversions for cleaner code
public Result<int, string> Calculate(int value)
{
if (value < 0) return "Negative value";
return value * 2;
}
// Compare results
if (result1 == result2) { ... }
// Use in collections
var uniqueResults = new HashSet<Result<int, string>>(results);💡 Real-World Impact
Code Reduction Example
Before (15 lines):
public Result<User, string> CreateUser(CreateUserRequest request)
{
if (request == null)
return Result<User, string>.Failure("Request is null");
if (string.IsNullOrEmpty(request.Email))
return Result<User, string>.Failure("Email is required");
if (string.IsNullOrEmpty(request.Password))
return Result<User, string>.Failure("Password is required");
if (request.Password.Length < 8)
return Result<User, string>.Failure("Password too short");
var user = new User(request);
return Result<User, string>.Success(user);
}After (7 lines - 53% reduction!):
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);
}🎯 Best Practices
DO:
✅ Use implicit conversions for guard clauses and early returns
✅ Use different types for T and TError (e.g., Result<int, string>)
✅ Define custom error types for complex domains
✅ Use equality in tests and assertions
✅ Mix explicit and implicit styles as appropriate
DON'T:
❌ Use Result<T, T> (same type for success and error)
❌ Sacrifice code clarity for brevity
❌ Forget that explicit style is still available
🔧 Technical Details
Equality Implementation
Implemented using standard .NET patterns:
IEquatable<Result<T, TError>>interfaceEquals(object)overrideGetHashCode()override (netstandard2.0 compatible)operator ==andoperator !=ToString()override
Implicit Conversions
Two operators added:
public static implicit operator Result<T, TError>(T value) => Success(value);
public static implicit operator Result<T, TError>(TError error) => Failure(error);These are compile-time only conversions with zero runtime overhead.
📊 Compatibility
| Framework | Status |
|---|---|
| netstandard2.0 | ✅ Fully Supported |
| netstandard2.1+ | ✅ Fully Supported |
| .NET Framework 4.6.1+ | ✅ Fully Supported |
| .NET Core 2.0+ | ✅ Fully Supported |
| .NET 5+ | ✅ Fully Supported |
📝 Changelog
Added
- Implemented
IEquatable<Result<T, TError>>for value equality - Added
Equals(object)override - Added
GetHashCode()override (netstandard2.0 compatible) - Added
operator ==andoperator != - Added
ToString()override for better debugging - Added implicit conversion from
TtoResult<T, TError>.Success - Added implicit conversion from
TErrortoResult<T, TError>.Failure - Added package tags:
equality,implicit-conversion
Changed
- None (no breaking changes)
Fixed
- Results can now be compared for equality (was always false before)
- Results can now be used in collections properly
Removed
- None
🎓 Examples and Patterns
Pattern 1: Clean Validation
public Result<Email, ValidationError> ValidateEmail(string input)
{
if (string.IsNullOrWhiteSpace(input))
return new ValidationError("Email", "Email is required");
if (!input.Contains("@"))
return new ValidationError("Email", "Invalid format");
return new Email(input);
}Pattern 2: Switch Expressions
public Result<OrderStatus, string> ParseStatus(string status)
{
return status.ToUpper() switch
{
"PENDING" => OrderStatus.Pending,
"CONFIRMED" => OrderStatus.Confirmed,
"SHIPPED" => OrderStatus.Shipped,
"DELIVERED" => OrderStatus.Delivered,
_ => $"Unknown status: {status}"
};
}Pattern 3: Collection Operations
// Remove duplicate results
var results = new List<Result<int, string>> { /* ... */ };
var unique = new HashSet<Result<int, string>>(results);
// Use as dictionary keys
var cache = new Dictionary<Result<string, Error>, CachedData>();Pattern 4: Test Assertions
[Fact]
public void Divide_ReturnsCorrectResult()
{
var result = Calculator.Divide(10, 2);
var expected = Result<int, string>.Success(5);
Assert.Equal(expected, result); // Now works! ✅
}🚀 Performance
All improvements have zero runtime overhead:
- Equality: Standard .NET equality pattern, O(1) hash code
- Implicit conversions: Compile-time only, no runtime cost
- ToString(): Only called when explicitly invoked (debugging)
🙏 Acknowledgments
Thanks to all users who provided feedback and requested these features!
Special thanks to the community for:
- Requesting equality support in collections
- Suggesting implicit conversions for cleaner syntax
- Testing the beta releases
📚 Additional Resources
- GitHub: https://github.com/GDB-11/BindSharp/
- Documentation: See README.md for complete usage guide
- Previous Releases:
❓ FAQ
Q: Will this break my existing code?
A: No! All changes are backwards compatible. Existing code works unchanged.
Q: Do I have to use implicit conversions?
A: No! They're optional. Mix and match with explicit style as you prefer.
Q: What if T and TError are the same type?
A: Don't do this! It creates ambiguity. Use custom error types instead.
Q: Does equality work with custom types?
A: Yes! As long as your custom types implement equality properly.
Q: What about IAsyncDisposable support?
A: Not available in netstandard2.0. Would require netstandard2.1+.
🎉 Conclusion
Version 1.3.0 brings significant quality-of-life improvements:
- ✅ Proper equality support
- ✨ Cleaner, more maintainable code
- ✅ Zero breaking changes
- 🎯 Better type safety
Upgrade today and enjoy cleaner, more expressive functional code!
Happy coding! 🚀