Skip to content

BindSharp 1.3.0

Choose a tag to compare

@GDB-11 GDB-11 released this 24 Nov 04:53
· 9 commits to main since this release

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.0

Or 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>> interface
  • Equals(object) override
  • GetHashCode() override (netstandard2.0 compatible)
  • operator == and operator !=
  • 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 == and operator !=
  • Added ToString() override for better debugging
  • Added implicit conversion from T to Result<T, TError>.Success
  • Added implicit conversion from TError to Result<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


❓ 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! 🚀