ResultPattern.Net 1.0.0

dotnet add package ResultPattern.Net --version 1.0.0
                    
NuGet\Install-Package ResultPattern.Net -Version 1.0.0
                    
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="ResultPattern.Net" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ResultPattern.Net" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="ResultPattern.Net" />
                    
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 ResultPattern.Net --version 1.0.0
                    
#r "nuget: ResultPattern.Net, 1.0.0"
                    
#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 ResultPattern.Net@1.0.0
                    
#: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=ResultPattern.Net&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=ResultPattern.Net&version=1.0.0
                    
Install as a Cake Tool

Result Pattern

build

Result<T> represents the outcome of an operation: success with a value or failure with an error.

It is used to model failures that belong to the normal flow of an application:

  • Failures that are part of the application's normal flow.
  • Validation.
  • Resources not found.
  • Data reading.
  • Conflicts.
  • Permissions.
  • Persistence.
  • Invalid responses or expected errors from external services.
  • Flows where each step depends on the previous one.

It is useful in situations where an operation can fail normally or when failure is part of the expected behavior.

It does not replace exceptions, it complements them.

Why the Result Pattern?

The Result pattern provides a clear and explicit way to manage success and failure without relying on exceptions for control flow.

Instead of throwing exceptions or returning null, methods return a structured result that makes outcomes predictable and easier to handle.

❌ Problems it solves

Traditional approaches often lead to:

  • Hidden control flow through exceptions
  • null values and potential NullReferenceExceptions
  • Deeply nested if statements
  • Inconsistent error handling across the application

✔️ Benefits

With Result<T> you get:

  • Explicit success/failure flow
  • Non-exception-based control flow
  • Safer and more predictable APIs
  • Easier composition of operations
  • Improved testability

Result type

The fundamental usage of Result is by explicitly returning either success or failure.

Result (without a value) is used for operations where only the success or failure of the operation matters.

public Result SaveUser(User user)
{
    if (user is null)
        return Result.Failure(Error.Validation("User.Required", "User is required"));

    _users.Save(user);

    return Result.Ok();
}

Result<T> is used when an operation needs to return a value.

public Result<User> CreateUser(string name)
{
    if (string.IsNullOrWhiteSpace(name))
        return Result<User>.Failure(Error.Validation("User.NotFound", "User is required"));

    return Result<User>.Ok(new User(name));
}

The result can be returned either explicitly or implicitly through implicit conversion

Properties

Result<T> exposes the following properties to inspect the result state:

  • IsSuccess: Indicates whether the operation succeeded.
  • IsFailure: Indicates whether the operation failed. This is the inverse of IsSuccess.
  • Error: Contains the error associated with a failure. If the result is successful, it contains Error.None.
  • Value: Contains the result value when the operation succeeds. If the result is a failure, accessing this property throws an exception.

Check IsSuccess or IsFailure when the flow uses 'early returns'.

Result<User> result = await GetUserAsync(id);

if (result.IsFailure)
{
    Console.WriteLine($"Error: {result.Error.Message}");
    return;
}

User user = result.Value;
Console.WriteLine($"User found: {user.Name}");

Value should only be accessed after confirming that the result is successful. Otherwise, an exception will be thrown. This is the expected behavior.

Error type

Error describes an expected failure in a stable and structured way.

public sealed record Error(ErrorType Type, string Code, string Message);

| Property | Type | Description | ---------|-------|--------------------------| | Type | ErrorType | Categorizes the error at a high level. Makes it easier to identify the nature of the failure. |Code | string | A domain-specific error identifier. It should remain stable and be useful for tracing, logging, testing, and integrations. |Message | string | A human-readable description of the problem. It may vary depending on the context. <details> <summary>View ErrorType enum</summary>

public enum ErrorType
{
    // Success
    None,

    // Input / Permissions
    Validation,        // Invalid input data
    Unauthorized,      // Not authenticated / invalid credentials
    Forbidden,         // Insufficient permissions

    // State / Resources
    NotFound,          // Resource does not exist
    Conflict,          // Inconsistent state (e.g. duplicate, concurrency)
    InvalidOperation,  // Invalid state for performing the action
    NotSupported,      // Operation not supported in this context

