PANiXiDA.Core.ResultPattern 1.0.2

There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package PANiXiDA.Core.ResultPattern --version 1.0.2
                    
NuGet\Install-Package PANiXiDA.Core.ResultPattern -Version 1.0.2
                    
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="PANiXiDA.Core.ResultPattern" Version="1.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PANiXiDA.Core.ResultPattern" Version="1.0.2" />
                    
Directory.Packages.props
<PackageReference Include="PANiXiDA.Core.ResultPattern" />
                    
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 PANiXiDA.Core.ResultPattern --version 1.0.2
                    
#r "nuget: PANiXiDA.Core.ResultPattern, 1.0.2"
                    
#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 PANiXiDA.Core.ResultPattern@1.0.2
                    
#: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=PANiXiDA.Core.ResultPattern&version=1.0.2
                    
Install as a Cake Addin
#tool nuget:?package=PANiXiDA.Core.ResultPattern&version=1.0.2
                    
Install as a Cake Tool

PANiXiDA.Core.ResultPattern

PANiXiDA.Core.ResultPattern is a small .NET library for explicit success and failure handling in business logic without using exceptions as the primary control-flow contract.

It is designed for .NET developers who want predictable result-based workflows, typed errors, and composable synchronous and asynchronous operation pipelines.

Status

CI NuGet NuGet downloads Target Framework License

Overview

When a method in business logic can end not only with success but also with an expected failure, exceptions often become an awkward contract:

  • the method signature does not show that the method can fail;
  • business errors get mixed with technical exceptions;
  • the code starts to grow with try/catch blocks;
  • composing multiple steps becomes harder to read.

PANiXiDA.Core.ResultPattern addresses this by making operation outcomes explicit:

  • Result represents success or failure without a value;
  • Result<T> represents success or failure with a value;
  • Error provides a unified error model with type, message, and metadata;
  • extension methods such as Map, Bind, BindAsync, Ensure, Tap, and Match help compose operation pipelines.

This library is especially useful in:

  • application services;
  • use cases;
  • orchestrator layers;
  • domain factories and validators;
  • API boundaries.

Features

  • Explicit success/failure contract with Result and Result<T>
  • Typed error model through Error and ErrorType
  • Support for both single and multiple errors
  • Synchronous and asynchronous pipeline composition
  • Validation-style workflow support with error aggregation
  • Lightweight public API
  • XML-documented public API surface
  • Suitable for application, domain, and API boundary layers

Quick Start

Requirements

  • .NET 10 SDK

Installation

The library targets net10.0.

<ItemGroup>
  <PackageReference Include="PANiXiDA.Core.ResultPattern" Version="..." />
</ItemGroup>

Minimal import

using PANiXiDA.Core.ResultPattern;

First example

using PANiXiDA.Core.ResultPattern;

Result<string> GetUserName(bool exists)
{
    if (!exists)
    {
        return Result.Failure<string>(Error.NotFound("User not found"));
    }

    return Result.Success("John");
}

var result = GetUserName(exists: true);

if (result.IsSuccess)
{
    Console.WriteLine(result.Value);
}

Usage

Creating errors

using PANiXiDA.Core.ResultPattern;

var validationError = Error.Validation("Email is required");
var notFoundError = Error.NotFound("User not found");
var conflictError = Error.Conflict("Email is already in use");
var forbiddenError = Error.Forbidden("Insufficient permissions");

var fieldError = Error.Validation("Invalid email format")
    .WithField("email")
    .WithMetadata("attemptedValue", "not-an-email");

Result without a value

using PANiXiDA.Core.ResultPattern;

Result DeleteUser(bool userExists)
{
    if (!userExists)
    {
        return Result.Failure(Error.NotFound("User not found"));
    }

    return Result.Success();
}

Result<T> with a value

using PANiXiDA.Core.ResultPattern;

public sealed record UserDto(Guid Id, string Email);

Result<UserDto> GetUser(Guid id, UserDto? user)
{
    if (user is null)
    {
        return Result.Failure<UserDto>(Error.NotFound("User not found"));
    }

    return Result.Success(user);
}

