FadiPhor.Result 0.0.14-preview

This is a prerelease version of FadiPhor.Result.
dotnet add package FadiPhor.Result --version 0.0.14-preview
                    
NuGet\Install-Package FadiPhor.Result -Version 0.0.14-preview
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FadiPhor.Result" Version="0.0.14-preview" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FadiPhor.Result" Version="0.0.14-preview" />
                    
Directory.Packages.props
<PackageReference Include="FadiPhor.Result" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FadiPhor.Result --version 0.0.14-preview
                    
#r "nuget: FadiPhor.Result, 0.0.14-preview"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FadiPhor.Result@0.0.14-preview
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FadiPhor.Result&version=0.0.14-preview&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=FadiPhor.Result&version=0.0.14-preview&prerelease
                    
Install as a Cake Tool

FadiPhor.Result

Result<T> is an abstract record with exactly two sealed subtypes:

  • Success<T> — holds a non-null value of type T.
  • Failure<T> — holds an Error.

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 switch expression over Result<T> (the _ => throw arm in Match, 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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