    // Execution
    Timeout,           // Operation exceeded the time limit
    Cancelled,         // Operation was cancelled (cancellation token, user, etc.)

    // Other
    General,           // Unspecified or unknown error
    Internal,          // Internal technical error (bug, unhandled exception)
}

</details> <br>

The Error type provides static factory methods for representing the most common system errors.

You can use the built-in factories to create errors with a consistent format:

Error.Validation("User.EmailInvalid", "The email address is invalid.");
Error.NotFound("User.NotFound", $"The user with id '{id}' does not exist.");
Error.Conflict("User.AlreadyExists", "The user is already registered.");
Error.Unauthorized("Auth.InvalidCredentials", "The credentials are invalid.");
Error.Internal("Failed to read configuration.");
Error.General();

Implicit Conversion

Result<T> supports implicit conversions to reduce boilerplate and enable a more natural expression of the flow [docs].

This allows you to return either an Error or a value of type T directly, without manually wrapping them in a Result.

Error → Result / Result<T>

T → Result<T>

This reduces boilerplate and makes failure paths more concise, improving readability in methods that return Result<T>.

public async Task<Result<User>> GetUserAsync(int id)
{
    var user = await _repo.FindAsync(id);

    return user is null
        ? Error.NotFound("User.NotFound", $"The user with id '{id}' does not exist")
        : user;
}

In the example above:

  • Error → Result<User> through implicit conversion
  • User → Result<User> through implicit conversion

Extensions

Extensions are available in both synchronous and asynchronous versions.

The asynchronous version is where the pattern provides the most value, allowing you to chain validations, repositories, external services, and transformations without multiple nested if statements. It operates on Task<Result<T>> and is the most common choice in modern applications, where I/O operations and result composition are prevalent.

Function Purpose
Map / MapAsync Transform
Bind / BindAsync Chain
Ensure / EnsureAsync Validate
OnSuccess / OnSuccessAsync Execute on success
OnFailure / OnFailureAsync Execute on failure
Match / MatchAsync Close (returns a value)
Switch / SwitchAsync Close (executes an action)

Match / MatchAsync

Use Match or MatchAsync to return a value by handling both outcomes (success or failure). Define what should happen when the operation succeeds and what should happen when it fails.

Both delegates must return the same type.

string message = await GetUserAsync(id)
    .MatchAsync(
        user => $"User: {user.Name}",
        error => $"Error: {error.Message}");

Switch / SwitchAsync

Use Switch or SwitchAsync to terminate the flow and execute an action. It does not return a value.

await GetUserAsync(id)
    .SwitchAsync(
        user => Console.WriteLine($"Current user: {user.Name}"),
        error => Console.WriteLine(error.Message));

Map / MapAsync

Map and MapAsync are used to transform the value contained in a successful Result<T>.

If the result is a failure, the error is propagated without applying the transformation.

// Async: User -> UserDto if GetUserAsync succeeds
Result<UserDto> asyncResult = await GetUserAsync(id)
    .MapAsync(user => new UserDto(user.Id, user.Name));

// Sync: synchronous version
Result<UserDto> syncResult = GetUser(id)
    .Map(user => new UserDto(user.Id, user.Name));

Bind / BindAsync

Bind and BindAsync are used to chain multiple dependent operations. Use them when the next step in the chain also returns a Result<T> and can therefore fail. They allow complex flows to be expressed linearly, without nesting.

If any operation fails, the error is automatically propagated, returned, and execution stops.

// Chaining with another operation that returns Result
Result<Profile> asyncResult = await GetUserAsync(id)
    .BindAsync(user => GetProfileAsync(user.Id));

// Multiple dependent operations
Result<Order> result = await ValidateRequestAsync(request)
    .BindAsync(validRequest => CreateOrderAsync(validRequest))
    .BindAsync(order => ReserveStockAsync(order))
    .BindAsync(order => SaveOrderAsync(order));

The flow reads from top to bottom as a sequence of domain steps.

This approach avoids callback hell and deeply nested if statements, keeping the execution flow more declarative and easier to read.

