| name | csharpessentials-results |
| description | Use when handling operation outcomes without exceptions — Result and Result<T> for success/failure, railway-oriented chaining with Then/ThenAsync/Ensure, Match for consumption, and Result.And/Or for combining multiple results. |
CSharpEssentials.Results
Result and Result<T> model operation outcomes as values. No exceptions for control flow.
Installation
dotnet add package CSharpEssentials.Results
Namespace
using CSharpEssentials.ResultPattern;
using CSharpEssentials.Errors;
Creating Results
Result ok = Result.Success();
Result<int> v = Result.Success(42);
Result fail = Result.Failure(Error.Validation("Input.Invalid", "Input was invalid."));
Result<string> fail1 = Result.Failure<string>(Error.NotFound("User.NotFound", "User not found."));
Result<string> fail2 = Result<string>.Failure(Error.Conflict("User.Duplicate", "Duplicate."));
Result<int> multi = Result.Failure<int>(
Error.Validation("Name.Empty", "Name is required."),
Error.Validation("Email.Invalid", "Email is invalid."));
Result<User> r = user;
Result<User> r = Error.NotFound("...", "...");
Checking the Result
if (result.IsFailure)
return result.FirstError;
if (result.IsSuccess)
return result.Value;
Chaining — railway-oriented
Result<int> result = Parse("5")
.Then(n => n * 2)
.Then(n => n + 10);
Result<OrderConfirmation> placed = await GetUserAsync(id)
.ThenAsync(user => ValidateOrderAsync(user, order))
.ThenAsync(order => ChargePaymentAsync(order));
Result<int> ensured = Result.Success(50)
.Ensure(v => v > 0, Error.Validation("Range", "Must be positive."))
.Ensure(v => v < 100, Error.Validation("Range", "Must be less than 100."));
Result<User> validated = await GetUserAsync(id)
.EnsureAsync(u => IsActiveAsync(u), Error.Validation("User.Inactive", "Account is inactive."));
Consuming — Match
string msg = result.Match(
onSuccess: value => $"OK: {value}",
onError: errors => $"Failed: {errors[0].Description}");
await result.MatchAsync(
onSuccess: async value => await SendConfirmationAsync(value),
onError: async errors => await LogErrorsAsync(errors));
Combining Results
Result combined = Result.And(r1, r2, r3);
Result any = Result.Or(r1, r2, r3);
Safe Execution
Result<int> safe = Result.Try(() => int.Parse(input), ex => Error.Exception(ex));
Result<Data> data = await Result.TryAsync(() => _db.GetAsync(id), ex => Error.Exception(ex));
Conditional Side Effects
result.TapIf(v => v > 0, v => Audit(v));
result.TapIf(featureEnabled, v => Track(v));
await result.TapIfAsync(v => v > 0, async v => await AuditAsync(v));
await result.TapIfAsync(isEnabled, async v => await TrackAsync(v));
await GetResultAsync().TapIfAsync(v => v > 0, v => Enqueue(v));
await GetValueTaskResultAsync().TapIfAsync(true, async v => await LogAsync(v));
Best Practices
- Never access
.Value without checking .IsSuccess first
onError in Match receives Error[] (array) — not a single Error
Then() short-circuits: once a failure occurs, subsequent Then() calls are skipped
- Prefer
Result.Failure<T> over the implicit Error → Result<T> conversion when self-documentation matters
- Use
Ensure() to add guard conditions without breaking the chain