FadiPhor.Result
0.0.14-preview
dotnet add package FadiPhor.Result --version 0.0.14-preview
NuGet\Install-Package FadiPhor.Result -Version 0.0.14-preview
<PackageReference Include="FadiPhor.Result" Version="0.0.14-preview" />
<PackageVersion Include="FadiPhor.Result" Version="0.0.14-preview" />
<PackageReference Include="FadiPhor.Result" />
paket add FadiPhor.Result --version 0.0.14-preview
#r "nuget: FadiPhor.Result, 0.0.14-preview"
#:package FadiPhor.Result@0.0.14-preview
#addin nuget:?package=FadiPhor.Result&version=0.0.14-preview&prerelease
#tool nuget:?package=FadiPhor.Result&version=0.0.14-preview&prerelease
FadiPhor.Result
Result<T> is an abstract record with exactly two sealed subtypes:
Success<T>— holds a non-null value of typeT.Failure<T>— holds anError.
All types are records (immutable). T is constrained to notnull.
Base Type: All Result<T> types inherit from a non-generic abstract Result base that exposes IsSuccess and IsFailure properties. This enables middleware and infrastructure code to operate on results without knowing their generic payload type.
Error Model
Error is an abstract record with a required Code (string), an optional Message, and an abstract HttpStatusCode:
public abstract record Error(string Code)
{
public virtual string? Message => null;
public abstract int HttpStatusCode { get; }
}
Every error must declare its HTTP status code. This allows infrastructure code (middleware, filters, etc.) to map any error to the correct HTTP response without pattern-matching on concrete types.
Define domain errors by inheriting from Error:
public record InsufficientFundsError(decimal Required, decimal Available) : Error("insufficient_funds")
{
public override int HttpStatusCode => 402;
public override string? Message => $"Required {Required:C} but only {Available:C} available";
}
ValidationFailure
A built-in Error subtype for returning multiple validation issues:
var issues = new List<ValidationIssue>
{
new("Email", "Email is required"),
new("Age", "Must be 18 or older"),
new("Name", "Name is unusually short", ValidationSeverity.Warning)
};
return new ValidationFailure(issues);
ValidationFailure always has Code = "validation.failed". Each ValidationIssue carries an Identifier, Message, and Severity (defaults to ValidationSeverity.Error).
ValidationSeverity values: Error, Warning, Info.
Built-in Error Types
The library ships with common error types that cover the most frequent failure scenarios. Each has a fixed Code, HttpStatusCode, and an optional custom Message:
| Type | Code | HttpStatusCode | Default Message |
|---|---|---|---|
NotFoundError |
"not_found" |
404 |
"The requested resource was not found." |
UnauthenticatedError |
"unauthenticated" |
401 |
"Authentication is required." |
UnauthorizedError |
"unauthorized" |
403 |
"You do not have permission to perform this action." |
ConflictError |
"conflict" |
409 |
"The request conflicts with the current state of the resource." |
UnexpectedError |
"unexpected" |
500 |
"An unexpected error occurred." |
ValidationFailure |
"validation.failed" |
422 |
null |
Use with a default message:
return new NotFoundError();
Or provide a custom diagnostic message:
return new NotFoundError($"User {userId} was not found.");
return new UnauthenticatedError("Token has expired.");
return new UnauthorizedError("Admin role required.");
return new ConflictError("Duplicate email address.");
Use HttpStatusCode for generic error-to-response mapping without pattern-matching:
return result.Match(
onSuccess: user => Ok(user),
onFailure: error => StatusCode(error.HttpStatusCode, new { error.Code, error.Message })
);
Or pattern-match across error types for fine-grained control:
return result.Match(
onSuccess: user => Ok(user),
onFailure: error => error switch
{
ValidationFailure vf => BadRequest(new { vf.Issues }),
NotFoundError => NotFound(),
UnauthenticatedError => Unauthorized(),
UnauthorizedError => Forbid(),
ConflictError => Conflict(),
UnexpectedError => StatusCode(500),
_ => Problem(detail: error.Code)
}
);
Error Codes
All built-in error codes are available as constants on FadiPhorErrorCodes:
if (error.Code == FadiPhorErrorCodes.NotFound)
{
// handle not found
}
Available constants: ValidationFailed, NotFound, Unauthenticated, Unauthorized, Conflict, Unexpected.
Unit
Use Result<Unit> for operations that succeed or fail without producing a value:
public Result<Unit> DeleteUser(int id)
{
if (!repository.Exists(id))
return new NotFoundError($"user/{id} was not found");
repository.Delete(id);
return Unit.Value; // implicit conversion to Success<Unit>
}
Creating Results
Implicit Conversions
Result<T> defines implicit operators from T → Success<T> and Error → Failure<T>. This means you can return values or errors directly when the return type is Result<T>:
public Result<User> GetUser(int id)
{
var user = repository.Find(id);
if (user is null)
return new NotFoundError($"user/{id} was not found"); // implicit → Failure<User>
return user; // implicit → Success<User>
}
Both conversions throw ArgumentNullException if given null.
Factory Methods
When implicit conversion is not applicable (e.g. in generic contexts), use the ResultFactory class:
var success = ResultFactory.Success(42);
var failure = ResultFactory.Failure<int>(new NotFoundError("item/7 was not found"));
Composition
Bind
Chains operations that return Result<T>. On failure, the chain short-circuits and propagates the error:
var result = GetOrder(orderId)
.Bind(order => ValidateOrder(order))
.Bind(order => ChargePayment(order))
.Bind(order => CreateShipment(order));
Async variant — operates on Task<Result<T>>:
var result = await GetOrderAsync(orderId)
.Bind(order => ValidateOrderAsync(order))
.Bind(order => ChargePaymentAsync(order));
Ensure
Validates a success value against a predicate. Returns the original result if the predicate passes, or converts it to a failure:
var result = GetUser(id)
.Ensure(
user => user.IsActive,
() => new Error("user.inactive"));
If the result is already a failure, Ensure passes it through unchanged.
MapError
Transforms the error in a failure without affecting successes:
var result = GetUser(id)
.MapError(error => new WrappedError(error.Code));
Tap
Executes a side effect on a success value without changing the result. Failures pass through unchanged:
var result = GetUser(id)
.Tap(user => logger.LogInformation("Found user {Id}", user.Id))
.Bind(user => MapToDto(user));
TryGetValue
Extracts the success value using a try-pattern:
if (result.TryGetValue(out var user))
{
// use user
}
TryGetError
Extracts the error from a failure using a try-pattern. Useful for early-return orchestration:
var result = await ValidateAsync(request);
if (result.TryGetError(out var error))
return error; // implicit Error → Result<T>
GetValueOrThrow
Extracts the success value after failure has been handled. Throws InvalidOperationException if called on a failure:
if (result.TryGetError(out var error))
return error;
var value = result.GetValueOrThrow();
Match
Exhaustively handles both cases and produces a value:
return result.Match(
onSuccess: user => Ok(user),
onFailure: error => error switch
{
NotFoundError => NotFound(),
ValidationFailure vf => BadRequest(new { vf.Issues }),
_ => Problem(detail: error.Code)
}
);
Realistic Chain Example
public async Task<Result<OrderConfirmation>> PlaceOrder(PlaceOrderRequest request)
{
return await ValidateRequest(request)
.Bind(req => GetCustomerAsync(req.CustomerId))
.Bind(customer => CreateOrderAsync(customer, request.Items))
.Bind(order => ChargePaymentAsync(order))
.Bind(order => Task.FromResult(
ResultFactory.Success(new OrderConfirmation(order.Id, order.Total))));
}
If ValidateRequest returns a ValidationFailure, none of the subsequent steps execute. If ChargePaymentAsync fails, the error propagates and OrderConfirmation is never created.
Infrastructure Usage
The non-generic Result base type allows middleware, pipeline behaviors, and cross-cutting concerns to inspect and transform results without knowing the generic payload type.
Check status
if (response is Result result && result.IsFailure)
{
_logger.LogWarning("Operation failed");
}
TryGetError (non-generic)
Extracts the error from any Result without knowing its generic type. Returns false and assigns null for successes:
if (response is Result result &&
result.TryGetError(out var error))
{
logger.LogWarning("Failure: {Code}", error.Code);
}
MapError (non-generic)
Transforms the error of any Result without knowing its generic type. Successes pass through unchanged. The concrete Result<T> type is preserved at runtime:
if (response is Result result)
{
response = result.MapError(error =>
new WrappedError($"service.{error.Code}"));
}
GetHttpStatusCode
Returns the HTTP status code for any Result. For successes, returns the provided successStatusCode (defaults to 200). For failures, returns the error's HttpStatusCode:
if (response is Result result)
{
var statusCode = result.GetHttpStatusCode(); // 200 on success, error code on failure
var statusCode = result.GetHttpStatusCode(201); // 201 on success (e.g. for created)
}
These extensions operate on the base Result type and are intended strictly for failure-side infrastructure. They do not expose the success value.
Union Structure
Result<T> is a binary union. Success<T> and Failure<T> are both sealed. The IsSuccess and IsFailure properties are available but Match / Bind are the primary consumption patterns.
Introducing additional union states (e.g. PartialSuccess<T>) would require:
- A new sealed subtype of
Result<T>. - Updating every
switchexpression overResult<T>(the_ => throwarm inMatch,Bind,Ensure,MapError,Tap). - Updating the JSON converter if serialization is used.
This is by design — the sealed structure makes the union exhaustive and any extension is a deliberate, auditable change.
| 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 FadiPhor.Result:
| Package | Downloads |
|---|---|
|
FadiPhor.Result.Serialization.Json
System.Text.Json serialization support for FadiPhor.Result, including discriminated union handling and polymorphic error serialization. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.0.14-preview | 99 | 4/13/2026 |
| 0.0.13-preview | 79 | 4/12/2026 |
| 0.0.12-preview | 69 | 4/12/2026 |