Hrz.Returnables
0.1.1
dotnet add package Hrz.Returnables --version 0.1.1
NuGet\Install-Package Hrz.Returnables -Version 0.1.1
<PackageReference Include="Hrz.Returnables" Version="0.1.1" />
<PackageVersion Include="Hrz.Returnables" Version="0.1.1" />
<PackageReference Include="Hrz.Returnables" />
paket add Hrz.Returnables --version 0.1.1
#r "nuget: Hrz.Returnables, 0.1.1"
#:package Hrz.Returnables@0.1.1
#addin nuget:?package=Hrz.Returnables&version=0.1.1
#tool nuget:?package=Hrz.Returnables&version=0.1.1
<p align="center"> <img src="icon.png" alt="Hrz Returnables" width="120" /> </p>
<h1 align="center">Hrz Returnables</h1>
<p align="center"> A lightweight, zero-dependency Result/Error pattern library for .NET.<br/> Replace exception-driven control flow with explicit, type-safe return values. </p>
<p align="center"> <a href="https://nuget.org/packages/Hrz.Returnables"><img src="https://img.shields.io/nuget/v/Hrz.Returnables.svg?style=flat-square&logo=nuget&label=NuGet" alt="NuGet Version" /></a> <a href="https://nuget.org/packages/Hrz.Returnables"><img src="https://img.shields.io/nuget/dt/Hrz.Returnables.svg?style=flat-square&logo=nuget&label=Downloads" alt="NuGet Downloads" /></a> <a href="https://github.com/horizon-co/hrz-returnables/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/horizon-co/hrz-returnables/ci.yml?branch=main&style=flat-square&logo=github&label=CI" alt="CI Status" /></a> <a href="https://github.com/horizon-co/hrz-returnables/blob/main/LICENSE"><img src="https://img.shields.io/github/license/horizon-co/hrz-returnables?style=flat-square&label=License" alt="License" /></a> </p>
Why Returnables?
Exceptions are expensive, invisible in method signatures, and easy to forget. The Result pattern makes success and failure explicit — your code tells the caller exactly what can happen, and the compiler helps enforce it.
Returnables gives you this pattern with zero boilerplate:
using Hrz.Returnables;
public Result<User> GetUser(int id)
{
var user = repository.Find(id);
if (user is null)
{
return Result<User>.Fail("User not found");
}
return user; // implicit conversion
}
No wrapping. No .Success(value) calls. No factory noise. Just return the value or an exception — Returnables figures out which one it is.
Key Features
- Implicit operators — return
TValueorErrordirectly from anyResult<TValue>method - Zero dependencies — pure .NET BCL, no third-party packages
- Stack-allocated —
readonly structtypes with zero heap overhead on success paths - Null-safe —
[MemberNotNullWhen]attributes give the compiler full nullability flow analysis - Try/TryAsync — wrap any operation in a try/catch and get a
Resultback - Fluent side-effects —
OnSuccess/OnFailurefor logging and telemetry without breaking chains - Conditional factories —
SuccessIf/FailIffor guard-clause style results
Installation
dotnet add package Hrz.Returnables
Or via the Package Manager:
PM> Install-Package Hrz.Returnables
Quick Start
Result — void operations
For commands and side-effects that don't return a value:
using Hrz.Returnables;
public Result DeleteUser(int id)
{
if (!repository.Exists(id))
{
return Result.Fail("User not found");
}
repository.Delete(id);
return Result.Success;
}
Result<TValue> — operations with data
For operations that return a value on success:
public Result<decimal> Divide(decimal dividend, decimal divisor)
{
if (divisor == 0)
{
return Result<decimal>.Fail("Division by zero");
}
return dividend / divisor; // implicit conversion
}
Consuming a Result
Check Succeeded or Failed, then access Value or Error:
var result = Divide(10, 3);
if (result.Succeeded)
{
Console.WriteLine($"Answer: {result.Value}");
}
else
{
Console.WriteLine($"Error: {result.Error.Message}");
}
Or use Switch for exhaustive handling:
result.Switch(
onSuccess: value => Console.WriteLine($"Answer: {value}"),
onFailure: error => Console.WriteLine($"Error: {error.Message}"));
Implicit Conversions
Results convert implicitly to and from their inner types:
// Value → Result (success)
Result<string> result = "Hello, world!";
// Error → Result (failure)
Result<string> result = Error.Create("BrokeCode", "Something broke");
// Result → Value (unwrap)
string value = result;
// Result → Error (extract error)
Error? error = result;
// Result<T> → Result (discard value, keep outcome)
Result voidResult = result;
Core API
Result (void)
| Member | Description |
|---|---|
Result.Success |
Pre-built success instance |
Result.Failure |
Pre-built failure with "Process failed" |
Result.Fail(string?) |
Failure from a message |
Result.Fail<TError>(TError) |
Failure from a typed exception |
Result.Fail() |
Failure with default message |
Result.From<T>(T?) |
Creates a Result<T> from a value |
Result.Try(Action) |
Wraps action in try/catch |
Result.TryAsync(Func<Task>) |
Wraps async action in try/catch |
Result.SuccessIf(bool, ...) |
Success when condition is true |
Result.FailIf(bool, ...) |
Failure when condition is true |
.Succeeded |
true on success |
.Failed |
true on failure |
.Error |
The exception, or null on success |
.Switch(onSuccess, onFailure) |
Exhaustive handling |
.OnSuccess(Action) |
Side-effect on success |
.OnFailure(Action<Error>) |
Side-effect on failure |
Result<TValue>
All members from Result (void) plus:
| Member | Description |
|---|---|
Result<T>.Failure |
Pre-built failure with "Process failed" |
Result<T>.From(T?) |
Success from value, failure if Exception |
Result<T>.Fail(...) |
Same overloads as void Result |
Result<T>.Try(Func<T>) |
Wraps function in try/catch |
Result<T>.TryAsync(Func<Task<T>>) |
Wraps async function in try/catch |
Result<T>.SuccessIf(bool, T?, ...) |
Conditional success with value |
Result<T>.FailIf(bool, T?, ...) |
Conditional failure with value |
.Value |
The success value, or default on failure |
.Switch(onSuccess, onFailure) |
Exhaustive handling with value |
.OnSuccess(Action<T>) |
Side-effect with value on success |
.OnFailure(Action<Exception>) |
Side-effect on failure |
Try / TryAsync
Wrap any operation — exceptions become failed results instead of propagating:
// Synchronous
Result<Config> config = Result<Config>.Try(
() => JsonSerializer.Deserialize<Config>(json)!);
// Async
Result<User> user = await Result<User>.TryAsync(
async () => await httpClient.GetFromJsonAsync<User>(url));
// Void
Result result = Result.Try(() => fileSystem.Delete(path));
SuccessIf / FailIf
Guard-clause style conditional results:
// With a message
Result result = Result.FailIf(string.IsNullOrEmpty(name), "Name is required");
// With a typed exception
Result result = Result.SuccessIf(user.IsActive, UnauthorizedAccessError.Create());
// With a value
Result<int> result = Result<int>.SuccessIf(age >= 18, value: age, errorMessage: "Must be 18+");
OnSuccess / OnFailure
Fluent side-effects for logging, telemetry, or caching without breaking the chain:
var result = GetUser(42)
.OnSuccess(user => logger.LogInformation("Loaded user {Id}", user.Id))
.OnFailure(error => logger.LogError(error, "User lookup failed"));
Contributing
- Fork the repository
- Create your feature branch from
main(release/your-featurefor new features,fix/your-fixfor patches) - Run the full check before submitting:
make check - Open a Pull Request against
main
Development Commands
make build # Build the solution
make test # Run all tests
make lint # Check code style
make branch-lint # Validate branch name
make commit-lint # Validate commit messages
make check # Full pre-PR validation (build + lint + branch + commits + tests)
make pack # Create NuGet package locally
License
This project is licensed under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.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.