FailOr 1.0.0
See the version list below for details.
dotnet add package FailOr --version 1.0.0
NuGet\Install-Package FailOr -Version 1.0.0
<PackageReference Include="FailOr" Version="1.0.0" />
<PackageVersion Include="FailOr" Version="1.0.0" />
<PackageReference Include="FailOr" />
paket add FailOr --version 1.0.0
#r "nuget: FailOr, 1.0.0"
#:package FailOr@1.0.0
#addin nuget:?package=FailOr&version=1.0.0
#tool nuget:?package=FailOr&version=1.0.0
FailOr
FailOr is a small .NET result-type library for representing success or one-or-more failures without relying on exceptions for normal control flow.
It centers on three public entry points:
FailOr<T>for a successful value or a collection of failuresFailuresfor the abstract failure union carried by failed resultsFailurefor factory methods that createFailures.General,Failures.Validation, andFailures.Exceptional
The library also includes convenience APIs for common result workflows:
FailOr.Success(...)andFailOr.Fail(...)for constructionThen(...),ThenAsync(...),ThenEnsure(...),ThenEnsureAsync(...),FailWhen(...),FailWhenAsync(...),ThenDo(...),ThenDoAsync(...),IfSuccess(...), andIfSuccessAsync(...)for chaining, success validation, success-side effects, and terminal success observationIfFail(...)andIfFailAsync(...)for observing failures without recoveringMatch(...)andMatchFirst(...)for branchingZip(...)for aggregating multiple resultsCombine(...)for choosing a preferred success with fallback
Target framework
FailOr currently targets net10.0.
Installation
Install from NuGet:
dotnet add package FailOr
Core concepts
FailOr<T> is either:
- a success value of type
T - a failure result containing one or more
Failuresvalues
Failures always exposes:
CodeDetails
Concrete failure values are created through the Failure factory surface:
Failure.General(...)for general-purpose failures with optional metadataFailure.Validation(...)for property-scoped validation failures with one-or-more messagesFailure.Exceptional(...)for failures that wrap exceptions
Use IsSuccess, IsFailure, UnsafeUnwrap(), and Failures to inspect a result directly when needed.
Quick start
Create success and failure results
using FailOr;
var success = FailOr.Success(42);
var failure = FailOr.Fail<int>(Failure.General("Input was invalid."));
FailOr<int> implicitSuccess = 42;
FailOr<int> implicitSingleFailure = Failure.General("Input was invalid.");
Failures[] implicitFailures =
[
Failure.General("Primary problem"),
Failure.General("Secondary problem")
];
FailOr<int> implicitFailureArray = implicitFailures;
FailOr<int> implicitFailureList = new List<Failures>
{
Failure.General("First list problem"),
Failure.General("Second list problem")
};
if (success.IsSuccess)
{
Console.WriteLine(success.UnsafeUnwrap());
}
if (failure.IsFailure)
{
Console.WriteLine(failure.Failures[0].Details);
}
The implicit conversions delegate to the same Success(...) and Fail(...) validation paths, so null reference successes and invalid failure collections still throw the same exceptions as the factory methods.
Create specific failure cases
using FailOr;
var general = Failure.General(
"Request timed out.",
code: "Http.Timeout",
metadata: new Dictionary<string, object?> { ["attempt"] = 3 });
var validation = Failure.Validation(
"Email",
"Email is required.",
"Email must contain '@'.");
var exceptional = Failure.Exceptional(new InvalidOperationException("Operation failed."));
Chain success values with Then
Use Then when the next step should only run after success.
using FailOr;
var result = FailOr.Success(10)
.Then(value => value + 5)
.Then(value => FailOr.Success(value * 2));
var finalValue = result.UnsafeUnwrap(); // 30
Async variants are also available:
using FailOr;
var result = await FailOr.Success(10)
.ThenAsync(async value =>
{
await Task.Delay(10);
return value + 5;
});
Map safely with Try
Use Try when the next step should only run after success, but thrown exceptions should become failures instead of escaping the pipeline.
using FailOr;
var result = FailOr.Success("42")
.Try(value => int.Parse(value));
if (result.IsFailure)
{
var failure = (Failures.Exceptional)result.Failures[0];
Console.WriteLine(failure.Exception.Message);
}
You can also translate the exception into a custom repository-native result:
using FailOr;
var result = FailOr.Success("42x")
.Try(
value => int.Parse(value),
exception => Failure.General($"Mapping failed: {exception.Message}"));
Validate success values with ThenEnsure
Use ThenEnsure when the next step should validate the current success and keep that original value flowing when validation succeeds.
using FailOr;
var result = FailOr.Success(10)
.ThenEnsure(value =>
value >= 0
? FailOr.Success(true)
: FailOr.Fail<bool>(Failure.General("Value must be non-negative.")))
.Then(value => value + 5);
var finalValue = result.UnsafeUnwrap(); // 15
Async validation helpers are also available:
using FailOr;
var result = await FailOr.Success(10)
.ThenEnsureAsync(async value =>
{
await Task.Delay(10);
return value % 2 == 0
? FailOr.Success(true)
: FailOr.Fail<bool>(Failure.General("Value must be even."));
});
Fail a success when a predicate matches with FailWhen
Use FailWhen when the validation rule is naturally expressed as "this success should fail when this condition is true." Use ThenEnsure when the validation step is better modeled as another FailOr-producing operation.
using FailOr;
var result = FailOr.Success(3)
.FailWhen(value => value % 2 != 0, Failure.General("Value must be even."));
Async predicate-based validation is available for direct and task-wrapped results:
using FailOr;
var result = await Task.FromResult(FailOr.Success(10))
.FailWhenAsync(
async value =>
{
await Task.Delay(10);
return value < 0;
},
Failure.General("Value must be non-negative."));
Run success-side effects with ThenDo
Use ThenDo when you want to observe a success without changing the flowing result.
using FailOr;
var result = FailOr.Success(10)
.ThenDo(value => Console.WriteLine($"Observed: {value}"))
.Then(value => value + 5);
var finalValue = result.UnsafeUnwrap(); // 15
The same side-effect helpers are available for task-wrapped results:
using FailOr;
var result = await Task.FromResult(FailOr.Success(10))
.ThenDoAsync(async value =>
{
await Task.Delay(10);
Console.WriteLine($"Observed: {value}");
})
.Then(value => value + 5);
Observe terminal success with IfSuccess
Use IfSuccess when you want to observe a success as a terminal side effect and do not need to keep chaining the FailOr<T> value. Use ThenDo when the same observation should preserve the result for continued chaining.
using FailOr;
FailOr.Success(10)
.IfSuccess(value => Console.WriteLine($"Observed terminal success: {value}"));
await Task.FromResult(FailOr.Success(10))
.IfSuccessAsync(async value =>
{
await Task.Delay(10);
Console.WriteLine($"Observed terminal success: {value}");
});
Run failure-side effects with IfFail
Use IfFail when you want to observe a failure without replacing it, recovering from it, or otherwise changing control flow.
using FailOr;
var result = FailOr.Fail<int>(Failure.General("Primary lookup failed."));
result.IfFail(failures => Console.WriteLine($"Observed: {failures[0].Details}"));
Async variants are available for both direct and task-wrapped results:
using FailOr;
await Task.FromResult(FailOr.Fail<int>(Failure.General("Primary lookup failed.")))
.IfFailAsync(async failures =>
{
await Task.Delay(10);
Console.WriteLine($"Observed {failures.Count} failure(s).");
});
Branch with Match
Use Match when you want a single expression that handles both outcomes.
using FailOr;
var message = FailOr.Fail<int>(Failure.General("The value could not be produced."))
.Match(
success: value => $"Value: {value}",
failure: failures => failures[0].Details);
Use MatchFirst when only the first failure matters:
using FailOr;
var message = FailOr.Fail<int>(
Failure.General("Primary problem"),
Failure.General("Secondary problem"))
.MatchFirst(
success: value => $"Value: {value}",
failure: firstFailure => firstFailure.Details);
Aggregate with Zip
Zip combines successful results into tuples and preserves failures in left-to-right order.
using FailOr;
var zipped = FailOr.Zip(
FailOr.Success(1),
FailOr.Success("two"),
FailOr.Success(true));
var (number, text, flag) = zipped.UnsafeUnwrap();
If any input fails, the returned result is failed and contains every Failures value from the failed inputs, including mixed failure cases such as validation and exceptional failures.
Prefer one result with Combine
Combine returns the left result when it succeeds; otherwise it returns the right result.
using FailOr;
var preferred = FailOr.Combine(
FailOr.Fail<int>(Failure.General("Primary source unavailable")),
FailOr.Success(99));
Console.WriteLine(preferred.UnsafeUnwrap()); // 99
Local development
Restore local tools, packages, build, and test from the repository root:
dotnet tool restore
dotnet restore fail-or.slnx
dotnet build fail-or.slnx
dotnet test --solution fail-or.slnx
Format C# code with the repository-local CSharpier tool:
dotnet csharpier format .
Verify formatting without rewriting files:
dotnet csharpier check .
Create a local package with an explicit version:
dotnet pack src/FailOr/FailOr.csproj -c Release -p:Version=1.2.3
GitHub Actions
The repository includes two workflows:
CIruns on pushes tomainand pull requests targetingmainReleaseruns when a GitHub Release is published
The CI workflow restores, builds, and tests the solution so it can be used as a required branch-protection check.
The release workflow:
- reads the GitHub Release tag
- requires the tag to match
v<NuGetSemVer> - strips the leading
v - runs restore, build, test, and pack
- publishes the resulting package to nuget.org only after all checks pass
Examples of accepted release tags:
v1.2.3v1.2.3-beta.1
Examples of rejected release tags:
1.2.3release-1.2.3
NuGet trusted publishing setup
Before the release workflow can publish to nuget.org, complete this setup:
- Create the GitHub repository.
- Verify project metadata and repository links point to
oneirosoft/fail-or. - In nuget.org, configure a trusted publishing policy for this repository and the workflow file
release.yml. - Add a GitHub repository variable named
NUGET_ORG_USERNAMEwith the nuget.org account or profile name that owns the package. - Optionally configure a GitHub Actions environment if you want additional release approvals or environment-scoped controls.
The workflow requests an OIDC token and exchanges it for a short-lived NuGet API key during the release job. No long-lived NuGet API key is stored in GitHub.
Releasing
The release process is:
- Ensure
mainis green. - Create and push a tag in the form
v<NuGetSemVer>. - Publish a GitHub Release for that tag.
- Let the
Releaseworkflow build, test, pack, and publish the package.
The GitHub Release tag is the package version source of truth. The project file intentionally does not hardcode a package version.
Repository metadata
Repository metadata:
- GitHub repository:
https://github.com/oneirosoft/fail-or - Issue tracker:
https://github.com/oneirosoft/fail-or/issues - Releases:
https://github.com/oneirosoft/fail-or/releases
License
This project is licensed under the MIT License. See the repository LICENSE file for the full text.
| 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 (1)
Showing the top 1 NuGet packages that depend on FailOr:
| Package | Downloads |
|---|---|
|
FailOr.Validations
Typed property-based validation helpers for FailOr with selector normalization and transform pipelines. |
GitHub repositories
This package is not used by any popular GitHub repositories.