A lightweight, robust, and type-safe implementation of the Result Pattern for .NET. Eliminates the need for exceptions for flow control and provides a standard way to handle success and failure scenarios.
$ dotnet add package Easy.Tools.ResultEasy.Tools.Result is a lightweight, high-performance, and enterprise-ready .NET library that implements the Result Pattern. It helps you write cleaner, more robust code by replacing exceptions with typed return values for flow control.
It supports both Imperative (if (result.IsFailure)) and Functional (result.Match(...)) programming styles.
Success() results are cached to minimize memory pressure (GC friendly).Match, Map, Tap, and Ensure for fluent chaining.null checks or magic strings.Error or Value directly without verbose syntax..NET 10, .NET 8, .NET 6, .NET Standard 2.0/2.1, and .NET Framework 4.7.2+.Install via NuGet Package Manager:
Install-Package Easy.Tools.Result
Or via .NET CLI:
dotnet add package Easy.Tools.Result
Instead of throwing exceptions, define your domain errors statically.
public static class DomainErrors
{
public static readonly Error UserNotFound = new("User.NotFound", "The user with the specified ID was not found.");
public static readonly Error InvalidEmail = new("User.InvalidEmail", "The email format is invalid.");
}
Refactor your services to return Result. You can use Implicit Conversions for cleaner code.
public Result<User> GetUserById(int id)
{
var user = _repository.Find(id);
if (user is null)
{
// Implicitly converts Error to Result<User>
return DomainErrors.UserNotFound;
}
// Implicitly converts User to Result<User>
return user;
}
Use properties to control flow explicitly.
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
Result<User> result = _userService.GetUserById(id);
if (result.IsFailure)
{
return BadRequest(new { code = result.Error.Code, message = result.Error.Message });
}
return Ok(result.Value);
}
Use Match to handle success and failure in a single expression.
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
return _userService.GetUserById(id)
.Match(
onSuccess: user => Ok(user),
onFailure: error => BadRequest(new { error.Code, error.Message })
);
}
Use Tap for side effects (logging) and Map for transformation.
public IActionResult GetUserDto(int id)
{
return _userService.GetUserById(id)
.Tap(user => _logger.LogInformation($"User found: {user.Name}")) // Side-effect (Logging)
.Map(user => new UserDto(user.Name, user.Email)) // Transformation
.Match(
onSuccess: dto => Ok(dto),
onFailure: error => BadRequest(error)
);
}
You can deconstruct the result into a tuple, similar to Go or Swift.
var (isSuccess, error) = _userService.DeleteUser(id);
if (!isSuccess)
{
Console.WriteLine($"Error: {error.Code}");
return;
}
You can compare errors directly.
if (result.Error == DomainErrors.UserNotFound)
{
// Handle specific error case
}
Exceptions are for Exceptional Circumstances: A user not being found or a validation error is not an exception; it's a valid business scenario.
Performance: Throwing exceptions is expensive in .NET (stack trace generation). Returning a Result object is essentially free, especially with our Zero-Allocation optimizations.
Readability: Result<User> explicitly tells the developer "This operation might fail", whereas returning User lies (it might be null or throw).
Contributions and suggestions are welcome. Please open an issue or submit a pull request.
This project is licensed under the MIT License.
2025 Elmin Alirzayev / Easy Code Tools