diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..8a0a6f1
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,30 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
+!**/.gitignore
+!.git/HEAD
+!.git/config
+!.git/packed-refs
+!.git/refs/heads/**
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..9e451e5
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,141 @@
+[*]
+indent_style = space
+csharp_indent_labels = one_less_than_current
+csharp_using_directive_placement = outside_namespace:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_prefer_braces = true:silent
+csharp_style_namespace_declarations = file_scoped:warning
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_throw_expression = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+csharp_style_prefer_switch_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_pattern_matching = true:silent
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_extended_property_pattern = true:suggestion
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = false:silent
+csharp_style_var_elsewhere = false:silent
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
+csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
+csharp_prefer_system_threading_lock = true:suggestion
+csharp_style_prefer_readonly_struct_member = true:suggestion
+csharp_style_prefer_readonly_struct = true:suggestion
+csharp_prefer_static_anonymous_function = true:suggestion
+csharp_prefer_static_local_function = true:suggestion
+csharp_space_around_binary_operators = before_and_after
+
+[*.{cs,vb}]
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+tab_width = 4
+indent_size = 4
+end_of_line = lf
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_explicit_tuple_names = true:warning
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:warning
+dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
+dotnet_style_namespace_match_folder = true:suggestion
+dotnet_code_quality_unused_parameters = all:suggestion
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_property = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_event = false:silent
+dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
+dotnet_style_allow_multiple_blank_lines_experimental = true:silent
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_readonly_field = true:suggestion
+dotnet_diagnostic.IDE0005.severity = error
+dotnet_diagnostic.CS1591.severity = none
+
+[src/**/*.cs]
+dotnet_diagnostic.CS1591.severity = error
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..20a51ba
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,40 @@
+* text=auto
+
+# Source
+*.cs text eol=lf
+*.csx text eol=lf
+
+# Project files
+*.csproj text eol=lf
+*.props text eol=lf
+*.targets text eol=lf
+*.sln text eol=crlf
+*.slnx text eol=lf
+
+# Config / docs
+*.json text eol=lf
+*.jsonc text eol=lf
+*.xml text eol=lf
+*.config text eol=lf
+*.editorconfig text eol=lf
+*.gitattributes text eol=lf
+*.gitignore text eol=lf
+*.md text eol=lf
+*.yml text eol=lf
+*.yaml text eol=lf
+
+# Scripts
+*.ps1 text eol=crlf
+*.bat text eol=crlf
+*.cmd text eol=crlf
+*.sh text eol=lf
+
+# Binary assets
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.pdf binary
+*.zip binary
+*.dll binary
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..620092a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,42 @@
+name: CI
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+
+permissions:
+ contents: read
+ packages: write
+
+jobs:
+ format:
+ uses: PANiXiDA-Infrastructure/ci-cd/.github/workflows/dotnet-format.yml@main
+ secrets:
+ registry-user: ${{ secrets.REGISTRY_USER }}
+ registry-token: ${{ secrets.REGISTRY_TOKEN }}
+
+ tests:
+ uses: PANiXiDA-Infrastructure/ci-cd/.github/workflows/dotnet-tests.yml@main
+ secrets:
+ registry-user: ${{ secrets.REGISTRY_USER }}
+ registry-token: ${{ secrets.REGISTRY_TOKEN }}
+
+ publish:
+ needs:
+ - format
+ - tests
+ uses: PANiXiDA-Infrastructure/ci-cd/.github/workflows/dotnet-publish-global-nuget.yml@main
+ secrets:
+ NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
+
+ notify:
+ if: ${{ always() && github.event_name != 'pull_request' }}
+ needs: [ publish ]
+ uses: PANiXiDA-Infrastructure/ci-cd/.github/workflows/notify-telegram.yml@main
+ with:
+ event-name: ${{ github.event_name }}
+ secrets:
+ telegram-bot-token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
+ telegram-chat-id: ${{ vars.TELEGRAM_CHAT_ID }}
+ github-token: ${{ secrets.REGISTRY_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3fa8e50
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+*.suo
+*.user
+[Bb]in/
+[Oo]bj/
+.vs/
+.idea
+TestResults/
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..722d98f
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,91 @@
+# AGENTS.md
+
+## Purpose
+
+This repository is a reference template for PANiXiDA .NET NuGet packages.
+The goal is to keep package structure, CI, versioning, tests, documentation, and release flow consistent across repositories.
+
+## Repository layout
+
+- `src/` — package source code
+- `tests/` — automated tests
+- `README.md` — package overview and usage examples
+- `Directory.Build.props` — shared build settings
+- `Directory.Packages.props` — centralized package versions
+- `version.json` — Nerdbank.GitVersioning configuration
+- `ci.yml` — CI pipeline
+
+## Technology baseline
+
+- .NET 10
+- Nullable enabled
+- Implicit usings enabled
+- Central package management enabled
+- Microsoft Testing Platform
+- xUnit v3
+- FluentAssertions
+- Nerdbank.GitVersioning
+
+## Coding rules
+
+- Preserve existing naming.
+- Do not introduce expression-bodied method declarations.
+- Prefer explicit, readable code over short clever code.
+- Public APIs must include XML documentation written in English.
+- Avoid breaking public API changes unless explicitly requested.
+- Do not weaken nullability annotations.
+- Do not silently change exception behavior.
+
+## Library design rules
+
+- Keep public API small and intentional.
+- Favor immutability for public models.
+- Validate all public method arguments.
+- Prefer deterministic behavior and stable error messages.
+- Do not add dependencies without a strong reason.
+
+## 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.
+- When fixing a bug, add a regression test first.
+- 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.
+
+## Documentation
+
+- Update `README.md` when public behavior, public API, package metadata, or development workflow changes.
+- Keep the README structure aligned with the repository standard.
+- Keep installation instructions, target framework, badges, and package information accurate.
+- Keep all examples compilable and aligned with the current API.
+- Ensure usage examples reflect real and recommended library usage.
+- Remove outdated or duplicate documentation when updating existing sections.
+
+## Build and validation
+
+Before considering work complete, run:
+
+- `dotnet restore`
+- `dotnet format`
+- `dotnet build --configuration Release`
+- `dotnet test --configuration Release`
+
+## Packaging and versioning
+
+- Package metadata must stay complete and consistent.
+- README and icon must be included in the package.
+- Changes affecting package contents must be reflected in versioning inputs.
+- Do not change package id, repository url, or license without explicit request.
+
+## Definition of done
+
+A change is done only when:
+
+- code is formatted,
+- code builds,
+- tests pass,
+- relevant tests were added or updated,
+- README was updated if public behavior changed,
+- no unnecessary files or dependencies were introduced.
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..97d7c62
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,16 @@
+
+
+ net10.0
+ enable
+ enable
+
+ true
+ true
+ true
+
+ true
+ true
+ latest
+ $(NoWarn);NU1507
+
+
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 0000000..f6b4be9
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,29 @@
+
+
+ PANiXiDA
+ PANiXiDA
+ git
+
+ true
+ true
+
+ true
+ snupkg
+
+ README.md
+ Apache-2.0
+ icon.png
+
+
+
+
+
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..1272c60
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,13 @@
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
diff --git a/PANiXiDA.Core.ResultPattern.slnx b/PANiXiDA.Core.ResultPattern.slnx
new file mode 100644
index 0000000..7934d64
--- /dev/null
+++ b/PANiXiDA.Core.ResultPattern.slnx
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index ce644d8..23a6920 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,657 @@
-# panixida-core-result-pattern
\ No newline at end of file
+# 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
+
+[](https://github.com/PANiXiDA-Dotnet-Core/result-pattern/actions/workflows/ci.yml)
+[](https://www.nuget.org/packages/PANiXiDA.Core.ResultPattern)
+[](https://www.nuget.org/packages/PANiXiDA.Core.ResultPattern)
+[](https://dotnet.microsoft.com/)
+[](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` 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`
+- 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`.
+
+```xml
+
+
+
+```
+
+### Minimal import
+
+```csharp
+using PANiXiDA.Core.ResultPattern;
+```
+
+### First example
+
+```csharp
+using PANiXiDA.Core.ResultPattern;
+
+Result GetUserName(bool exists)
+{
+ if (!exists)
+ {
+ return Result.Failure(Error.NotFound("User not found"));
+ }
+
+ return Result.Success("John");
+}
+
+var result = GetUserName(exists: true);
+
+if (result.IsSuccess)
+{
+ Console.WriteLine(result.Value);
+}
+```
+
+## Usage
+
+### Creating errors
+
+```csharp
+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
+
+```csharp
+using PANiXiDA.Core.ResultPattern;
+
+Result DeleteUser(bool userExists)
+{
+ if (!userExists)
+ {
+ return Result.Failure(Error.NotFound("User not found"));
+ }
+
+ return Result.Success();
+}
+```
+
+### `Result` with a value
+
+```csharp
+using PANiXiDA.Core.ResultPattern;
+
+public sealed record UserDto(Guid Id, string Email);
+
+Result GetUser(Guid id, UserDto? user)
+{
+ if (user is null)
+ {
+ return Result.Failure(Error.NotFound("User not found"));
+ }
+
+ return Result.Success(user);
+}
+```
+
+### Checking `IsSuccess` / `IsFailure` and reading `FirstError`
+
+```csharp
+var result = DeleteUser(userExists: false);
+
+if (result.IsFailure)
+{
+ Console.WriteLine(result.FirstError.Message);
+}
+```
+
+### `Value`, `ValueOrDefault`, and `TryGetValue`
+
+```csharp
+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);
+}
+```
+
+```csharp
+var failedResult = Result.Failure(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
+
+```csharp
+Result ValidateRegistration(string email, string password)
+{
+ var errors = new List();
+
+ 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
+
+```csharp
+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.
+
+```csharp
+Result validationResult = ValidateRegistration(email, password);
+Result requestIdResult = validationResult.Map(() => Guid.NewGuid());
+```
+
+```csharp
+public sealed record User(Guid Id, string Email);
+public sealed record UserResponse(Guid Id, string Email);
+
+Result userResult = Result.Success(new User(Guid.NewGuid(), "user@example.com"));
+
+Result 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.
+
+```csharp
+Result validationResult = ValidateRegistration(email, password);
+Result createUserResult = validationResult.Bind(() =>
+{
+ return CreateUser(email, password);
+});
+```
+
+```csharp
+Result userResult = GetUserById(userId);
+Result activationResult = userResult.Bind(ActivateUser);
+```
+
+```csharp
+Result userResult = GetUserById(userId);
+
+Result responseResult = userResult.Bind(user =>
+{
+ return LoadProfile(user.Id).Map(profile =>
+ {
+ return new UserResponse(user.Id, user.Email);
+ });
+});
+```
+
+### `BindAsync` for asynchronous composition
+
+```csharp
+Result validationResult = ValidateRegistration(email, password);
+Result createUserResult = await validationResult.BindAsync(() =>
+{
+ return CreateUserAsync(email, password);
+});
+```
+
+```csharp
+Result userResult = await GetUserByIdAsync(userId);
+
+Result 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
+
+```csharp
+Result userResult = GetUserById(userId);
+
+Result 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.
+
+```csharp
+Result createResult = CreateUser(email, password);
+
+Result 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.
+
+```csharp
+Result 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))}";
+ });
+```
+
+```csharp
+Result deleteResult = DeleteUser(userExists: false);
+
+var message = deleteResult.Match(
+ onSuccess: () =>
+ {
+ return "User deleted";
+ },
+ onFailure: errors =>
+ {
+ return $"Deletion failed: {errors[0].Message}";
+ });
+```
+
+### Full pipeline example
+
+```csharp
+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> RegisterAsync(RegisterUserCommand command)
+{
+ var validationResult = ValidateRegistration(command.Email, command.Password);
+ var uniqueEmailResult = validationResult.Bind(() =>
+ {
+ return EnsureEmailIsUnique(command.Email);
+ });
+
+ if (uniqueEmailResult.IsFailure)
+ {
+ return Result.Failure(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
+
+```csharp
+public async Task Register(RegisterUserCommand command)
+{
+ var result = await RegisterAsync(command);
+
+ return result.Match(
+ 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
+
+```text
+.
+├── 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
+
+```bash
+dotnet restore
+dotnet build --configuration Release
+```
+
+### Format
+
+```bash
+dotnet format
+```
+
+### Test
+
+```bash
+dotnet test --configuration Release
+```
+
+### Full local validation
+
+```bash
+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` — success or failure with a value
+
+### Core operations
+
+* `Result.Success()`
+* `Result.Success(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`
+
+* `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:
+
+```bash
+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](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
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..1d364c6
--- /dev/null
+++ b/global.json
@@ -0,0 +1,9 @@
+{
+ "sdk": {
+ "version": "10.0.100",
+ "rollForward": "latestFeature"
+ },
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}
diff --git a/icon.png b/icon.png
new file mode 100644
index 0000000..0e15a5e
Binary files /dev/null and b/icon.png differ
diff --git a/src/PANiXiDA.Core.ResultPattern/Error.cs b/src/PANiXiDA.Core.ResultPattern/Error.cs
new file mode 100644
index 0000000..275c55d
--- /dev/null
+++ b/src/PANiXiDA.Core.ResultPattern/Error.cs
@@ -0,0 +1,154 @@
+using System.Collections.ObjectModel;
+
+namespace PANiXiDA.Core.ResultPattern;
+
+///
+/// Represents an immutable error returned by an operation result.
+///
+public sealed record Error
+{
+ ///
+ /// The metadata key used to store the field name for validation errors.
+ ///
+ public const string FieldMetadataKey = "field";
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// A human-readable error message.
+ /// The error category.
+ /// Additional machine-readable error data.
+ public Error(
+ string message,
+ ErrorType type,
+ IReadOnlyDictionary? metadata = null)
+ {
+ if (string.IsNullOrWhiteSpace(message))
+ {
+ throw new ArgumentException("Error message cannot be empty.", nameof(message));
+ }
+
+ Message = message;
+ Type = type;
+ Metadata = new ReadOnlyDictionary(
+ metadata is null
+ ? []
+ : new Dictionary(metadata));
+ }
+
+ ///
+ /// Gets the error message.
+ ///
+ public string Message { get; }
+
+ ///
+ /// Gets the error category.
+ ///
+ public ErrorType Type { get; }
+
+ ///
+ /// Gets additional machine-readable error details.
+ ///
+ public IReadOnlyDictionary Metadata { get; }
+
+ ///
+ /// Creates a validation error.
+ ///
+ /// The validation error message.
+ /// A new instance of with type .
+ public static Error Validation(string message)
+ {
+ return new Error(message, ErrorType.Validation);
+ }
+
+ ///
+ /// Creates an error for a missing entity or resource.
+ ///
+ /// The error message.
+ /// A new instance of with type .
+ public static Error NotFound(string message)
+ {
+ return new Error(message, ErrorType.NotFound);
+ }
+
+ ///
+ /// Creates a state conflict error.
+ ///
+ /// The conflict error message.
+ /// A new instance of with type .
+ public static Error Conflict(string message)
+ {
+ return new Error(message, ErrorType.Conflict);
+ }
+
+ ///
+ /// Creates an authorization error.
+ ///
+ /// The authorization error message.
+ /// A new instance of with type .
+ public static Error Unauthorized(string message)
+ {
+ return new Error(message, ErrorType.Unauthorized);
+ }
+
+ ///
+ /// Creates an insufficient permissions error.
+ ///
+ /// The access error message.
+ /// A new instance of with type .
+ public static Error Forbidden(string message)
+ {
+ return new Error(message, ErrorType.Forbidden);
+ }
+
+ ///
+ /// Creates a business or application failure error.
+ ///
+ /// The error message.
+ /// A new instance of with type .
+ public static Error Failure(string message)
+ {
+ return new Error(message, ErrorType.Failure);
+ }
+
+ ///
+ /// Creates an unexpected error.
+ ///
+ /// The error message.
+ /// A new instance of with type .
+ public static Error Unexpected(string message)
+ {
+ return new Error(message, ErrorType.Unexpected);
+ }
+
+ ///
+ /// Returns a copy of the current error with an added or replaced metadata entry.
+ ///
+ /// The metadata key to add or replace.
+ /// The metadata value associated with the key.
+ /// A new instance of with updated metadata.
+ public Error WithMetadata(string key, object? value)
+ {
+ var metadata = new Dictionary(Metadata)
+ {
+ [key] = value
+ };
+
+ return new Error(Message, Type, metadata);
+ }
+
+ ///
+ /// Returns a copy of the current error with the field name stored in metadata.
+ ///
+ /// The field name associated with the error.
+ /// A new instance of with the field metadata populated.
+ public Error WithField(string field)
+ {
+ if (string.IsNullOrWhiteSpace(field))
+ {
+ throw new ArgumentException("Field cannot be empty.", nameof(field));
+ }
+
+ return WithMetadata(FieldMetadataKey, field);
+ }
+}
diff --git a/src/PANiXiDA.Core.ResultPattern/ErrorType.cs b/src/PANiXiDA.Core.ResultPattern/ErrorType.cs
new file mode 100644
index 0000000..7d946a1
--- /dev/null
+++ b/src/PANiXiDA.Core.ResultPattern/ErrorType.cs
@@ -0,0 +1,42 @@
+namespace PANiXiDA.Core.ResultPattern;
+
+///
+/// Defines the error categories supported by the library.
+///
+public enum ErrorType
+{
+ ///
+ /// An input validation error.
+ ///
+ Validation = 1,
+
+ ///
+ /// An error indicating that an entity or resource was not found.
+ ///
+ NotFound = 2,
+
+ ///
+ /// An error indicating a conflict with the current state.
+ ///
+ Conflict = 3,
+
+ ///
+ /// An authorization error.
+ ///
+ Unauthorized = 4,
+
+ ///
+ /// An insufficient permissions error.
+ ///
+ Forbidden = 5,
+
+ ///
+ /// An expected business or application failure.
+ ///
+ Failure = 6,
+
+ ///
+ /// An unexpected error that does not belong to a narrower category.
+ ///
+ Unexpected = 7
+}
diff --git a/src/PANiXiDA.Core.ResultPattern/GenericResult.cs b/src/PANiXiDA.Core.ResultPattern/GenericResult.cs
new file mode 100644
index 0000000..527c891
--- /dev/null
+++ b/src/PANiXiDA.Core.ResultPattern/GenericResult.cs
@@ -0,0 +1,77 @@
+namespace PANiXiDA.Core.ResultPattern;
+
+///
+/// Represents the result of an operation that can complete successfully with a value or fail with an error.
+///
+/// The type of the successful value.
+public sealed class Result : Result
+{
+ private readonly TValue? value;
+
+ ///
+ /// Initializes a new successful instance of .
+ ///
+ /// The successful value.
+ internal Result(TValue value)
+ : base(true, EmptyErrors)
+ {
+ this.value = value;
+ }
+
+ ///
+ /// Initializes a new failed instance of .
+ ///
+ /// Errors associated with the failed result.
+ internal Result(Error[] errors)
+ : base(false, errors)
+ {
+ value = default;
+ }
+
+ ///
+ /// Gets the successful result value.
+ ///
+ /// Thrown when the result is a failure.
+ public TValue Value
+ {
+ get
+ {
+ if (IsFailure)
+ {
+ throw new InvalidOperationException("Cannot access value of failed result.");
+ }
+
+ return value!;
+ }
+ }
+
+ ///
+ /// Gets the successful value or if the result is a failure.
+ ///
+ public TValue? ValueOrDefault
+ {
+ get
+ {
+ return IsSuccess ? value : default;
+ }
+ }
+
+ ///
+ /// Attempts to get the successful value without throwing an exception.
+ ///
+ ///
+ /// When this method returns, contains the successful value or if the result is a failure.
+ ///
+ /// if the result is successful; otherwise, .
+ public bool TryGetValue(out TValue? resultValue)
+ {
+ if (IsSuccess)
+ {
+ resultValue = value;
+ return true;
+ }
+
+ resultValue = default;
+ return false;
+ }
+}
diff --git a/src/PANiXiDA.Core.ResultPattern/PANiXiDA.Core.ResultPattern.csproj b/src/PANiXiDA.Core.ResultPattern/PANiXiDA.Core.ResultPattern.csproj
new file mode 100644
index 0000000..447089f
--- /dev/null
+++ b/src/PANiXiDA.Core.ResultPattern/PANiXiDA.Core.ResultPattern.csproj
@@ -0,0 +1,20 @@
+
+
+ true
+
+ PANiXiDA.Core.ResultPattern
+
+ PANiXiDA.Core.ResultPattern
+ A library for implementing the Result pattern in .NET applications with typed errors, result composition support, and convenient error handling without exceptions in business logic.
+ Result;ResultPattern;Errors;Functional;DotNet
+
+ https://github.com/PANiXiDA-Dotnet-Core/result-pattern
+ https://github.com/PANiXiDA-Dotnet-Core/result-pattern
+
+
+
+
+ all
+
+
+
diff --git a/src/PANiXiDA.Core.ResultPattern/Result.cs b/src/PANiXiDA.Core.ResultPattern/Result.cs
new file mode 100644
index 0000000..202bee7
--- /dev/null
+++ b/src/PANiXiDA.Core.ResultPattern/Result.cs
@@ -0,0 +1,186 @@
+using System.Collections.ObjectModel;
+
+namespace PANiXiDA.Core.ResultPattern;
+
+///
+/// Represents the result of an operation that can complete successfully or fail with an error.
+///
+public class Result
+{
+ ///
+ /// An empty error collection used for successful results.
+ ///
+ protected static readonly Error[] EmptyErrors = [];
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// Indicates whether the result is successful.
+ /// Errors associated with the result.
+ protected Result(bool isSuccess, Error[] errors)
+ {
+ ArgumentNullException.ThrowIfNull(errors);
+
+ if (isSuccess && errors.Length > 0)
+ {
+ throw new ArgumentException("Successful result cannot contain errors.", nameof(errors));
+ }
+
+ if (!isSuccess && errors.Length == 0)
+ {
+ throw new ArgumentException("Failed result must contain at least one error.", nameof(errors));
+ }
+
+ IsSuccess = isSuccess;
+ Errors = new ReadOnlyCollection(errors.ToArray());
+ }
+
+ ///
+ /// Gets a value indicating whether the result is successful.
+ ///
+ public bool IsSuccess { get; }
+
+ ///
+ /// Gets a value indicating whether the result completed with an error.
+ ///
+ public bool IsFailure
+ {
+ get
+ {
+ return !IsSuccess;
+ }
+ }
+
+ ///
+ /// Gets the collection of errors associated with the result.
+ ///
+ public IReadOnlyList Errors { get; }
+
+ ///
+ /// Gets the first error of the result.
+ ///
+ /// Thrown when the result is successful.
+ public Error FirstError
+ {
+ get
+ {
+ if (IsSuccess)
+ {
+ throw new InvalidOperationException("Successful result does not contain errors.");
+ }
+
+ return Errors[0];
+ }
+ }
+
+ ///
+ /// Creates a successful result without a value.
+ ///
+ /// A successful instance of .
+ public static Result Success()
+ {
+ return new Result(true, EmptyErrors);
+ }
+
+ ///
+ /// Creates a successful result with a value.
+ ///
+ /// The type of the successful value.
+ /// The value to wrap in the result.
+ /// A successful instance of .
+ public static Result Success(TValue value)
+ {
+ return new Result(value);
+ }
+
+ ///
+ /// Creates a failed result with a single error.
+ ///
+ /// The error associated with the failed result.
+ /// A failed instance of .
+ public static Result Failure(Error error)
+ {
+ ArgumentNullException.ThrowIfNull(error);
+ return new Result(false, [error]);
+ }
+
+ ///
+ /// Creates a failed result with multiple errors.
+ ///
+ /// Errors associated with the failed result.
+ /// A failed instance of .
+ public static Result Failure(IEnumerable errors)
+ {
+ ArgumentNullException.ThrowIfNull(errors);
+
+ var materializedErrors = errors.Where(item => item is not null).ToArray();
+
+ if (materializedErrors.Length == 0)
+ {
+ throw new ArgumentException("Failed result must contain at least one error.", nameof(errors));
+ }
+
+ return new Result(false, materializedErrors);
+ }
+
+ ///
+ /// Creates a failed generic result with a single error.
+ ///
+ /// The type of the value that would be returned on success.
+ /// The error associated with the failed result.
+ /// A failed instance of .
+ public static Result Failure(Error error)
+ {
+ ArgumentNullException.ThrowIfNull(error);
+ return new Result([error]);
+ }
+
+ ///
+ /// Creates a failed generic result with multiple errors.
+ ///
+ /// The type of the value that would be returned on success.
+ /// Errors associated with the failed result.
+ /// A failed instance of .
+ public static Result Failure(IEnumerable errors)
+ {
+ ArgumentNullException.ThrowIfNull(errors);
+
+ var materializedErrors = errors.Where(static item => item is not null).ToArray();
+
+ if (materializedErrors.Length == 0)
+ {
+ throw new ArgumentException("Failed result must contain at least one error.", nameof(errors));
+ }
+
+ return new Result(materializedErrors);
+ }
+
+ ///
+ /// Combines multiple results into a single result.
+ ///
+ /// The results to combine.
+ ///
+ /// A successful result if all provided results are successful; otherwise, a failed result with all collected errors.
+ ///
+ public static Result Combine(params Result[] results)
+ {
+ ArgumentNullException.ThrowIfNull(results);
+
+ if (results.Any(static item => item is null))
+ {
+ throw new ArgumentException("Results collection cannot contain null values.", nameof(results));
+ }
+
+ var errors = results
+ .Where(item => item.IsFailure)
+ .SelectMany(item => item.Errors)
+ .ToArray();
+
+ if (errors.Length == 0)
+ {
+ return Success();
+ }
+
+ return Failure(errors);
+ }
+}
diff --git a/src/PANiXiDA.Core.ResultPattern/ResultCombiner.cs b/src/PANiXiDA.Core.ResultPattern/ResultCombiner.cs
new file mode 100644
index 0000000..28a0067
--- /dev/null
+++ b/src/PANiXiDA.Core.ResultPattern/ResultCombiner.cs
@@ -0,0 +1,393 @@
+namespace PANiXiDA.Core.ResultPattern;
+
+///
+/// Provides helper methods for combining multiple generic results.
+///
+public static class ResultCombiner
+{
+ ///
+ /// Combines two generic results into a single tuple result.
+ ///
+ /// The type of the first result value.
+ /// The type of the second result value.
+ /// The first result.
+ /// The second result.
+ ///
+ /// A successful result containing a tuple of values if both results are successful; otherwise, a failed result with all collected errors.
+ ///
+ public static Result<(T1, T2)> Combine(
+ Result first,
+ Result second)
+ {
+ ArgumentNullException.ThrowIfNull(first);
+ ArgumentNullException.ThrowIfNull(second);
+
+ return CombineCore(
+ () => Result.Combine(first, second),
+ () => Result.Success((first.Value, second.Value)));
+ }
+
+ ///
+ /// Combines two generic results and continues the successful result with a function that returns another generic result.
+ ///
+ /// The type of the first result value.
+ /// The type of the second result value.
+ /// The type of the continuation result value.
+ /// The first result.
+ /// The second result.
+ /// The continuation function to execute when all results are successful.
+ ///
+ /// The result returned by if both results are successful; otherwise, a failed result with all collected errors.
+ ///
+ public static Result Combine(
+ Result first,
+ Result second,
+ Func> bind)
+ {
+ ArgumentNullException.ThrowIfNull(bind);
+
+ return Combine(first, second)
+ .Bind(values => bind(values.Item1, values.Item2));
+ }
+
+ ///
+ /// Combines three generic results into a single tuple result.
+ ///
+ /// The type of the first result value.
+ /// The type of the second result value.
+ /// The type of the third result value.
+ /// The first result.
+ /// The second result.
+ /// The third result.
+ ///
+ /// A successful result containing a tuple of values if all results are successful; otherwise, a failed result with all collected errors.
+ ///
+ public static Result<(T1, T2, T3)> Combine(
+ Result first,
+ Result second,
+ Result third)
+ {
+ ArgumentNullException.ThrowIfNull(first);
+ ArgumentNullException.ThrowIfNull(second);
+ ArgumentNullException.ThrowIfNull(third);
+
+ return CombineCore(
+ () => Result.Combine(first, second, third),
+ () => Result.Success((first.Value, second.Value, third.Value)));
+ }
+
+ ///
+ /// Combines three generic results and continues the successful result with a function that returns another generic result.
+ ///
+ /// The type of the first result value.
+ /// The type of the second result value.
+ /// The type of the third result value.
+ /// The type of the continuation result value.
+ /// The first result.
+ /// The second result.
+ /// The third result.
+ /// The continuation function to execute when all results are successful.
+ ///
+ /// The result returned by if all results are successful; otherwise, a failed result with all collected errors.
+ ///
+ public static Result Combine(
+ Result first,
+ Result second,
+ Result third,
+ Func> bind)
+ {
+ ArgumentNullException.ThrowIfNull(bind);
+
+ return Combine(first, second, third)
+ .Bind(values => bind(values.Item1, values.Item2, values.Item3));
+ }
+
+ ///
+ /// Combines four generic results into a single tuple result.
+ ///
+ /// The type of the first result value.
+ /// The type of the second result value.
+ /// The type of the third result value.
+ /// The type of the fourth result value.
+ /// The first result.
+ /// The second result.
+ /// The third result.
+ /// The fourth result.
+ ///
+ /// A successful result containing a tuple of values if all results are successful; otherwise, a failed result with all collected errors.
+ ///
+ public static Result<(T1, T2, T3, T4)> Combine(
+ Result first,
+ Result second,
+ Result third,
+ Result fourth)
+ {
+ ArgumentNullException.ThrowIfNull(first);
+ ArgumentNullException.ThrowIfNull(second);
+ ArgumentNullException.ThrowIfNull(third);
+ ArgumentNullException.ThrowIfNull(fourth);
+
+ return CombineCore(
+ () => Result.Combine(first, second, third, fourth),
+ () => Result.Success((first.Value, second.Value, third.Value, fourth.Value)));
+ }
+
+ ///
+ /// Combines four generic results and continues the successful result with a function that returns another generic result.
+ ///
+ /// The type of the first result value.
+ /// The type of the second result value.
+ /// The type of the third result value.
+ /// The type of the fourth result value.
+ /// The type of the continuation result value.
+ /// The first result.
+ /// The second result.
+ /// The third result.
+ /// The fourth result.
+ /// The continuation function to execute when all results are successful.
+ ///
+ /// The result returned by if all results are successful; otherwise, a failed result with all collected errors.
+ ///
+ public static Result Combine(
+ Result first,
+ Result second,
+ Result third,
+ Result fourth,
+ Func> bind)
+ {
+ ArgumentNullException.ThrowIfNull(bind);
+
+ return Combine(first, second, third, fourth)
+ .Bind(values => bind(values.Item1, values.Item2, values.Item3, values.Item4));
+ }
+
+ ///
+ /// Combines five generic results into a single tuple result.
+ ///
+ /// The type of the first result value.
+ /// The type of the second result value.
+ /// The type of the third result value.
+ /// The type of the fourth result value.
+ /// The type of the fifth result value.
+ /// The first result.
+ /// The second result.
+ /// The third result.
+ /// The fourth result.
+ /// The fifth result.
+ ///
+ /// A successful result containing a tuple of values if all results are successful; otherwise, a failed result with all collected errors.
+ ///
+ public static Result<(T1, T2, T3, T4, T5)> Combine(
+ Result first,
+ Result second,
+ Result third,
+ Result fourth,
+ Result fifth)
+ {
+ ArgumentNullException.ThrowIfNull(first);
+ ArgumentNullException.ThrowIfNull(second);
+ ArgumentNullException.ThrowIfNull(third);
+ ArgumentNullException.ThrowIfNull(fourth);
+ ArgumentNullException.ThrowIfNull(fifth);
+
+ return CombineCore(
+ () => Result.Combine(first, second, third, fourth, fifth),
+ () => Result.Success((first.Value, second.Value, third.Value, fourth.Value, fifth.Value)));
+ }
+
+ ///
+ /// Combines five generic results and continues the successful result with a function that returns another generic result.
+ ///
+ /// The type of the first result value.
+ /// The type of the second result value.
+ /// The type of the third result value.
+ /// The type of the fourth result value.
+ /// The type of the fifth result value.
+ /// The type of the continuation result value.
+ /// The first result.
+ /// The second result.
+ /// The third result.
+ /// The fourth result.
+ /// The fifth result.
+ /// The continuation function to execute when all results are successful.
+ ///
+ /// The result returned by if all results are successful; otherwise, a failed result with all collected errors.
+ ///
+ public static Result Combine(
+ Result first,
+ Result second,
+ Result third,
+ Result fourth,
+ Result fifth,
+ Func> bind)
+ {
+ ArgumentNullException.ThrowIfNull(bind);
+
+ return Combine(first, second, third, fourth, fifth)
+ .Bind(values => bind(values.Item1, values.Item2, values.Item3, values.Item4, values.Item5));
+ }
+
+ ///
+ /// Combines six generic results into a single tuple result.
+ ///
+ /// The type of the first result value.
+ /// The type of the second result value.
+ /// The type of the third result value.
+ /// The type of the fourth result value.
+ /// The type of the fifth result value.
+ /// The type of the sixth result value.
+ /// The first result.
+ /// The second result.
+ /// The third result.
+ /// The fourth result.
+ /// The fifth result.
+ /// The sixth result.
+ ///
+ /// A successful result containing a tuple of values if all results are successful; otherwise, a failed result with all collected errors.
+ ///
+ public static Result<(T1, T2, T3, T4, T5, T6)> Combine(
+ Result first,
+ Result second,
+ Result third,
+ Result fourth,
+ Result fifth,
+ Result sixth)
+ {
+ ArgumentNullException.ThrowIfNull(first);
+ ArgumentNullException.ThrowIfNull(second);
+ ArgumentNullException.ThrowIfNull(third);
+ ArgumentNullException.ThrowIfNull(fourth);
+ ArgumentNullException.ThrowIfNull(fifth);
+ ArgumentNullException.ThrowIfNull(sixth);
+
+ return CombineCore(
+ () => Result.Combine(first, second, third, fourth, fifth, sixth),
+ () => Result.Success((first.Value, second.Value, third.Value, fourth.Value, fifth.Value, sixth.Value)));
+ }
+
+ ///
+ /// Combines six generic results and continues the successful result with a function that returns another generic result.
+ ///
+ /// The type of the first result value.
+ /// The type of the second result value.
+ /// The type of the third result value.
+ /// The type of the fourth result value.
+ /// The type of the fifth result value.
+ /// The type of the sixth result value.
+ /// The type of the continuation result value.
+ /// The first result.
+ /// The second result.
+ /// The third result.
+ /// The fourth result.
+ /// The fifth result.
+ /// The sixth result.
+ /// The continuation function to execute when all results are successful.
+ ///
+ /// The result returned by if all results are successful; otherwise, a failed result with all collected errors.
+ ///
+ public static Result Combine(
+ Result first,
+ Result second,
+ Result third,
+ Result fourth,
+ Result fifth,
+ Result sixth,
+ Func> bind)
+ {
+ ArgumentNullException.ThrowIfNull(bind);
+
+ return Combine(first, second, third, fourth, fifth, sixth)
+ .Bind(values => bind(values.Item1, values.Item2, values.Item3, values.Item4, values.Item5, values.Item6));
+ }
+
+ ///
+ /// Combines seven generic results into a single tuple result.
+ ///
+ /// The type of the first result value.
+ /// The type of the second result value.
+ /// The type of the third result value.
+ /// The type of the fourth result value.
+ /// The type of the fifth result value.
+ /// The type of the sixth result value.
+ /// The type of the seventh result value.
+ /// The first result.
+ /// The second result.
+ /// The third result.
+ /// The fourth result.
+ /// The fifth result.
+ /// The sixth result.
+ /// The seventh result.
+ ///
+ /// A successful result containing a tuple of values if all results are successful; otherwise, a failed result with all collected errors.
+ ///
+ public static Result<(T1, T2, T3, T4, T5, T6, T7)> Combine(
+ Result first,
+ Result second,
+ Result third,
+ Result fourth,
+ Result fifth,
+ Result sixth,
+ Result seventh)
+ {
+ ArgumentNullException.ThrowIfNull(first);
+ ArgumentNullException.ThrowIfNull(second);
+ ArgumentNullException.ThrowIfNull(third);
+ ArgumentNullException.ThrowIfNull(fourth);
+ ArgumentNullException.ThrowIfNull(fifth);
+ ArgumentNullException.ThrowIfNull(sixth);
+ ArgumentNullException.ThrowIfNull(seventh);
+
+ return CombineCore(
+ () => Result.Combine(first, second, third, fourth, fifth, sixth, seventh),
+ () => Result.Success((first.Value, second.Value, third.Value, fourth.Value, fifth.Value, sixth.Value, seventh.Value)));
+ }
+
+ ///
+ /// Combines seven generic results and continues the successful result with a function that returns another generic result.
+ ///
+ /// The type of the first result value.
+ /// The type of the second result value.
+ /// The type of the third result value.
+ /// The type of the fourth result value.
+ /// The type of the fifth result value.
+ /// The type of the sixth result value.
+ /// The type of the seventh result value.
+ /// The type of the continuation result value.
+ /// The first result.
+ /// The second result.
+ /// The third result.
+ /// The fourth result.
+ /// The fifth result.
+ /// The sixth result.
+ /// The seventh result.
+ /// The continuation function to execute when all results are successful.
+ ///
+ /// The result returned by if all results are successful; otherwise, a failed result with all collected errors.
+ ///
+ public static Result Combine(
+ Result first,
+ Result second,
+ Result third,
+ Result fourth,
+ Result fifth,
+ Result sixth,
+ Result seventh,
+ Func> bind)
+ {
+ ArgumentNullException.ThrowIfNull(bind);
+
+ return Combine(first, second, third, fourth, fifth, sixth, seventh)
+ .Bind(values => bind(values.Item1, values.Item2, values.Item3, values.Item4, values.Item5, values.Item6, values.Item7));
+ }
+
+ private static Result CombineCore(Func combine, Func> bind)
+ {
+ var combinedResult = combine();
+ if (combinedResult.IsFailure)
+ {
+ return Result.Failure(combinedResult.Errors);
+ }
+
+ return bind();
+ }
+}
diff --git a/src/PANiXiDA.Core.ResultPattern/ResultExtensions.cs b/src/PANiXiDA.Core.ResultPattern/ResultExtensions.cs
new file mode 100644
index 0000000..ad7986e
--- /dev/null
+++ b/src/PANiXiDA.Core.ResultPattern/ResultExtensions.cs
@@ -0,0 +1,250 @@
+namespace PANiXiDA.Core.ResultPattern;
+
+///
+/// Provides helper methods for composing and transforming and .
+///
+public static class ResultExtensions
+{
+ ///
+ /// Transforms a successful non-generic result into a successful generic result.
+ ///
+ /// The type of the resulting value.
+ /// The source result.
+ /// The mapping function to execute on success.
+ ///
+ /// A successful with the transformed value, or a failed result with the source result errors preserved.
+ ///
+ public static Result Map(this Result result, Func map)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ ArgumentNullException.ThrowIfNull(map);
+
+ if (result.IsFailure)
+ {
+ return Result.Failure(result.Errors);
+ }
+
+ return Result.Success(map());
+ }
+
+ ///
+ /// Transforms a successful generic result into another successful generic result.
+ ///
+ /// The type of the source value.
+ /// The type of the resulting value.
+ /// The source result.
+ /// The mapping function to execute on success.
+ ///
+ /// A successful with the transformed value, or a failed result with the source result errors preserved.
+ ///
+ public static Result Map(this Result result, Func map)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ ArgumentNullException.ThrowIfNull(map);
+
+ if (result.IsFailure)
+ {
+ return Result.Failure(result.Errors);
+ }
+
+ return Result.Success(map(result.Value));
+ }
+
+ ///
+ /// Continues a successful generic result with a function that returns a non-generic result.
+ ///
+ /// The type of the source value.
+ /// The source result.
+ /// The continuation function to execute on success.
+ ///
+ /// The result returned by if the source result is successful; otherwise, a failed result with the source result errors preserved.
+ ///
+ public static Result Bind(this Result result, Func bind)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ ArgumentNullException.ThrowIfNull(bind);
+
+ if (result.IsFailure)
+ {
+ return Result.Failure(result.Errors);
+ }
+
+ return bind(result.Value);
+ }
+
+ ///
+ /// Continues a successful generic result with a function that returns another generic result.
+ ///
+ /// The type of the source value.
+ /// The type of the continuation result value.
+ /// The source result.
+ /// The continuation function to execute on success.
+ ///
+ /// The result returned by if the source result is successful; otherwise, a failed result with the source result errors preserved.
+ ///
+ public static Result Bind(this Result result, Func> bind)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ ArgumentNullException.ThrowIfNull(bind);
+
+ if (result.IsFailure)
+ {
+ return Result.Failure(result.Errors);
+ }
+
+ return bind(result.Value);
+ }
+
+ ///
+ /// Continues a successful non-generic result with a function that returns a generic result.
+ ///
+ /// The type of the continuation result value.
+ /// The source result.
+ /// The continuation function to execute on success.
+ ///
+ /// The result returned by if the source result is successful; otherwise, a failed result with the source result errors preserved.
+ ///
+ public static Result Bind(this Result result, Func> bind)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ ArgumentNullException.ThrowIfNull(bind);
+
+ if (result.IsFailure)
+ {
+ return Result.Failure(result.Errors);
+ }
+
+ return bind();
+ }
+
+ ///
+ /// Asynchronously continues a successful generic result with a function that returns a generic result.
+ ///
+ /// The type of the source value.
+ /// The type of the continuation result value.
+ /// The source result.
+ /// The asynchronous continuation function to execute on success.
+ ///
+ /// A task returning the result of if the source result is successful; otherwise, a failed result with the source result errors preserved.
+ ///
+ public static Task> BindAsync(this Result result, Func>> bind)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ ArgumentNullException.ThrowIfNull(bind);
+
+ if (result.IsFailure)
+ {
+ return Task.FromResult(Result.Failure(result.Errors));
+ }
+
+ return bind(result.Value);
+ }
+
+ ///
+ /// Asynchronously continues a successful non-generic result with a function that returns a generic result.
+ ///
+ /// The type of the continuation result value.
+ /// The source result.
+ /// The asynchronous continuation function to execute on success.
+ ///
+ /// A task returning the result of if the source result is successful; otherwise, a failed result with the source result errors preserved.
+ ///
+ public static Task> BindAsync(this Result result, Func>> bind)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ ArgumentNullException.ThrowIfNull(bind);
+
+ if (result.IsFailure)
+ {
+ return Task.FromResult(Result.Failure(result.Errors));
+ }
+
+ return bind();
+ }
+
+ ///
+ /// Validates a successful generic result with an additional predicate.
+ ///
+ /// The type of the source value.
+ /// The source result.
+ /// The predicate that must return for the result to remain successful.
+ /// The error returned when the predicate fails.
+ ///
+ /// The source result if it is already failed or if the predicate succeeds; otherwise, a failed result with the error.
+ ///
+ public static Result Ensure(this Result result, Func predicate, Error error)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ ArgumentNullException.ThrowIfNull(predicate);
+ ArgumentNullException.ThrowIfNull(error);
+
+ if (result.IsFailure)
+ {
+ return result;
+ }
+
+ return predicate(result.Value)
+ ? result
+ : Result.Failure(error);
+ }
+
+ ///
+ /// Executes a side effect for a successful generic result and returns the original result.
+ ///
+ /// The type of the source value.
+ /// The source result.
+ /// The side effect to execute on success.
+ /// The source result.
+ public static Result Tap(this Result result, Action action)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ ArgumentNullException.ThrowIfNull(action);
+
+ if (result.IsFailure)
+ {
+ return result;
+ }
+
+ action(result.Value);
+ return result;
+ }
+
+ ///
+ /// Converts a non-generic result into a final value by handling both success and failure.
+ ///
+ /// The type of the final value.
+ /// The source result.
+ /// The function to execute on success.
+ /// The function to execute on failure.
+ /// The value returned by the matching branch.
+ public static TOut Match(this Result result, Func onSuccess, Func, TOut> onFailure)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ ArgumentNullException.ThrowIfNull(onSuccess);
+ ArgumentNullException.ThrowIfNull(onFailure);
+
+ return result.IsSuccess
+ ? onSuccess()
+ : onFailure(result.Errors);
+ }
+
+ ///
+ /// Converts a generic result into a final value by handling both success and failure.
+ ///
+ /// The type of the source value.
+ /// The type of the final value.
+ /// The source result.
+ /// The function to execute on success.
+ /// The function to execute on failure.
+ /// The value returned by the matching branch.
+ public static TOut Match(this Result result, Func onSuccess, Func, TOut> onFailure)
+ {
+ ArgumentNullException.ThrowIfNull(result);
+ ArgumentNullException.ThrowIfNull(onSuccess);
+ ArgumentNullException.ThrowIfNull(onFailure);
+
+ return result.IsSuccess
+ ? onSuccess(result.Value)
+ : onFailure(result.Errors);
+ }
+}
diff --git a/tests/PANiXiDA.Core.ResultPattern.UnitTests/ErrorTests.cs b/tests/PANiXiDA.Core.ResultPattern.UnitTests/ErrorTests.cs
new file mode 100644
index 0000000..40c7ffe
--- /dev/null
+++ b/tests/PANiXiDA.Core.ResultPattern.UnitTests/ErrorTests.cs
@@ -0,0 +1,290 @@
+namespace PANiXiDA.Core.ResultPattern.UnitTests;
+
+public sealed class ErrorTests
+{
+ [Fact(DisplayName = "Error ctor → sets properties when valid message, type, and metadata are provided")]
+ public void Constructor_Should_SetProperties_When_MessageTypeAndMetadataAreValid()
+ {
+ const string message = "Validation failed";
+ const string metadataKey = "code";
+ const int metadataValue = 42;
+ var metadata = new Dictionary
+ {
+ [metadataKey] = metadataValue
+ };
+
+ var error = new Error(message, ErrorType.Validation, metadata);
+
+ error.Message.Should().Be(message);
+ error.Type.Should().Be(ErrorType.Validation);
+ error.Metadata.Should().ContainSingle()
+ .Which.Should().Be(new KeyValuePair(metadataKey, metadataValue));
+ error.Metadata.Should().NotBeSameAs(metadata);
+ }
+
+ [Fact(DisplayName = "Error ctor → creates a defensive metadata copy when metadata is provided")]
+ public void Constructor_Should_CreateDefensiveMetadataCopy_When_MetadataIsProvided()
+ {
+ const string metadataKey = "code";
+ const int originalMetadataValue = 42;
+ const int changedMetadataValue = 100;
+ var metadata = new Dictionary
+ {
+ [metadataKey] = originalMetadataValue
+ };
+
+ var error = new Error("Validation failed", ErrorType.Validation, metadata);
+
+ metadata[metadataKey] = changedMetadataValue;
+ metadata["extra"] = true;
+
+ error.Metadata.Should().ContainSingle()
+ .Which.Should().Be(new KeyValuePair(metadataKey, originalMetadataValue));
+ }
+
+ [Fact(DisplayName = "Error metadata → throws NotSupportedException when metadata is cast to a mutable dictionary and modified")]
+ public void Metadata_Should_ThrowNotSupportedException_When_CastToMutableDictionaryAndModified()
+ {
+ var error = Error.Validation("Validation failed")
+ .WithMetadata("code", 42);
+ var metadata = error.Metadata.Should()
+ .BeAssignableTo>()
+ .Subject;
+
+ Action act = () => metadata["code"] = 100;
+
+ act.Should().Throw();
+ }
+
+ [Fact(DisplayName = "Error ctor → creates empty metadata when metadata is not provided")]
+ public void Constructor_Should_InitializeEmptyMetadata_When_MetadataIsNull()
+ {
+ const string message = "Validation failed";
+
+ var error = new Error(message, ErrorType.Validation);
+
+ error.Metadata.Should().BeEmpty();
+ }
+
+ [Fact(DisplayName = "Error copy → returns an equivalent new error when copied with record syntax")]
+ public void Copy_Should_ReturnEquivalentNewError_When_RecordWithExpressionIsUsed()
+ {
+ var error = Error.Validation("Validation failed")
+ .WithField("email");
+
+ var copiedError = error with { };
+
+ copiedError.Should().NotBeSameAs(error);
+ copiedError.Should().Be(error);
+ }
+
+ [Fact(DisplayName = "Error ctor → throws ArgumentException when message is null")]
+ public void Constructor_Should_ThrowArgumentException_When_MessageIsNull()
+ {
+ Action act = static () => _ = new Error(null!, ErrorType.Validation);
+
+ var exception = act.Should().Throw().Which;
+
+ exception.ParamName.Should().Be("message");
+ }
+
+ [Fact(DisplayName = "Error ctor → throws ArgumentException when message is empty")]
+ public void Constructor_Should_ThrowArgumentException_When_MessageIsEmpty()
+ {
+ Action act = () => _ = new Error(string.Empty, ErrorType.Validation);
+
+ var exception = act.Should().Throw().Which;
+
+ exception.ParamName.Should().Be("message");
+ }
+
+ [Fact(DisplayName = "Error ctor → throws ArgumentException when message contains only whitespace")]
+ public void Constructor_Should_ThrowArgumentException_When_MessageIsWhitespace()
+ {
+ Action act = () => _ = new Error(" ", ErrorType.Validation);
+
+ var exception = act.Should().Throw().Which;
+
+ exception.ParamName.Should().Be("message");
+ }
+
+ [Fact(DisplayName = "Validation → returns a Validation error when a message is provided")]
+ public void Validation_Should_ReturnValidationError_When_MessageIsProvided()
+ {
+ const string message = "Validation failed";
+
+ var error = Error.Validation(message);
+
+ AssertError(error, message, ErrorType.Validation);
+ }
+
+ [Fact(DisplayName = "NotFound → returns a NotFound error when a message is provided")]
+ public void NotFound_Should_ReturnNotFoundError_When_MessageIsProvided()
+ {
+ const string message = "Entity not found";
+
+ var error = Error.NotFound(message);
+
+ AssertError(error, message, ErrorType.NotFound);
+ }
+
+ [Fact(DisplayName = "Conflict → returns a Conflict error when a message is provided")]
+ public void Conflict_Should_ReturnConflictError_When_MessageIsProvided()
+ {
+ const string message = "Entity already exists";
+
+ var error = Error.Conflict(message);
+
+ AssertError(error, message, ErrorType.Conflict);
+ }
+
+ [Fact(DisplayName = "Unauthorized → returns an Unauthorized error when a message is provided")]
+ public void Unauthorized_Should_ReturnUnauthorizedError_When_MessageIsProvided()
+ {
+ const string message = "Authentication required";
+
+ var error = Error.Unauthorized(message);
+
+ AssertError(error, message, ErrorType.Unauthorized);
+ }
+
+ [Fact(DisplayName = "Forbidden → returns a Forbidden error when a message is provided")]
+ public void Forbidden_Should_ReturnForbiddenError_When_MessageIsProvided()
+ {
+ const string message = "Access denied";
+
+ var error = Error.Forbidden(message);
+
+ AssertError(error, message, ErrorType.Forbidden);
+ }
+
+ [Fact(DisplayName = "Failure → returns a Failure error when a message is provided")]
+ public void Failure_Should_ReturnFailureError_When_MessageIsProvided()
+ {
+ const string message = "Operation failed";
+
+ var error = Error.Failure(message);
+
+ AssertError(error, message, ErrorType.Failure);
+ }
+
+ [Fact(DisplayName = "Unexpected → returns an Unexpected error when a message is provided")]
+ public void Unexpected_Should_ReturnUnexpectedError_When_MessageIsProvided()
+ {
+ const string message = "Unexpected error";
+
+ var error = Error.Unexpected(message);
+
+ AssertError(error, message, ErrorType.Unexpected);
+ }
+
+ [Fact(DisplayName = "WithMetadata → adds new metadata when the key does not exist")]
+ public void WithMetadata_Should_AddMetadata_When_KeyDoesNotExist()
+ {
+ const string message = "Validation failed";
+ var key = Error.FieldMetadataKey;
+ const string value = "name";
+ var error = Error.Validation(message);
+
+ var updatedError = error.WithMetadata(key, value);
+
+ updatedError.Metadata.Should().ContainSingle()
+ .Which.Should().Be(new KeyValuePair(key, value));
+ }
+
+ [Fact(DisplayName = "WithMetadata → overwrites metadata when the key already exists")]
+ public void WithMetadata_Should_OverrideMetadata_When_KeyAlreadyExists()
+ {
+ var key = Error.FieldMetadataKey;
+ const string message = "Validation failed";
+ const string originalValue = "old-name";
+ const string updatedValue = "new-name";
+ var error = new Error(
+ message,
+ ErrorType.Validation,
+ new Dictionary { [key] = originalValue });
+
+ var updatedError = error.WithMetadata(key, updatedValue);
+
+ updatedError.Metadata.Should().ContainSingle()
+ .Which.Should().Be(new KeyValuePair(key, updatedValue));
+ }
+
+ [Fact(DisplayName = "WithMetadata → returns a new error and does not modify the original when metadata is updated")]
+ public void WithMetadata_Should_ReturnNewErrorWithoutMutatingOriginal_When_MetadataIsUpdated()
+ {
+ var key = Error.FieldMetadataKey;
+ const string message = "Validation failed";
+ const string originalValue = "old-name";
+ const string metadataKey = "code";
+ const int metadataValue = 100;
+ var error = new Error(
+ message,
+ ErrorType.Validation,
+ new Dictionary { [key] = originalValue });
+
+ var updatedError = error.WithMetadata(metadataKey, metadataValue);
+
+ updatedError.Should().NotBeSameAs(error);
+
+ error.Metadata.Should().ContainSingle()
+ .Which.Should().Be(new KeyValuePair(key, originalValue));
+
+ updatedError.Metadata.Should().HaveCount(2);
+ updatedError.Metadata.Should().Contain(new KeyValuePair(key, originalValue));
+ updatedError.Metadata.Should().Contain(new KeyValuePair(metadataKey, metadataValue));
+ }
+
+ [Fact(DisplayName = "WithField → adds field metadata when a valid field name is provided")]
+ public void WithField_Should_AddFieldMetadata_When_FieldIsValid()
+ {
+ const string message = "Validation failed";
+ const string field = "name";
+ var error = Error.Validation(message);
+
+ var updatedError = error.WithField(field);
+
+ updatedError.Metadata.Should().ContainSingle()
+ .Which.Should().Be(new KeyValuePair(Error.FieldMetadataKey, field));
+ }
+
+ [Fact(DisplayName = "WithField → throws ArgumentException when field is null")]
+ public void WithField_Should_ThrowArgumentException_When_FieldIsNull()
+ {
+ var error = Error.Validation("Validation failed");
+ Action act = () => error.WithField(null!);
+
+ var exception = act.Should().Throw().Which;
+
+ exception.ParamName.Should().Be("field");
+ }
+
+ [Fact(DisplayName = "WithField → throws ArgumentException when field is empty")]
+ public void WithField_Should_ThrowArgumentException_When_FieldIsEmpty()
+ {
+ var error = Error.Validation("Validation failed");
+ Action act = () => error.WithField(string.Empty);
+
+ var exception = act.Should().Throw().Which;
+
+ exception.ParamName.Should().Be("field");
+ }
+
+ [Fact(DisplayName = "WithField → throws ArgumentException when field contains only whitespace")]
+ public void WithField_Should_ThrowArgumentException_When_FieldIsWhitespace()
+ {
+ var error = Error.Validation("Validation failed");
+ Action act = () => error.WithField(" ");
+
+ var exception = act.Should().Throw().Which;
+
+ exception.ParamName.Should().Be("field");
+ }
+
+ private static void AssertError(Error error, string expectedMessage, ErrorType expectedType)
+ {
+ error.Message.Should().Be(expectedMessage);
+ error.Type.Should().Be(expectedType);
+ error.Metadata.Should().BeEmpty();
+ }
+}
diff --git a/tests/PANiXiDA.Core.ResultPattern.UnitTests/GenericResultTests.cs b/tests/PANiXiDA.Core.ResultPattern.UnitTests/GenericResultTests.cs
new file mode 100644
index 0000000..26bef24
--- /dev/null
+++ b/tests/PANiXiDA.Core.ResultPattern.UnitTests/GenericResultTests.cs
@@ -0,0 +1,166 @@
+namespace PANiXiDA.Core.ResultPattern.UnitTests;
+
+public sealed class GenericResultTests
+{
+ [Fact(DisplayName = "Success → returns a successful generic result when a value is provided")]
+ public void Success_Should_ReturnSuccessfulGenericResult_When_ValueIsProvided()
+ {
+ const int value = 42;
+
+ var result = Result.Success(value);
+
+ result.IsSuccess.Should().BeTrue();
+ result.IsFailure.Should().BeFalse();
+ result.Errors.Should().BeEmpty();
+ result.Value.Should().Be(value);
+ }
+
+ [Fact(DisplayName = "Success → returns a successful generic result when null is provided")]
+ public void Success_Should_ReturnSuccessfulGenericResult_When_ValueIsNull()
+ {
+ string? value = null;
+
+ var result = Result.Success(value);
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().BeNull();
+ result.ValueOrDefault.Should().BeNull();
+ }
+
+ [Fact(DisplayName = "Value → returns the value when the result is successful")]
+ public void Value_Should_ReturnValue_When_ResultIsSuccess()
+ {
+ const string value = "value";
+
+ var result = Result.Success(value);
+
+ result.Value.Should().Be(value);
+ }
+
+ [Fact(DisplayName = "Value → throws InvalidOperationException when the result is a failure")]
+ public void Value_Should_ThrowInvalidOperationException_When_ResultIsFailure()
+ {
+ var result = Result.Failure(Error.Validation("Validation failed"));
+ Action act = () => _ = result.Value;
+
+ var exception = act.Should().Throw().Which;
+
+ exception.Message.Should().Be("Cannot access value of failed result.");
+ }
+
+ [Fact(DisplayName = "ValueOrDefault → returns the value when the result is successful")]
+ public void ValueOrDefault_Should_ReturnValue_When_ResultIsSuccess()
+ {
+ const string value = "value";
+
+ var result = Result.Success(value);
+
+ result.ValueOrDefault.Should().Be(value);
+ }
+
+ [Fact(DisplayName = "ValueOrDefault → returns default when the result is a failure")]
+ public void ValueOrDefault_Should_ReturnDefault_When_ResultIsFailure()
+ {
+ var result = Result.Failure(Error.Validation("Validation failed"));
+
+ var value = result.ValueOrDefault;
+
+ value.Should().BeNull();
+ }
+
+ [Fact(DisplayName = "TryGetValue → returns true and the value when the result is successful")]
+ public void TryGetValue_Should_ReturnTrueAndValue_When_ResultIsSuccess()
+ {
+ const string value = "value";
+ var result = Result.Success(value);
+
+ var success = result.TryGetValue(out var actualValue);
+
+ success.Should().BeTrue();
+ actualValue.Should().Be(value);
+ }
+
+ [Fact(DisplayName = "TryGetValue → returns false and default when the result is a failure")]
+ public void TryGetValue_Should_ReturnFalseAndDefault_When_ResultIsFailure()
+ {
+ var result = Result.Failure(Error.Validation("Validation failed"));
+
+ var success = result.TryGetValue(out var value);
+
+ success.Should().BeFalse();
+ value.Should().BeNull();
+ }
+
+ [Fact(DisplayName = "Failure(Error) → returns a failed generic result when an error is provided")]
+ public void Failure_Should_ReturnFailureGenericResult_When_ErrorIsProvided()
+ {
+ var error = Error.Validation("Validation failed");
+
+ var result = Result.Failure(error);
+
+ result.IsFailure.Should().BeTrue();
+ result.Errors.Should().ContainSingle().Which.Should().BeSameAs(error);
+ }
+
+ [Fact(DisplayName = "Failure(Error) → throws ArgumentNullException when error is null")]
+ public void Failure_Should_ThrowArgumentNullException_When_ErrorIsNull()
+ {
+ Action act = () => Result.Failure((Error)null!);
+
+ var exception = act.Should().Throw().Which;
+
+ exception.ParamName.Should().Be("error");
+ }
+
+ [Fact(DisplayName = "Failure(IEnumerable) → returns a failed generic result when the collection contains errors")]
+ public void Failure_Should_ReturnFailureGenericResult_When_ErrorCollectionContainsErrors()
+ {
+ var firstError = Error.Validation("Validation failed");
+ var secondError = Error.NotFound("Not found");
+
+ var result = Result.Failure([firstError, secondError]);
+
+ result.IsFailure.Should().BeTrue();
+ result.Errors.Should().Equal(firstError, secondError);
+ }
+
+ [Fact(DisplayName = "Failure(IEnumerable) → filters out null values when the collection contains null")]
+ public void Failure_Should_FilterOutNullErrors_When_ErrorCollectionContainsNullValues()
+ {
+ var error = Error.Validation("Validation failed");
+
+ var result = Result.Failure([error, null!]);
+
+ result.Errors.Should().ContainSingle().Which.Should().BeSameAs(error);
+ }
+
+ [Fact(DisplayName = "Failure(IEnumerable) → throws ArgumentNullException when the error collection is null")]
+ public void Failure_Should_ThrowArgumentNullException_When_ErrorCollectionIsNull()
+ {
+ Action act = () => Result.Failure((IEnumerable)null!);
+
+ var exception = act.Should().Throw().Which;
+
+ exception.ParamName.Should().Be("errors");
+ }
+
+ [Fact(DisplayName = "Failure(IEnumerable) → throws ArgumentException when the error collection is empty")]
+ public void Failure_Should_ThrowArgumentException_When_ErrorCollectionIsEmpty()
+ {
+ Action act = () => Result.Failure([]);
+
+ var exception = act.Should().Throw().Which;
+
+ exception.ParamName.Should().Be("errors");
+ }
+
+ [Fact(DisplayName = "Failure(IEnumerable) → throws ArgumentException when the collection contains only null")]
+ public void Failure_Should_ThrowArgumentException_When_ErrorCollectionContainsOnlyNullValues()
+ {
+ Action act = () => Result.Failure([null!]);
+
+ var exception = act.Should().Throw().Which;
+
+ exception.ParamName.Should().Be("errors");
+ }
+}
diff --git a/tests/PANiXiDA.Core.ResultPattern.UnitTests/PANiXiDA.Core.ResultPattern.UnitTests.csproj b/tests/PANiXiDA.Core.ResultPattern.UnitTests/PANiXiDA.Core.ResultPattern.UnitTests.csproj
new file mode 100644
index 0000000..b6ae9de
--- /dev/null
+++ b/tests/PANiXiDA.Core.ResultPattern.UnitTests/PANiXiDA.Core.ResultPattern.UnitTests.csproj
@@ -0,0 +1,21 @@
+
+
+ Exe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/PANiXiDA.Core.ResultPattern.UnitTests/ResultCombinerTests.cs b/tests/PANiXiDA.Core.ResultPattern.UnitTests/ResultCombinerTests.cs
new file mode 100644
index 0000000..7d8a796
--- /dev/null
+++ b/tests/PANiXiDA.Core.ResultPattern.UnitTests/ResultCombinerTests.cs
@@ -0,0 +1,346 @@
+namespace PANiXiDA.Core.ResultPattern.UnitTests;
+
+public sealed class ResultCombinerTests
+{
+ [Fact(DisplayName = "Combine(Result, Result) → returns a tuple result when both results are successful")]
+ public void TupleCombine2_Should_ReturnTuple_When_AllResultsAreSuccessful()
+ {
+ var first = Result.Success(1);
+ var second = Result.Success(2);
+
+ var result = ResultCombiner.Combine(first, second);
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().Be((1, 2));
+ }
+
+ [Fact(DisplayName = "Combine(Result, Result) → throws ArgumentNullException when the first result is null")]
+ public void TupleCombine2_Should_ThrowArgumentNullException_When_FirstIsNull()
+ {
+ Action act = () => ResultCombiner.Combine(
+ null!,
+ Result.Success(2));
+
+ var exception = act.Should().Throw().Which;
+
+ exception.ParamName.Should().Be("first");
+ }
+
+ [Fact(DisplayName = "Combine(Result, Result, Result) → returns a tuple result when all results are successful")]
+ public void TupleCombine3_Should_ReturnTuple_When_AllResultsAreSuccessful()
+ {
+ var first = Result.Success(1);
+ var second = Result.Success(2);
+ var third = Result.Success(3);
+
+ var result = ResultCombiner.Combine(first, second, third);
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().Be((1, 2, 3));
+ }
+
+ [Fact(DisplayName = "Combine(Result, Result, Result) → returns a failure with aggregated errors when the results contain errors")]
+ public void TupleCombine3_Should_ReturnFailure_When_AnyResultIsFailure()
+ {
+ var firstError = Error.Validation("first");
+ var secondError = Error.Conflict("second");
+
+ var first = Result.Failure(firstError);
+ var second = Result.Success(2);
+ var third = Result.Failure(secondError);
+
+ var result = ResultCombiner.Combine(first, second, third);
+
+ result.IsFailure.Should().BeTrue();
+ result.Errors.Should().Equal(firstError, secondError);
+ }
+
+ [Fact(DisplayName = "Combine(Result, Result, Result, Result) → returns a tuple result when all results are successful")]
+ public void TupleCombine4_Should_ReturnTuple_When_AllResultsAreSuccessful()
+ {
+ var first = Result.Success(1);
+ var second = Result.Success(2);
+ var third = Result.Success(3);
+ var fourth = Result.Success(4);
+
+ var result = ResultCombiner.Combine(first, second, third, fourth);
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().Be((1, 2, 3, 4));
+ }
+
+ [Fact(DisplayName = "Combine(Result, Result, Result, Result, Result) → returns a tuple result when all results are successful")]
+ public void TupleCombine5_Should_ReturnTuple_When_AllResultsAreSuccessful()
+ {
+ var first = Result.Success(1);
+ var second = Result.Success(2);
+ var third = Result.Success(3);
+ var fourth = Result.Success(4);
+ var fifth = Result.Success(5);
+
+ var result = ResultCombiner.Combine(first, second, third, fourth, fifth);
+
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().Be((1, 2, 3, 4, 5));
+ }
+
+ [Fact(DisplayName = "Combine(Result, Result, Result