Checking IsSuccess / IsFailure and reading FirstError

var result = DeleteUser(userExists: false);

if (result.IsFailure)
{
    Console.WriteLine(result.FirstError.Message);
}

Value, ValueOrDefault, and TryGetValue

var userResult = GetUser(Guid.NewGuid(), new UserDto(Guid.NewGuid(), "user@example.com"));

var value = userResult.Value;
var sameValue = userResult.ValueOrDefault;

if (userResult.TryGetValue(out var user))
{
    Console.WriteLine(user.Email);
}
var failedResult = Result.Failure<UserDto>(Error.NotFound("User not found"));

var defaultValue = failedResult.ValueOrDefault;
var hasValue = failedResult.TryGetValue(out var missingUser);

Console.WriteLine(defaultValue is null); // True
Console.WriteLine(hasValue);             // False
Console.WriteLine(missingUser is null);  // True

Returning multiple errors

Result ValidateRegistration(string email, string password)
{
    var errors = new List<Error>();

    if (string.IsNullOrWhiteSpace(email))
    {
        errors.Add(Error.Validation("Email is required").WithField("email"));
    }

    if (string.IsNullOrWhiteSpace(password))
    {
        errors.Add(Error.Validation("Password is required").WithField("password"));
    }

    if (errors.Count > 0)
    {
        return Result.Failure(errors);
    }

    return Result.Success();
}

Combine for joining multiple validations

var emailValidation = ValidateEmail(email);
var passwordValidation = ValidatePassword(password);
var agreementValidation = ValidateAgreement(agreementAccepted);

var validationResult = Result.Combine(
    emailValidation,
    passwordValidation,
    agreementValidation);

if (validationResult.IsFailure)
{
    return validationResult;
}

Map for transforming a result

Map is useful when the source operation is already successful and you only need to transform the value.

Result validationResult = ValidateRegistration(email, password);
Result<Guid> requestIdResult = validationResult.Map(() => Guid.NewGuid());
public sealed record User(Guid Id, string Email);
public sealed record UserResponse(Guid Id, string Email);

Result<User> userResult = Result.Success(new User(Guid.NewGuid(), "user@example.com"));

Result<UserResponse> responseResult = userResult.Map(user =>
{
    return new UserResponse(user.Id, user.Email);
});

Bind for composing steps that already return Result

Bind is useful when the next step can also fail.

Result validationResult = ValidateRegistration(email, password);
Result<Guid> createUserResult = validationResult.Bind(() =>
{
    return CreateUser(email, password);
});
Result<User> userResult = GetUserById(userId);
Result activationResult = userResult.Bind(ActivateUser);
Result<User> userResult = GetUserById(userId);

Result<UserResponse> responseResult = userResult.Bind(user =>
{
    return LoadProfile(user.Id).Map(profile =>
    {
        return new UserResponse(user.Id, user.Email);
    });
});

BindAsync for asynchronous composition

Result validationResult = ValidateRegistration(email, password);
Result<Guid> createUserResult = await validationResult.BindAsync(() =>
{
    return CreateUserAsync(email, password);
});
Result<User> userResult = await GetUserByIdAsync(userId);

Result<UserResponse> responseResult = await userResult.BindAsync(async user =>
{
    var profileResult = await LoadProfileAsync(user.Id);

    return profileResult.Map(profile =>
    {
        return new UserResponse(user.Id, user.Email);
    });
});

Ensure for additional checks after success

Result<User> userResult = GetUserById(userId);

Result<User> activeUserResult = userResult
    .Ensure(
        user => user.IsActive,
        Error.Forbidden("User is blocked"))
    .Ensure(
        user => user.EmailConfirmed,
        Error.Validation("Email is not confirmed").WithField("email"));

Tap for side effects

Tap does not change the result and is useful for logging, auditing, metrics, and other side effects.

Result<User> createResult = CreateUser(email, password);

Result<User> sameResult = createResult.Tap(user =>
{
    Console.WriteLine($"User created: {user.Id}");
});