Ensure / EnsureAsync

Use Ensure or EnsureAsync to validate the value of a successful Result without leaving the flow.

// Async
Result<User> asyncResult = await GetUserAsync(id)
    .EnsureAsync(
        user => user.IsActive,
        Error.Validation("User.State", "The user is not active."));

// Sync
Result<User> syncResult = GetUser(id)
    .Ensure(
        user => user.IsActive,
        Error.Validation("User.State", "The user is not active."));

If the result is already a failure, the validation is not executed and the original error is preserved.

OnSuccess / OnSuccessAsync

Use OnSuccess or OnSuccessAsync to execute a side effect only when the Result is successful, without modifying the value or interrupting the flow.

They are useful for logging, metrics, caching, notifications, and similar scenarios.

// Async
Result<User> asyncResult = await GetUserAsync(id)
    .OnSuccessAsync(user => Log($"User found: {user.Id}"));

// Sync
Result<User> syncResult = GetUser(id)
    .OnSuccess(user => Log($"User found: {user.Id}"));

OnSuccess is useful when you need to observe the flow without affecting its outcome.

OnFailure / OnFailureAsync

Use OnFailure or OnFailureAsync to execute a side effect (logging, metrics, alerts, etc.) only when the operation fails, without modifying the result or interrupting the flow.

// Async
Result<User> asyncResult = await GetUserAsync(id)
    .OnFailureAsync(error => Log($"Error: {error.Message}"));

// Sync
Result<User> syncResult = GetUser(id)
    .OnFailure(error => Log($"Error: {error.Message}"));

It does not transform the error; it only observes the failure and returns the same result, allowing the chain to continue.

Complete Example

public async Task<Result<UserDto>> GetUserDtoAsync(int id)
{
    return await GetUserAsync(id)
        .EnsureAsync(
            user => user.IsActive,
            Error.Validation("User.State", "The user is not active."))
        .BindAsync(user => LoadPermissionsAsync)
        .OnSuccessAsync(user => Log($"User loaded: {user.Id}"))
        .OnFailureAsync(error => Log($"Error: {error.Message}"))
        .MapAsync(UserToDto);
}

private static UserDto UserToDto(User user) =>
    new(user.Id, user.Name);

Consuming the result:

public async Task<string> GetUserMessageAsync(int id)
{
    var result = await _userService.GetUserDtoAsync(id);

    return result.Match(
        dto => $"User: {dto.Name}",
        error => $"Error: {error.Message}");
}

This flow represents a declarative composition where each step operates on the result of the previous one without breaking the chain.

If a step fails, the following steps are not executed. The error is propagated to the end of the flow.

The synchronous flow is best suited for pure validations, in-memory transformations, or domain rules that do not require I/O.

public Result<int> CalculateTotal(int price)
{
    return Ok(price)
        .Ensure(value => value > 0,
                Error.Validation("Value.Invalid", "The value must be greater than 0"))
        .Map(value => value * 2)
        .OnSuccess(value => Log($"Calculated total: {value}"));
}

Usage Guidelines

🟢 Best Practices
  • Use Result<T> for expected use-case failures.
  • Return an Error as soon as a validation fails.
  • Prefer async flows when working with I/O, external services, or repositories.
  • Use Map when you only need to transform the value.
  • Use Bind when the next step returns a Result.
  • Use Ensure for validations within the chain.
  • Use OnSuccess and OnFailure for side effects (logging, metrics, etc.).
  • End flows with Match, IsFailure, or IsSuccess.
  • Keep Code values stable; Message values may change.
  • If there is no valid value, return an Error instead of null.
🔴 Anti-Patterns
  • Do not use Result<T> to hide programming errors.
  • Do not access Value without checking for success first.
  • Do not use OnSuccess or OnFailure to modify the result.
  • Do not return null as a successful result.
  • Do not mix domain logic and error handling within the same flow.

Use Result<T> to control the flow, not to hide it.

Contributing

Contributions are welcome! Feel free to open an issue or submit a pull request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Copyright (c) 2026 BracoZS

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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 86 5/31/2026