Match for finishing the pipeline

Match is convenient at the application boundary, when you need to choose the final behavior for success and failure.

Result<UserResponse> result = GetUserById(userId)
    .Map(user =>
    {
        return new UserResponse(user.Id, user.Email);
    });

var response = result.Match(
    onSuccess: user =>
    {
        return $"200 OK: {user.Email}";
    },
    onFailure: errors =>
    {
        return $"400/404: {string.Join("; ", errors.Select(error => error.Message))}";
    });
Result deleteResult = DeleteUser(userExists: false);

var message = deleteResult.Match(
    onSuccess: () =>
    {
        return "User deleted";
    },
    onFailure: errors =>
    {
        return $"Deletion failed: {errors[0].Message}";
    });

Full pipeline example

using PANiXiDA.Core.ResultPattern;

public sealed record RegisterUserCommand(string Email, string Password);
public sealed record User(Guid Id, string Email, bool IsActive, bool EmailConfirmed);
public sealed record UserResponse(Guid Id, string Email);

public async Task<Result<UserResponse>> RegisterAsync(RegisterUserCommand command)
{
    var validationResult = ValidateRegistration(command.Email, command.Password);
    var uniqueEmailResult = validationResult.Bind(() =>
    {
        return EnsureEmailIsUnique(command.Email);
    });

    if (uniqueEmailResult.IsFailure)
    {
        return Result.Failure<UserResponse>(uniqueEmailResult.Errors);
    }

    var createResult = await uniqueEmailResult.BindAsync(() =>
    {
        return CreateUserAsync(command);
    });

    var guardedResult = createResult
        .Ensure(user => user.IsActive, Error.Failure("User was created in an inconsistent state"))
        .Ensure(user => user.EmailConfirmed, Error.Validation("Email is not confirmed").WithField("email"))
        .Tap(user =>
        {
            Console.WriteLine($"Created user {user.Id}");
        });

    return guardedResult.Map(user =>
    {
        return new UserResponse(user.Id, user.Email);
    });
}

API boundary example

public async Task<IResult> Register(RegisterUserCommand command)
{
    var result = await RegisterAsync(command);

    return result.Match<IResult>(
        onSuccess: user =>
        {
            return Results.Ok(user);
        },
        onFailure: errors =>
        {
            var firstError = errors[0];

            return firstError.Type switch
            {
                ErrorType.Validation => Results.BadRequest(errors),
                ErrorType.NotFound => Results.NotFound(errors),
                ErrorType.Conflict => Results.Conflict(errors),
                ErrorType.Unauthorized => Results.Unauthorized(),
                ErrorType.Forbidden => Results.StatusCode(StatusCodes.Status403Forbidden),
                _ => Results.StatusCode(StatusCodes.Status500InternalServerError)
            };
        });
}

Configuration

This library does not require runtime configuration.

There are no required:

  • environment variables;
  • appsettings.json entries;
  • secrets;
  • ports;
  • external services.

The only consumer-side requirement is referencing the package from a compatible .NET project.

Project Structure

.
├── src/
│   └── PANiXiDA.Core.ResultPattern/
│       └── PANiXiDA.Core.ResultPattern.csproj
├── tests/
│   └── PANiXiDA.Core.ResultPattern.UnitTests/
│       └── PANiXiDA.Core.ResultPattern.UnitTests.csproj
├── .editorconfig
├── .gitattributes
├── .gitignore
├── Directory.Build.props
├── Directory.Build.targets
├── Directory.Packages.props
├── global.json
├── version.json
├── LICENSE
└── README.md

Main repository files

  • src/ — library source code
  • tests/ — automated tests
  • Directory.Build.props — shared MSBuild settings
  • Directory.Build.targets — shared package metadata and package content settings
  • Directory.Packages.props — centralized package versions
  • global.json — SDK and test runner configuration
  • version.json — Nerdbank.GitVersioning configuration
  • .editorconfig — code style rules
  • README.md — package overview and usage documentation

Development

Build

dotnet restore
dotnet build --configuration Release

Format

dotnet format

Test

dotnet test --configuration Release

Full local validation

dotnet restore
dotnet format
dotnet build --configuration Release
dotnet test --configuration Release

Tooling and conventions

This repository uses:

  • .NET 10
  • Nullable enabled
  • Implicit usings enabled
  • Central package management
  • Microsoft Testing Platform
  • xUnit v3
  • FluentAssertions
  • Nerdbank.GitVersioning

API / Contracts / Examples

Core types

  • Error — immutable error model with Message, Type, and Metadata

  • ErrorType — supported error categories:

    • Validation
    • NotFound
    • Conflict
    • Unauthorized
    • Forbidden
    • Failure
    • Unexpected
  • Result — success or failure without a value

  • Result<T> — success or failure with a value

Core operations

  • Result.Success()
  • Result.Success<T>(value)
  • Result.Failure(...)
  • Result.Combine(...)
  • Map(...)
  • Bind(...)
  • BindAsync(...)
  • Ensure(...)
  • Tap(...)
  • Match(...)

Working with errors

Factory methods:

  • Error.Validation(message)
  • Error.NotFound(message)
  • Error.Conflict(message)
  • Error.Unauthorized(message)
  • Error.Forbidden(message)
  • Error.Failure(message)
  • Error.Unexpected(message)

Additional helpers:

  • WithMetadata(key, value)
  • WithField(field)

Working with values in Result<T>

  • Value — returns the value on success, otherwise throws InvalidOperationException
  • ValueOrDefault — returns the value on success, or default on failure
  • TryGetValue(out value) — safely attempts to get the value

Working with errors in Result

  • Errors — returns the list of errors
  • FirstError — returns the first error, otherwise throws InvalidOperationException
  • IsSuccess / IsFailure — explicit result state checks

Behavioral notes

  • Value throws InvalidOperationException when the result is a failure.
  • FirstError throws InvalidOperationException when the result is successful.
  • Combine aggregates errors from all failed results.
  • Match is intended for finishing a result pipeline at the application boundary.

Roadmap / TODO

Potential future improvements:

  • add more advanced composition helpers if a clear use case appears;
  • extend documentation with more domain-oriented examples;
  • add dedicated examples for ASP.NET Core minimal APIs;
  • keep the package as a reusable standard for future PANiXiDA NuGet libraries.

Contributing

Contributions are welcome if they keep the package focused and predictable.

General rules

  • keep the public API small and intentional;
  • avoid unnecessary dependencies;
  • preserve existing naming;
  • do not introduce breaking API changes without a strong reason;
  • public APIs must have XML documentation in English.

Code style

  • follow the repository .editorconfig;
  • do not introduce expression-bodied method declarations;
  • prefer explicit and readable code over overly compact code.

Tests

  • add or update tests for every meaningful behavior change;
  • cover happy path, guard clauses, and failure scenarios;
  • verify public API behavior, not implementation details, unless required;
  • add a regression test first when fixing a bug;
  • do not add using Xunit; or using FluentAssertions; in test files, because they are provided as global usings in the test project;
  • write DisplayName values in English;
  • structure tests using the Arrange, Act, Assert pattern.

Validation before completion

Before considering work complete, run:

dotnet restore
dotnet format
dotnet build --configuration Release
dotnet test --configuration Release

License

This project is licensed under the Apache License, Version 2.0.

See the LICENSE file for details.

Maintainers / Contacts

Maintained by the PANiXiDA.

Repository:

  • PANiXiDA-Dotnet-Core/result-pattern

For questions or improvements, use:

  • GitHub Issues
  • Pull Requests
  • repository discussions, if enabled
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 PANiXiDA.Core.ResultPattern:

Package Downloads
PANiXiDA.Core.Application

Core application-layer abstractions and building blocks for .NET applications, including contracts, messaging, validation, and use case orchestration.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.10-g3b7188c478 79 4/18/2026
1.0.2 113 4/18/2026
1.0.1 83 4/18/2026