From 1012decef074400d7d4518d03c08f65388357a92 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Mon, 30 Mar 2026 12:51:19 -0500
Subject: [PATCH] chore: initial commit
---
.editorconfig | 390 ++++++++++++++
.gitea/workflows/ci.yml | 23 +
.gitea/workflows/release.yml | 122 +++++
.gitignore | 482 ++++++++++++++++++
.vscode/settings.json | 9 +
Directory.Build.props | 12 +
LICENSE.md | 19 +
README.md | 284 +++++++++++
StevanFreeborn.Results.slnx | 8 +
global.json | 9 +
src/Directory.Build.props | 6 +
src/Directory.Packages.props | 8 +
src/StevanFreeborn.Results/.editorconfig | 6 +
src/StevanFreeborn.Results/Error.cs | 40 ++
src/StevanFreeborn.Results/IError.cs | 20 +
src/StevanFreeborn.Results/Result.cs | 264 ++++++++++
.../ResultAsyncExtensions.cs | 290 +++++++++++
.../StevanFreeborn.Results.csproj | 29 ++
src/StevanFreeborn.Results/Unit.cs | 11 +
tests/.editorconfig | 6 +
tests/Directory.Packages.props | 8 +
.../ErrorTests.cs | 57 +++
.../ResultAsyncExtensionsTests.cs | 174 +++++++
.../ResultTTests.cs | 183 +++++++
.../ResultTests.cs | 146 ++++++
.../StevanFreeborn.Results.Tests.csproj | 13 +
26 files changed, 2619 insertions(+)
create mode 100644 .editorconfig
create mode 100644 .gitea/workflows/ci.yml
create mode 100644 .gitea/workflows/release.yml
create mode 100644 .gitignore
create mode 100644 .vscode/settings.json
create mode 100644 Directory.Build.props
create mode 100644 LICENSE.md
create mode 100644 README.md
create mode 100644 StevanFreeborn.Results.slnx
create mode 100644 global.json
create mode 100644 src/Directory.Build.props
create mode 100644 src/Directory.Packages.props
create mode 100644 src/StevanFreeborn.Results/.editorconfig
create mode 100644 src/StevanFreeborn.Results/Error.cs
create mode 100644 src/StevanFreeborn.Results/IError.cs
create mode 100644 src/StevanFreeborn.Results/Result.cs
create mode 100644 src/StevanFreeborn.Results/ResultAsyncExtensions.cs
create mode 100644 src/StevanFreeborn.Results/StevanFreeborn.Results.csproj
create mode 100644 src/StevanFreeborn.Results/Unit.cs
create mode 100644 tests/.editorconfig
create mode 100644 tests/Directory.Packages.props
create mode 100644 tests/StevanFreeborn.Results.Tests/ErrorTests.cs
create mode 100644 tests/StevanFreeborn.Results.Tests/ResultAsyncExtensionsTests.cs
create mode 100644 tests/StevanFreeborn.Results.Tests/ResultTTests.cs
create mode 100644 tests/StevanFreeborn.Results.Tests/ResultTests.cs
create mode 100644 tests/StevanFreeborn.Results.Tests/StevanFreeborn.Results.Tests.csproj
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..c265c6f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,390 @@
+root = true
+
+# All files
+[*]
+indent_style = space
+indent_size = 2
+
+# Xml files
+[*.xml]
+indent_size = 2
+
+# Xml project files
+[*.{csproj,fsproj,vbproj,proj,slnx}]
+indent_size = 2
+
+# Xml config files
+[*.{props,targets,config,nuspec}]
+indent_size = 2
+
+[*.json]
+indent_size = 2
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 2
+tab_width = 2
+
+# New line preferences
+insert_final_newline = false
+
+#### .NET Coding Conventions ####
+[*.{cs,vb}]
+
+# Organize usings
+dotnet_separate_import_directive_groups = true
+dotnet_sort_system_directives_first = true
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false:silent
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_property = false:silent
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_namespace_match_folder = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = false:silent
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+
+# Field preferences
+dotnet_style_readonly_field = true:warning
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all:suggestion
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+#### C# Coding Conventions ####
+[*.cs]
+
+# var preferences
+csharp_style_var_elsewhere = true:suggestion
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_lambdas = true:suggestion
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_extended_property_pattern = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_pattern_matching = true:silent
+csharp_style_prefer_switch_expression = true:suggestion
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Modifier preferences
+csharp_prefer_static_anonymous_function = true:suggestion
+csharp_prefer_static_local_function = true:warning
+csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion
+csharp_style_prefer_readonly_struct = true:suggestion
+csharp_style_prefer_readonly_struct_member = true:suggestion
+
+# Code-block preferences
+csharp_prefer_braces = true:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_namespace_declarations = file_scoped:suggestion
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_style_prefer_top_level_statements = true:silent
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace:silent
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#### Naming styles ####
+[*.{cs,vb}]
+
+# Naming rules
+
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
+dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
+dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
+
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
+
+dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
+dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
+dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.events_should_be_pascalcase.symbols = events
+dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
+dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
+dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
+dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
+dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
+dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
+dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
+
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase
+
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
+dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
+dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
+
+# Symbol specifications
+
+dotnet_naming_symbols.interfaces.applicable_kinds = interface
+dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interfaces.required_modifiers =
+
+dotnet_naming_symbols.enums.applicable_kinds = enum
+dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.enums.required_modifiers =
+
+dotnet_naming_symbols.events.applicable_kinds = event
+dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.events.required_modifiers =
+
+dotnet_naming_symbols.methods.applicable_kinds = method
+dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.methods.required_modifiers =
+
+dotnet_naming_symbols.properties.applicable_kinds = property
+dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.properties.required_modifiers =
+
+dotnet_naming_symbols.public_fields.applicable_kinds = field
+dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_fields.required_modifiers =
+
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_fields.required_modifiers =
+
+dotnet_naming_symbols.private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_static_fields.required_modifiers = static
+
+dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
+dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types_and_namespaces.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 =
+
+dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
+dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
+dotnet_naming_symbols.type_parameters.required_modifiers =
+
+dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
+dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_constant_fields.required_modifiers = const
+
+dotnet_naming_symbols.local_variables.applicable_kinds = local
+dotnet_naming_symbols.local_variables.applicable_accessibilities = local
+dotnet_naming_symbols.local_variables.required_modifiers =
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.applicable_accessibilities = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+
+dotnet_naming_symbols.parameters.applicable_kinds = parameter
+dotnet_naming_symbols.parameters.applicable_accessibilities = *
+dotnet_naming_symbols.parameters.required_modifiers =
+
+dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
+dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_constant_fields.required_modifiers = const
+
+dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
+
+dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
+
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+dotnet_naming_symbols.local_functions.applicable_accessibilities = *
+dotnet_naming_symbols.local_functions.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.pascalcase.required_prefix =
+dotnet_naming_style.pascalcase.required_suffix =
+dotnet_naming_style.pascalcase.word_separator =
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+
+dotnet_naming_style.ipascalcase.required_prefix = I
+dotnet_naming_style.ipascalcase.required_suffix =
+dotnet_naming_style.ipascalcase.word_separator =
+dotnet_naming_style.ipascalcase.capitalization = pascal_case
+
+dotnet_naming_style.tpascalcase.required_prefix = T
+dotnet_naming_style.tpascalcase.required_suffix =
+dotnet_naming_style.tpascalcase.word_separator =
+dotnet_naming_style.tpascalcase.capitalization = pascal_case
+
+dotnet_naming_style._camelcase.required_prefix = _
+dotnet_naming_style._camelcase.required_suffix =
+dotnet_naming_style._camelcase.word_separator =
+dotnet_naming_style._camelcase.capitalization = camel_case
+
+dotnet_naming_style.camelcase.required_prefix =
+dotnet_naming_style.camelcase.required_suffix =
+dotnet_naming_style.camelcase.word_separator =
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_style.s_camelcase.required_prefix = s_
+dotnet_naming_style.s_camelcase.required_suffix =
+dotnet_naming_style.s_camelcase.word_separator =
+dotnet_naming_style.s_camelcase.capitalization = camel_case
+
diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
new file mode 100644
index 0000000..a6b2c1a
--- /dev/null
+++ b/.gitea/workflows/ci.yml
@@ -0,0 +1,23 @@
+name: CI
+on:
+ pull_request:
+ branches: [main]
+jobs:
+ ci:
+ name: Build, Format, and Test
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Build
+ run: dotnet build --no-restore
+ - name: Check formatting
+ run: dotnet format --verify-no-changes
+ - name: Run tests
+ run: dotnet test --no-build --verbosity normal
diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml
new file mode 100644
index 0000000..f3f7f5d
--- /dev/null
+++ b/.gitea/workflows/release.yml
@@ -0,0 +1,122 @@
+name: Release
+on:
+ push:
+ branches: [main]
+jobs:
+ release:
+ name: Version, Build, and Release
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+ - name: Determine version
+ id: version
+ run: |
+ $commits = git log main..HEAD --format="%s"
+ $commitList = $commits -split "`n"
+ $major = 0
+ $minor = 0
+ $patch = 0
+
+ foreach ($message in $commitList) {
+ if ($message -match " BREAKING CHANGE|!:") {
+ $major = 1
+ } elseif ($message -match "^feat(\(.*\))?:") {
+ $minor = 1
+ } elseif ($message -match "^fix(\(.*\))?:") {
+ $patch = 1
+ }
+ }
+
+ if ($major -eq 1) {
+ $minor = 0
+ $patch = 0
+ } elseif ($minor -eq 1) {
+ $patch = 0
+ } elseif ($patch -eq 0 -and $commitList.Count -gt 0 -and $commitList[0]) {
+ $patch = 1
+ }
+
+ $currentVersion = "0.0.0"
+ $tag = git describe --tags --abbrev=0 2>$null
+
+ $hasExistingTag = $false
+ if ($tag) {
+ $hasExistingTag = $true
+ $currentVersion = $tag -replace "^v", ""
+ }
+
+ $parts = $currentVersion.Split(".")
+ $majorCurrent = [int]$parts[0]
+ $minorCurrent = [int]$parts[1]
+ $patchCurrent = [int]$parts[2]
+
+ $majorNew = $majorCurrent + $major
+ $minorNew = if ($major -eq 1) { 0 } else { $minorCurrent + $minor }
+ $patchNew = if ($major -eq 1 -or $minor -eq 1) { 0 } else { $patchCurrent + $patch }
+
+ if ($majorNew -eq $majorCurrent -and $minorNew -eq $minorCurrent -and $patchNew -eq $patchCurrent) {
+ if (-not $hasExistingTag) {
+ $version = "0.0.0"
+ echo "VERSION=$version" >> $env:GITHUB_OUTPUT
+ Write-Host "First release: publishing version 0.0.0"
+ } else {
+ Write-Host "No version bump needed, skipping release"
+ exit 0
+ }
+ } else {
+ $version = "$majorNew.$minorNew.$patchNew"
+ echo "VERSION=$version" >> $env:GITHUB_OUTPUT
+ }
+ shell: pwsh
+ - name: Update version in csproj
+ if: steps.version.outputs.VERSION
+ run: |
+ $version = "${{ steps.version.outputs.VERSION }}"
+ $csprojPath = "src/StevanFreeborn.Results/StevanFreeborn.Results.csproj"
+ $content = Get-Content $csprojPath -Raw
+
+ if ($content -match ".*?") {
+ $content = $content -replace ".*?", "$version"
+ } elseif ($content -match "") {
+ $content = $content -replace "", "`n $version"
+ }
+
+ Set-Content -Path $csprojPath -Value $content
+ - name: Create commit
+ if: steps.version.outputs.VERSION
+ run: |
+ git config user.name "gitea-actions[bot]"
+ git config user.email "gitea-actions[bot]@users.noreply.gitea.io"
+ git add -A
+ git commit -m "Bump version to ${{ steps.version.outputs.VERSION }}" || echo "No changes to commit"
+ - name: Create tag
+ if: steps.version.outputs.VERSION
+ run: |
+ git tag "v${{ steps.version.outputs.VERSION }}"
+ - name: Push changes and tags
+ if: steps.version.outputs.VERSION
+ run: |
+ git push origin main
+ git push origin v${{ steps.version.outputs.VERSION }}
+ - name: Restore dependencies
+ if: steps.version.outputs.VERSION
+ run: dotnet restore
+ - name: Build
+ if: steps.version.outputs.VERSION
+ run: dotnet build --configuration Release --no-restore
+ - name: Run tests
+ if: steps.version.outputs.VERSION
+ run: dotnet test --configuration Release --no-build --verbosity normal
+ - name: Pack
+ if: steps.version.outputs.VERSION
+ run: dotnet pack --configuration Release --no-build --output ./artifacts /p:PackageVersion=${{ steps.version.outputs.VERSION }}
+ - name: Push to NuGet
+ if: steps.version.outputs.VERSION
+ run: dotnet nuget push ./artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0808c4a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,482 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from `dotnet new gitignore`
+
+# dotenv files
+.env
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+# but not Directory.Build.rsp, as it configures directory-level build defaults
+!Directory.Build.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+.idea/
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Vim temporary swap files
+*.swp
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..03157de
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,9 @@
+{
+ "cSpell.words": [
+ "accessibilities",
+ "ipascalcase",
+ "pascalcase",
+ "slnx",
+ "tpascalcase"
+ ]
+}
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..1ade24a
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,12 @@
+
+
+ enable
+ enable
+ latest
+ latest
+ All
+ true
+ true
+ true
+
+
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..307cc61
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,19 @@
+# Copyright (c) 2026 Stevan Freeborn
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3151208
--- /dev/null
+++ b/README.md
@@ -0,0 +1,284 @@
+# StevanFreeborn.Results
+
+[](https://www.nuget.org/packages/StevanFreeborn.Results)
+[](https://www.nuget.org/packages/StevanFreeborn.Results)
+[](https://github.com/StevanFreeborn/stevanfreeborn.results/actions/workflows/ci.yaml)
+[](https://opensource.org/licenses/MIT)
+
+A minimalistic, AOT-compatible Result type library with support for custom error types, for railway-oriented programming and functional error handling in .NET.
+
+## Features
+
+- **Railway-Oriented Programming**: Chain operations that can fail without nested try-catch blocks
+- **Custom Error Types**: Define your own error types that implement `IError`
+- **AOT Compatible**: Works with Native AOT and trimming
+- **Nullable Reference Types**: Full support for nullable reference types
+- **No External Dependencies**: Lightweight with zero dependencies
+- **Comprehensive XML Documentation**: Full IntelliSense support
+
+## Installation
+
+Install via NuGet:
+
+```bash
+dotnet add package StevanFreeborn.Results
+```
+
+## Quick Start
+
+```csharp
+using StevanFreeborn.Results;
+
+// Create a successful result
+Result ok = Result.Ok(default);
+
+// Create a failed result
+Result fail = Result.Fail(new Error("NotFound", "User not found"));
+
+// Work with results that have values
+Result divisionResult = Divide(10, 2);
+
+if (divisionResult.IsSuccess)
+{
+ Console.WriteLine(divisionResult.Value); // 5
+}
+
+// Railway-oriented programming with Bind
+Result GetUser(int id) => ...;
+Result GetOrder(int orderId) => ...;
+
+// Chain operations without exceptions
+Result GetUserOrder(int userId, int orderId)
+{
+ return GetUser(userId)
+ .Bind(user => GetOrder(user.OrderId))
+ .Map(order => order.WithUserDetails(user));
+}
+```
+
+## Custom Error Types
+
+One of the key features is the ability to define your own error types:
+
+```csharp
+// Define a custom error
+public record DomainError(string Code, string Message) : IError;
+
+// Use with Result
+Result GetUser(int id)
+{
+ if (id <= 0)
+ {
+ return Result.Fail(new DomainError("InvalidId", "User ID must be positive"));
+ }
+
+ // ... fetch user
+ return Result.Ok(user);
+}
+
+// Chain with custom errors
+Result result = GetUser(1)
+ .Bind(user => GetOrder(user.OrderId))
+ .MapError(e => new DomainError(e.Code, $"Failed to get order: {e.Message}"));
+```
+
+## IError Interface
+
+The `IError` interface is the foundation for custom error types:
+
+```csharp
+public interface IError
+{
+ string Code { get; }
+ string Message { get; }
+}
+```
+
+Any type implementing `IError` can be used with `Result`.
+
+## Built-in Error Class
+
+The library includes a built-in `Error` class that implements `IError`:
+
+```csharp
+// Create an error
+var error = new Error("NotFound", "User not found");
+
+// With metadata
+var errorWithMetadata = new Error(
+ "ValidationFailed", "Invalid input",
+ new Dictionary { { "Field", "email" } }
+);
+```
+
+## Unit Type
+
+The `Unit` struct represents a void-like type for use when no value is needed:
+
+```csharp
+// For operations that only succeed or fail without a value
+Result operation = DoSomething();
+```
+
+## Result Types
+
+### `Result`
+
+The main Result type with generic type parameters for both value and error:
+
+```csharp
+// Creation
+Result ok = Result.Ok(42);
+Result fail = Result.Fail(new Error("Invalid", "Invalid input"));
+
+// Check status
+if (result.IsSuccess) { /* ... */ }
+if (result.IsFailure) { /* ... */ }
+
+// Access value (throws on failure)
+int value = result.Value;
+
+// Access error (throws on success)
+Error error = result.Error;
+```
+
+### Result without value
+
+For operations that don't return a value, use `Unit`:
+
+```csharp
+Result ok = Result.Ok(default);
+Result fail = Result.Fail(new Error("Failed", "Something went wrong"));
+```
+
+## Functional Operations
+
+### Map
+
+Transforms the value if success, propagates the error if failure.
+
+```csharp
+Result ok = Result.Ok(5);
+Result mapped = ok.Map(x => x.ToString()); // Result.Ok("5")
+```
+
+### MapError
+
+Transforms the error if failure, propagates the value if success.
+
+```csharp
+Result fail = Result.Fail(new Error("NotFound", "Not found"));
+Result mapped = fail.MapError(e => new Error("Unexpected", e.Message));
+```
+
+### Bind
+
+Chains another operation that returns a Result.
+
+```csharp
+Result GetUser(int id) => ...;
+Result GetOrder(int userId) => ...;
+
+Result GetUserOrder(int userId)
+{
+ return GetUser(userId)
+ .Bind(user => GetOrder(user.Id));
+}
+```
+
+### Match
+
+Executes different functions based on success or failure.
+
+```csharp
+Result result = Divide(10, 2);
+
+// Get a value
+string message = result.Match(
+ onSuccess: value => $"Result: {value}",
+ onFailure: error => $"Error: {error.Message}"
+);
+
+// Execute actions
+result.Match(
+ onSuccess: value => Console.WriteLine(value),
+ onFailure: error => Console.WriteLine(error.Message)
+);
+```
+
+### Try
+
+Wraps a function that may throw an exception in a Result.
+
+```csharp
+// Simple usage with default error handler
+Result result = Result.Try(() => File.ReadAllText("file.txt"));
+
+// Custom error handler
+Result result = Result.Try(
+ () => File.ReadAllText("file.txt"),
+ ex => new Error("ReadError", ex.Message)
+);
+```
+
+## Async Operations
+
+The library provides async extension methods for all functional operations.
+
+```csharp
+// Async Map
+Result user = await GetUserAsync(id);
+Result userName = await user.MapAsync(u => GetNameAsync(u));
+
+// Async Bind
+Result order = await GetUserAsync(id)
+ .BindAsync(user => GetOrderAsync(user.OrderId));
+
+// Async Match
+string result = await result.MatchAsync(
+ onSuccess: async value => await ProcessAsync(value),
+ onFailure: async error => await HandleErrorAsync(error)
+);
+
+// Async Try
+Result result = await ResultAsyncExtensions.TryAsync(
+ () => HttpClient.GetStringAsync("https://api.example.com")
+);
+```
+
+## Extension Methods
+
+### Result Methods
+
+| Method | Description |
+|--------------------------------------------------|------------------------------------|
+| `Map(Func)` | Transforms the value |
+| `Map(Func, Func)` | Transforms the value or error |
+| `MapError(Func)` | Transforms the error |
+| `Bind(Func>)` | Chains a new Result |
+| `Match(Func, Func)` | Pattern matching returning a value |
+| `Match(Action, Action)` | Pattern matching executing actions |
+| `Try(Func)` | Wraps a function that may throw |
+
+### Async Extensions
+
+| Method | Description |
+|-----------------|---------------------------|
+| `MapAsync` | Async version of Map |
+| `MapErrorAsync` | Async version of MapError |
+| `BindAsync` | Async version of Bind |
+| `MatchAsync` | Async version of Match |
+| `TryAsync` | Async version of Try |
+
+## Requirements
+
+- .NET Standard 2.1
+- .NET 10.0+
+
+## License
+
+MIT License - see [LICENSE.md](LICENSE.md) for details.
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request.
diff --git a/StevanFreeborn.Results.slnx b/StevanFreeborn.Results.slnx
new file mode 100644
index 0000000..b8a3a29
--- /dev/null
+++ b/StevanFreeborn.Results.slnx
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..ebc8585
--- /dev/null
+++ b/global.json
@@ -0,0 +1,9 @@
+{
+ "sdk": {
+ "version": "10.0.201",
+ "rollForward": "latestPatch"
+ },
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}
\ No newline at end of file
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 0000000..6d18a9d
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
new file mode 100644
index 0000000..727d96f
--- /dev/null
+++ b/src/Directory.Packages.props
@@ -0,0 +1,8 @@
+
+
+ true
+
+
+
+
+
diff --git a/src/StevanFreeborn.Results/.editorconfig b/src/StevanFreeborn.Results/.editorconfig
new file mode 100644
index 0000000..f5eafdf
--- /dev/null
+++ b/src/StevanFreeborn.Results/.editorconfig
@@ -0,0 +1,6 @@
+[*.cs]
+
+dotnet_diagnostic.CA1000.severity = none
+dotnet_diagnostic.CA1031.severity = none
+dotnet_diagnostic.CA1510.severity = none
+dotnet_diagnostic.CA2225.severity = none
\ No newline at end of file
diff --git a/src/StevanFreeborn.Results/Error.cs b/src/StevanFreeborn.Results/Error.cs
new file mode 100644
index 0000000..e47b974
--- /dev/null
+++ b/src/StevanFreeborn.Results/Error.cs
@@ -0,0 +1,40 @@
+namespace StevanFreeborn.Results
+{
+
+ ///
+ /// Represents an error with a code, message, and optional metadata.
+ ///
+#pragma warning disable CA1716 // Identifiers should not match keywords
+ public sealed record Error : IError
+#pragma warning restore CA1716 // Identifiers should not match keywords
+ {
+ ///
+ /// Gets the error code.
+ ///
+ public string Code { get; }
+
+ ///
+ /// Gets the error message.
+ ///
+ public string Message { get; }
+
+ ///
+ /// Gets optional metadata associated with the error.
+ ///
+ public IReadOnlyDictionary? Metadata { get; }
+
+ ///
+ /// Initializes a new instance of the class with the specified code, message, and optional metadata.
+ ///
+ /// The error code.
+ /// The error message.
+ /// Optional metadata associated with the error.
+ public Error(string code, string message, IReadOnlyDictionary? metadata = null)
+ {
+ Code = code;
+ Message = message;
+ Metadata = metadata;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Results/IError.cs b/src/StevanFreeborn.Results/IError.cs
new file mode 100644
index 0000000..efd9e15
--- /dev/null
+++ b/src/StevanFreeborn.Results/IError.cs
@@ -0,0 +1,20 @@
+namespace StevanFreeborn.Results
+{
+
+ ///
+ /// Represents an error with a code and message.
+ ///
+ public interface IError
+ {
+ ///
+ /// Gets the error code.
+ ///
+ string Code { get; }
+
+ ///
+ /// Gets the error message.
+ ///
+ string Message { get; }
+ }
+
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Results/Result.cs b/src/StevanFreeborn.Results/Result.cs
new file mode 100644
index 0000000..bfd3688
--- /dev/null
+++ b/src/StevanFreeborn.Results/Result.cs
@@ -0,0 +1,264 @@
+namespace StevanFreeborn.Results
+{
+
+ ///
+ /// Represents a result with a value and error type, used for operations that either succeed with a value or fail with an error.
+ ///
+ /// The type of the value.
+ /// The type of the error.
+ public sealed class Result where TError : IError
+ {
+ ///
+ /// Gets a value indicating whether the result is successful.
+ ///
+ public bool IsSuccess { get; }
+
+ ///
+ /// Gets a value indicating whether the result is a failure.
+ ///
+ public bool IsFailure => !IsSuccess;
+
+ ///
+ /// Gets the value of the result.
+ ///
+ /// Thrown when accessing Value on a failed result.
+ public T Value => IsSuccess
+ ? _value
+ : throw new InvalidOperationException($"Cannot access Value on a failed result. Error: [{Error.Code}] {Error.Message}");
+
+ ///
+ /// Gets the error associated with the result.
+ ///
+ /// Thrown when accessing Error on a successful result.
+ public TError Error => _error ?? throw new InvalidOperationException("Cannot access Error on a successful result.");
+
+ private readonly T _value;
+ private readonly TError? _error;
+
+ internal Result(bool isSuccess, T value, TError? error)
+ {
+ IsSuccess = isSuccess;
+ _value = value;
+ _error = error;
+ }
+
+ ///
+ /// Creates a successful result with the specified value.
+ ///
+ /// The value.
+ /// A successful .
+ public static Result Ok(T value)
+ {
+ return new Result(true, value, default);
+ }
+
+ ///
+ /// Creates a failed result with the specified error.
+ ///
+ /// The error.
+ /// A failed .
+ public static Result Fail(TError error)
+ {
+ return new Result(false, default!, error);
+ }
+
+ ///
+ /// Implicitly converts a value to a successful .
+ ///
+ /// The value to convert.
+ public static implicit operator Result(T value)
+ {
+ return Ok(value);
+ }
+
+ ///
+ /// Implicitly converts an to a failed .
+ ///
+ /// The error to convert.
+ public static implicit operator Result(TError error)
+ {
+ return Fail(error);
+ }
+
+ ///
+ /// Maps the value to a new type if the result is successful.
+ ///
+ /// The new type.
+ /// The function to map the value.
+ /// A new with the mapped value if successful, otherwise the current error.
+ /// Thrown when mapper is null.
+ public Result Map(Func mapper)
+ {
+ if (mapper is null)
+ {
+ throw new ArgumentNullException(nameof(mapper));
+ }
+
+ return IsSuccess ? Result.Ok(mapper(Value)) : Result.Fail(Error);
+ }
+
+ ///
+ /// Maps the error to a new error if the result is a failure.
+ ///
+ /// The function to map the error.
+ /// A new with the mapped error if failed, otherwise the current result.
+ /// Thrown when mapper is null.
+ public Result MapError(Func mapper) where TNewError : IError
+ {
+ if (mapper is null)
+ {
+ throw new ArgumentNullException(nameof(mapper));
+ }
+
+ return IsFailure ? Result.Fail(mapper(Error)) : Result.Ok(Value);
+ }
+
+ ///
+ /// Binds to a new result if the current result is successful.
+ ///
+ /// The new result type.
+ /// The function to bind to on success.
+ /// The result of the binder function if successful, otherwise the current error.
+ /// Thrown when binder is null.
+ public Result Bind(Func> binder)
+ {
+ if (binder is null)
+ {
+ throw new ArgumentNullException(nameof(binder));
+ }
+
+ return IsSuccess ? binder(Value) : Result.Fail(Error);
+ }
+
+ ///
+ /// Matches the result and returns a value based on success or failure.
+ ///
+ /// The type of the result.
+ /// The function to execute on success.
+ /// The function to execute on failure.
+ /// The result of the appropriate function.
+ /// Thrown when onSuccess or onFailure is null.
+ public TResult Match(Func onSuccess, Func onFailure)
+ {
+ if (onSuccess is null)
+ {
+ throw new ArgumentNullException(nameof(onSuccess));
+ }
+
+ if (onFailure is null)
+ {
+ throw new ArgumentNullException(nameof(onFailure));
+ }
+
+ return IsSuccess ? onSuccess(Value) : onFailure(Error);
+ }
+
+ ///
+ /// Matches the result and executes the appropriate action.
+ ///
+ /// The action to execute on success.
+ /// The action to execute on failure.
+ /// Thrown when onSuccess or onFailure is null.
+ public void Match(Action onSuccess, Action onFailure)
+ {
+ if (onSuccess is null)
+ {
+ throw new ArgumentNullException(nameof(onSuccess));
+ }
+
+ if (onFailure is null)
+ {
+ throw new ArgumentNullException(nameof(onFailure));
+ }
+
+ if (IsSuccess)
+ {
+ onSuccess(Value);
+ }
+ else
+ {
+ onFailure(Error);
+ }
+ }
+
+ ///
+ /// Executes the specified action if the result is successful, and returns the current result.
+ ///
+ /// The action to execute on success.
+ /// The current result.
+ /// Thrown when action is null.
+ public Result Map(Action action)
+ {
+ if (action is null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ if (IsSuccess)
+ {
+ action(Value);
+ }
+ return this;
+ }
+
+ ///
+ /// Executes the specified action if the result is successful, and returns the current result.
+ ///
+ /// The action to execute on success.
+ /// The current result.
+ /// Thrown when action is null.
+ public Result Map(Action action)
+ {
+ if (action is null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ if (IsSuccess)
+ {
+ action();
+ }
+ return this;
+ }
+
+ ///
+ /// Executes the specified function and wraps the result in a .
+ ///
+ /// The function to execute.
+ /// A successful result with the return value if no exception is thrown; otherwise, a failed result.
+ public static Result Try(Func func)
+ {
+ return Try(func, ex => (TError)(IError)new Error("UnexpectedError", ex.Message));
+ }
+
+ ///
+ /// Executes the specified function and wraps the result in a using the specified error handler.
+ ///
+ /// The function to execute.
+ /// The function to convert exceptions to errors.
+ /// A successful result with the return value if no exception is thrown; otherwise, a failed result.
+ /// Thrown when func or errorHandler is null.
+ public static Result Try(Func func, Func errorHandler)
+ {
+ if (func is null)
+ {
+ throw new ArgumentNullException(nameof(func));
+ }
+
+ if (errorHandler is null)
+ {
+ throw new ArgumentNullException(nameof(errorHandler));
+ }
+
+ try
+ {
+ return Ok(func());
+ }
+ catch (Exception ex)
+ {
+ return Fail(errorHandler(ex));
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Results/ResultAsyncExtensions.cs b/src/StevanFreeborn.Results/ResultAsyncExtensions.cs
new file mode 100644
index 0000000..6bc32b5
--- /dev/null
+++ b/src/StevanFreeborn.Results/ResultAsyncExtensions.cs
@@ -0,0 +1,290 @@
+namespace StevanFreeborn.Results
+{
+
+ ///
+ /// Provides async extension methods for and .
+ ///
+ public static class ResultAsyncExtensions
+ {
+ ///
+ /// Maps the value to a new type asynchronously if the result is successful.
+ ///
+ /// The type of the value.
+ /// The new type.
+ /// The type of the error.
+ /// The result.
+ /// The async function to map the value.
+ /// A task containing a new with the mapped value if successful, otherwise the current error.
+ /// Thrown when result or mapper is null.
+ public static async Task> MapAsync(this Result result, Func> mapper)
+ where TError : IError
+ {
+ if (result is null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+ if (mapper is null)
+ {
+ throw new ArgumentNullException(nameof(mapper));
+ }
+
+ return result.IsSuccess ? await mapper(result.Value).ConfigureAwait(false) : result.Error;
+ }
+
+ ///
+ /// Maps the error to a new error asynchronously if the result is a failure.
+ ///
+ /// The type of the value.
+ /// The type of the error.
+ /// The new error type.
+ /// The result.
+ /// The async function to map the error.
+ /// A task containing a new with the mapped error if failed, otherwise the current result.
+ /// Thrown when result or mapper is null.
+ public static async Task> MapErrorAsync(this Result result, Func> mapper)
+ where TError : IError
+ where TNewError : IError
+ {
+ if (result is null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+ if (mapper is null)
+ {
+ throw new ArgumentNullException(nameof(mapper));
+ }
+
+ return result.IsFailure ? await mapper(result.Error).ConfigureAwait(false) : Result.Ok(result.Value);
+ }
+
+ ///
+ /// Executes the specified async action if the result is successful, and returns the current result.
+ ///
+ /// The type of the error.
+ /// The result.
+ /// The async action to execute on success.
+ /// A task containing the result.
+ /// Thrown when result or onSuccess is null.
+ public static async Task> MapAsync(this Result result, Func onSuccess)
+ where TError : IError
+ {
+ if (result is null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+ if (onSuccess is null)
+ {
+ throw new ArgumentNullException(nameof(onSuccess));
+ }
+
+ if (result.IsSuccess)
+ {
+ await onSuccess(result.Value).ConfigureAwait(false);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Binds to a new async result if the current result is successful.
+ ///
+ /// The type of the error.
+ /// The result.
+ /// The async function to execute and bind to on success.
+ /// A task containing the result of the binder function if successful, otherwise the current result.
+ /// Thrown when result or onSuccess is null.
+ public static async Task> BindAsync(this Result result, Func>> onSuccess)
+ where TError : IError
+ {
+ if (result is null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+ if (onSuccess is null)
+ {
+ throw new ArgumentNullException(nameof(onSuccess));
+ }
+
+ return result.IsSuccess ? await onSuccess(result.Value).ConfigureAwait(false) : result;
+ }
+
+ ///
+ /// Matches the result asynchronously and returns a value based on success or failure.
+ ///
+ /// The type of the result.
+ /// The type of the error.
+ /// The result.
+ /// The async function to execute on success.
+ /// The async function to execute on failure.
+ /// A task containing the result of the appropriate function.
+ /// Thrown when result, onSuccess, or onFailure is null.
+ public static async Task MatchAsync(this Result result, Func> onSuccess, Func> onFailure)
+ where TError : IError
+ {
+ if (result is null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+ if (onSuccess is null)
+ {
+ throw new ArgumentNullException(nameof(onSuccess));
+ }
+
+ if (onFailure is null)
+ {
+ throw new ArgumentNullException(nameof(onFailure));
+ }
+
+ return result.IsSuccess
+ ? await onSuccess(result.Value).ConfigureAwait(false)
+ : await onFailure(result.Error).ConfigureAwait(false);
+ }
+
+ ///
+ /// Executes the specified async action and wraps the result in a .
+ ///
+ /// The async action to execute.
+ /// A task containing a successful result if no exception is thrown; otherwise, a failed result.
+ public static Task> TryAsync(Func action)
+ {
+ return TryAsync(action, ex => new Error("UnexpectedError", ex.Message));
+ }
+
+ ///
+ /// Executes the specified async action and wraps the result in a using the specified error handler.
+ ///
+ /// The async action to execute.
+ /// The function to convert exceptions to errors.
+ /// A task containing a successful result if no exception is thrown; otherwise, a failed result.
+ /// Thrown when action or errorHandler is null.
+ public static async Task> TryAsync(Func action, Func errorHandler)
+ {
+ if (action is null)
+ {
+ throw new ArgumentNullException(nameof(action));
+ }
+
+ if (errorHandler is null)
+ {
+ throw new ArgumentNullException(nameof(errorHandler));
+ }
+
+ try
+ {
+ await action().ConfigureAwait(false);
+ return Result.Ok(default);
+ }
+ catch (Exception ex)
+ {
+ return Result.Fail(errorHandler(ex));
+ }
+ }
+
+ ///
+ /// Binds to a new async result if the current result is successful.
+ ///
+ /// The type of the value.
+ /// The new result type.
+ /// The type of the error.
+ /// The result.
+ /// The async function to bind to on success.
+ /// A task containing the result of the binder function if successful, otherwise the current error.
+ /// Thrown when result or binder is null.
+ public static async Task> BindAsync(this Result result, Func>> binder)
+ where TError : IError
+ {
+ if (result is null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+ if (binder is null)
+ {
+ throw new ArgumentNullException(nameof(binder));
+ }
+
+ return result.IsSuccess ? await binder(result.Value).ConfigureAwait(false) : Result.Fail(result.Error);
+ }
+
+ ///
+ /// Matches the result asynchronously and returns a value based on success or failure.
+ ///
+ /// The type of the value.
+ /// The type of the result.
+ /// The type of the error.
+ /// The result.
+ /// The async function to execute on success.
+ /// The async function to execute on failure.
+ /// A task containing the result of the appropriate function.
+ /// Thrown when result, onSuccess, or onFailure is null.
+ public static async Task MatchAsync(this Result result, Func> onSuccess, Func> onFailure)
+ where TError : IError
+ {
+ if (result is null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+ if (onSuccess is null)
+ {
+ throw new ArgumentNullException(nameof(onSuccess));
+ }
+
+ if (onFailure is null)
+ {
+ throw new ArgumentNullException(nameof(onFailure));
+ }
+
+ return result.IsSuccess
+ ? await onSuccess(result.Value).ConfigureAwait(false)
+ : await onFailure(result.Error).ConfigureAwait(false);
+ }
+
+ ///
+ /// Executes the specified async function and wraps the result in a .
+ ///
+ /// The type of the value.
+ /// The async function to execute.
+ /// A task containing a successful result with the return value if no exception is thrown; otherwise, a failed result.
+ public static Task> TryAsync(Func> func)
+ {
+ return TryAsync(func, ex => new Error("UnexpectedError", ex.Message));
+ }
+
+ ///
+ /// Executes the specified async function and wraps the result in a using the specified error handler.
+ ///
+ /// The type of the value.
+ /// The async function to execute.
+ /// The function to convert exceptions to errors.
+ /// A task containing a successful result with the return value if no exception is thrown; otherwise, a failed result.
+ /// Thrown when func or errorHandler is null.
+ public static async Task> TryAsync(Func> func, Func errorHandler)
+ {
+ if (func is null)
+ {
+ throw new ArgumentNullException(nameof(func));
+ }
+
+ if (errorHandler is null)
+ {
+ throw new ArgumentNullException(nameof(errorHandler));
+ }
+
+ try
+ {
+ return Result.Ok(await func().ConfigureAwait(false));
+ }
+ catch (Exception ex)
+ {
+ return Result.Fail(errorHandler(ex));
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Results/StevanFreeborn.Results.csproj b/src/StevanFreeborn.Results/StevanFreeborn.Results.csproj
new file mode 100644
index 0000000..ba62683
--- /dev/null
+++ b/src/StevanFreeborn.Results/StevanFreeborn.Results.csproj
@@ -0,0 +1,29 @@
+
+
+ netstandard2.1;net10.0
+ true
+ true
+ true
+
+
+ StevanFreeborn.Results
+ 0.0.0
+ StevanFreeborn
+ A minimalistic, AOT-compatible Result type library for railway-oriented programming and functional error handling.
+ result;railway;functional;error-handling;discriminated-union
+ MIT
+ https://github.com/StevanFreeborn/stevanfreeborn.results
+ git
+ true
+ true
+ true
+ snupkg
+
+
+
+
+
+
+
+
+
diff --git a/src/StevanFreeborn.Results/Unit.cs b/src/StevanFreeborn.Results/Unit.cs
new file mode 100644
index 0000000..72bce74
--- /dev/null
+++ b/src/StevanFreeborn.Results/Unit.cs
@@ -0,0 +1,11 @@
+namespace StevanFreeborn.Results
+{
+
+ ///
+ /// Represents a void-like type for use with Result when no value is needed.
+ ///
+ public readonly struct Unit
+ {
+ }
+
+}
\ No newline at end of file
diff --git a/tests/.editorconfig b/tests/.editorconfig
new file mode 100644
index 0000000..707fe12
--- /dev/null
+++ b/tests/.editorconfig
@@ -0,0 +1,6 @@
+[*.cs]
+
+dotnet_diagnostic.CA1515.severity = none
+dotnet_diagnostic.CA2007.severity = none
+dotnet_diagnostic.CA2201.severity = none
+dotnet_diagnostic.CA1707.severity = none
diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props
new file mode 100644
index 0000000..cf8b59b
--- /dev/null
+++ b/tests/Directory.Packages.props
@@ -0,0 +1,8 @@
+
+
+ true
+
+
+
+
+
diff --git a/tests/StevanFreeborn.Results.Tests/ErrorTests.cs b/tests/StevanFreeborn.Results.Tests/ErrorTests.cs
new file mode 100644
index 0000000..f6c2368
--- /dev/null
+++ b/tests/StevanFreeborn.Results.Tests/ErrorTests.cs
@@ -0,0 +1,57 @@
+namespace StevanFreeborn.Results.Tests;
+
+public class ErrorTests
+{
+ [Test]
+ public async Task Constructor_WhenCalled_ItShouldSetCode()
+ {
+ var error = new Error("code", "message");
+ await Assert.That(error.Code).IsEqualTo("code");
+ }
+
+ [Test]
+ public async Task Constructor_WhenCalled_ItShouldSetMessage()
+ {
+ var error = new Error("code", "message");
+ await Assert.That(error.Message).IsEqualTo("message");
+ }
+
+ [Test]
+ public async Task Constructor_WhenCalledWithMetadata_ItShouldSetMetadata()
+ {
+ var metadata = new Dictionary { { "key", "value" } };
+ var error = new Error("code", "message", metadata);
+ await Assert.That(error.Metadata!.Count).IsEqualTo(1);
+ await Assert.That(error.Metadata["key"]).IsEqualTo("value");
+ }
+
+ [Test]
+ public async Task Constructor_WhenCalledWithNullMetadata_ItShouldSetMetadataToNull()
+ {
+ var error = new Error("code", "message", null);
+ await Assert.That(error.Metadata!).IsNull();
+ }
+
+ [Test]
+ public async Task Equals_WhenErrorsHaveSameCodeAndMessage_ItShouldReturnTrue()
+ {
+ var error1 = new Error("code", "message");
+ var error2 = new Error("code", "message");
+ await Assert.That(error1).IsEqualTo(error2);
+ }
+
+ [Test]
+ public async Task Equals_WhenErrorsHaveDifferentCode_ItShouldReturnFalse()
+ {
+ var error1 = new Error("code1", "message");
+ var error2 = new Error("code2", "message");
+ await Assert.That(error1 == error2).IsFalse();
+ }
+
+ [Test]
+ public async Task Error_WhenCreated_ItShouldImplementIError()
+ {
+ var error = new Error("code", "message");
+ await Assert.That(error).IsAssignableTo();
+ }
+}
diff --git a/tests/StevanFreeborn.Results.Tests/ResultAsyncExtensionsTests.cs b/tests/StevanFreeborn.Results.Tests/ResultAsyncExtensionsTests.cs
new file mode 100644
index 0000000..6b821a3
--- /dev/null
+++ b/tests/StevanFreeborn.Results.Tests/ResultAsyncExtensionsTests.cs
@@ -0,0 +1,174 @@
+using System.Globalization;
+
+namespace StevanFreeborn.Results.Tests;
+
+public class ResultAsyncExtensionsTests
+{
+ [Test]
+ public async Task MapAsync_WhenResultIsSuccess_ItShouldInvokeAsyncAction()
+ {
+ var result = Result.Ok(default);
+ var invoked = false;
+ await result.MapAsync(_ => { invoked = true; return Task.CompletedTask; });
+ await Assert.That(invoked).IsTrue();
+ }
+
+ [Test]
+ public async Task MapAsync_WhenResultIsFailure_ItShouldSkipAsyncAction()
+ {
+ var result = Result.Fail(new Error("code", "message"));
+ var invoked = false;
+ await result.MapAsync(_ => { invoked = true; return Task.CompletedTask; });
+ await Assert.That(invoked).IsFalse();
+ }
+
+ [Test]
+ public async Task MapAsync_WhenCalled_ItShouldReturnOriginalResult()
+ {
+ var result = Result.Ok(default);
+ var mapped = await result.MapAsync(_ => Task.CompletedTask);
+ await Assert.That(mapped.IsSuccess).IsTrue();
+ }
+
+ [Test]
+ public async Task BindAsync_WhenResultIsSuccess_ItShouldInvokeBinderAndReturnResult()
+ {
+ var result = Result.Ok(default);
+ var bound = await result.BindAsync(_ => Task.FromResult(Result.Fail(new Error("new_error", "new message"))));
+ await Assert.That(bound.IsFailure).IsTrue();
+ }
+
+ [Test]
+ public async Task BindAsync_WhenResultIsFailure_ItShouldReturnOriginalResult()
+ {
+ var result = Result.Fail(new Error("code", "message"));
+ var bound = await result.BindAsync(_ => Task.FromResult(Result.Ok(default)));
+ await Assert.That(bound.IsFailure).IsTrue();
+ await Assert.That(bound.Error.Code).IsEqualTo("code");
+ }
+
+ [Test]
+ public async Task MatchAsync_WhenResultIsSuccess_ItShouldReturnOnSuccessValue()
+ {
+ var result = Result.Ok(default);
+ var matched = await result.MatchAsync(_ => Task.FromResult("success"), _ => Task.FromResult("failure"));
+ await Assert.That(matched).IsEqualTo("success");
+ }
+
+ [Test]
+ public async Task MatchAsync_WhenResultIsFailure_ItShouldReturnOnFailureValue()
+ {
+ var result = Result.Fail(new Error("code", "message"));
+ var matched = await result.MatchAsync(_ => Task.FromResult("success"), e => Task.FromResult(e.Code));
+ await Assert.That(matched).IsEqualTo("code");
+ }
+
+ [Test]
+ public async Task TryAsync_WhenActionSucceeds_ItShouldReturnOk()
+ {
+ var result = await ResultAsyncExtensions.TryAsync(() => Task.CompletedTask);
+ await Assert.That(result.IsSuccess).IsTrue();
+ }
+
+ [Test]
+ public async Task TryAsync_WhenActionThrowsException_ItShouldReturnFailWithUnexpectedError()
+ {
+ var result = await ResultAsyncExtensions.TryAsync(() => throw new InvalidOperationException("test error"));
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(result.Error.Code).IsEqualTo("UnexpectedError");
+ }
+
+ [Test]
+ public async Task TryAsync_WhenActionThrowsException_ItShouldUseCustomError()
+ {
+ var result = await ResultAsyncExtensions.TryAsync(
+ () => throw new InvalidOperationException("test"),
+ ex => new Error("custom", ex.Message));
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(result.Error.Code).IsEqualTo("custom");
+ }
+
+ [Test]
+ public async Task MapAsync_WhenResultIsSuccess_ItShouldTransformValue()
+ {
+ var result = Result.Ok(5);
+ var mapped = await result.MapAsync(async n => (await Task.FromResult(n)).ToString(CultureInfo.InvariantCulture));
+ await Assert.That(mapped.IsSuccess).IsTrue();
+ await Assert.That(mapped.Value).IsEqualTo("5");
+ }
+
+ [Test]
+ public async Task MapAsync_WhenResultIsFailure_ItShouldPropagateError()
+ {
+ var error = new Error("code", "message");
+ var result = Result.Fail(error);
+ var mapped = await result.MapAsync(async n => (await Task.FromResult(n)).ToString(CultureInfo.InvariantCulture));
+ await Assert.That(mapped.IsFailure).IsTrue();
+ await Assert.That(mapped.Error).IsEqualTo(error);
+ }
+
+ [Test]
+ public async Task BindAsync_WhenResultIsSuccess_ItShouldChainToBinderResult()
+ {
+ var result = Result.Ok(5);
+ var bound = await result.BindAsync(async n => await Task.FromResult(Result.Ok(n.ToString(CultureInfo.InvariantCulture))));
+ await Assert.That(bound.IsSuccess).IsTrue();
+ await Assert.That(bound.Value).IsEqualTo("5");
+ }
+
+ [Test]
+ public async Task BindAsync_WhenResultIsFailure_ItShouldPropagateError()
+ {
+ var error = new Error("code", "message");
+ var result = Result.Fail(error);
+ var bound = await result.BindAsync(async n => await Task.FromResult(Result.Ok(n.ToString(CultureInfo.InvariantCulture))));
+ await Assert.That(bound.IsFailure).IsTrue();
+ await Assert.That(bound.Error).IsEqualTo(error);
+ }
+
+ [Test]
+ public async Task MatchAsyncGeneric_WhenResultIsSuccess_ItShouldReturnOnSuccessValue()
+ {
+ var result = Result.Ok("test");
+ var matched = await result.MatchAsync(
+ v => Task.FromResult(v.Length),
+ _ => Task.FromResult(0));
+ await Assert.That(matched).IsEqualTo(4);
+ }
+
+ [Test]
+ public async Task MatchAsyncGeneric_WhenResultIsFailure_ItShouldReturnOnFailureValue()
+ {
+ var result = Result.Fail(new Error("code", "message"));
+ var matched = await result.MatchAsync(
+ v => Task.FromResult(v.Length),
+ _ => Task.FromResult(-1));
+ await Assert.That(matched).IsEqualTo(-1);
+ }
+
+ [Test]
+ public async Task TryAsync_WhenFuncSucceeds_ItShouldReturnOkWithValue()
+ {
+ var result = await ResultAsyncExtensions.TryAsync(async () => await Task.FromResult(42));
+ await Assert.That(result.IsSuccess).IsTrue();
+ await Assert.That(result.Value).IsEqualTo(42);
+ }
+
+ [Test]
+ public async Task TryAsync_WhenFuncThrowsException_ItShouldReturnFailWithUnexpectedError()
+ {
+ var result = await ResultAsyncExtensions.TryAsync(() => throw new InvalidOperationException("test error"));
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(result.Error.Code).IsEqualTo("UnexpectedError");
+ }
+
+ [Test]
+ public async Task TryAsync_WhenFuncThrowsException_ItShouldUseCustomError()
+ {
+ var result = await ResultAsyncExtensions.TryAsync(
+ () => throw new InvalidOperationException("test"),
+ ex => new Error("custom", ex.Message));
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(result.Error.Code).IsEqualTo("custom");
+ }
+}
diff --git a/tests/StevanFreeborn.Results.Tests/ResultTTests.cs b/tests/StevanFreeborn.Results.Tests/ResultTTests.cs
new file mode 100644
index 0000000..35e846f
--- /dev/null
+++ b/tests/StevanFreeborn.Results.Tests/ResultTTests.cs
@@ -0,0 +1,183 @@
+using System.Globalization;
+
+namespace StevanFreeborn.Results.Tests;
+
+public class ResultTTests
+{
+ [Test]
+ public async Task Ok_WhenCalledWithValue_ItShouldReturnSuccessResult()
+ {
+ var result = Result.Ok("test");
+ await Assert.That(result.IsSuccess).IsTrue();
+ await Assert.That(result.IsFailure).IsFalse();
+ await Assert.That(result.Value).IsEqualTo("test");
+ }
+
+ [Test]
+ public async Task Fail_WhenCalled_ItShouldReturnFailureResult()
+ {
+ var error = new Error("code", "message");
+ var result = Result.Fail(error);
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(result.IsSuccess).IsFalse();
+ await Assert.That(result.Error).IsEqualTo(error);
+ }
+
+ [Test]
+ public async Task Value_WhenResultIsFailure_ItShouldThrowInvalidOperationException()
+ {
+ var result = Result.Fail(new Error("code", "message"));
+
+ Assert.Throws(() => _ = result.Value);
+ }
+
+ [Test]
+ public async Task Error_WhenResultIsSuccess_ItShouldThrowInvalidOperationException()
+ {
+ var result = Result.Ok("test");
+
+ Assert.Throws(() => _ = result.Error);
+ }
+
+ [Test]
+ public async Task ImplicitConversion_FromValue_ItShouldReturnSuccessResult()
+ {
+ Result result = "test";
+ await Assert.That(result.IsSuccess).IsTrue();
+ await Assert.That(result.Value).IsEqualTo("test");
+ }
+
+ [Test]
+ public async Task ImplicitConversion_FromError_ItShouldReturnFailureResult()
+ {
+ var error = new Error("code", "message");
+ Result result = error;
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(result.Error).IsEqualTo(error);
+ }
+
+ [Test]
+ public async Task Map_WhenResultIsSuccess_ItShouldTransformValue()
+ {
+ var result = Result.Ok(5);
+ var mapped = result.Map(n => n.ToString(CultureInfo.InvariantCulture));
+ await Assert.That(mapped.IsSuccess).IsTrue();
+ await Assert.That(mapped.Value).IsEqualTo("5");
+ }
+
+ [Test]
+ public async Task Map_WhenResultIsFailure_ItShouldPropagateError()
+ {
+ var error = new Error("code", "message");
+ var result = Result.Fail(error);
+ var mapped = result.Map(n => n.ToString(CultureInfo.InvariantCulture));
+ await Assert.That(mapped.IsFailure).IsTrue();
+ await Assert.That(mapped.Error).IsEqualTo(error);
+ }
+
+ [Test]
+ public async Task MapError_WhenResultIsFailure_ItShouldTransformError()
+ {
+ var originalError = new Error("original", "original message");
+ var result = Result.Fail(originalError);
+ var transformed = result.MapError(e => new Error("transformed", e.Message));
+ await Assert.That(transformed.IsFailure).IsTrue();
+ await Assert.That(transformed.Error.Code).IsEqualTo("transformed");
+ }
+
+ [Test]
+ public async Task MapError_WhenResultIsSuccess_ItShouldReturnUnchanged()
+ {
+ var result = Result.Ok(5);
+ var mapped = result.MapError(e => new Error("transformed", e.Message));
+ await Assert.That(mapped.IsSuccess).IsTrue();
+ await Assert.That(mapped.Value).IsEqualTo(5);
+ }
+
+ [Test]
+ public async Task Bind_WhenResultIsSuccess_ItShouldChainToBinderResult()
+ {
+ var result = Result.Ok(5);
+ var bound = result.Bind(n => Result.Ok(n.ToString(CultureInfo.InvariantCulture)));
+ await Assert.That(bound.IsSuccess).IsTrue();
+ await Assert.That(bound.Value).IsEqualTo("5");
+ }
+
+ [Test]
+ public async Task Bind_WhenResultIsFailure_ItShouldPropagateError()
+ {
+ var error = new Error("code", "message");
+ var result = Result.Fail(error);
+ var bound = result.Bind(n => Result.Ok(n.ToString(CultureInfo.InvariantCulture)));
+ await Assert.That(bound.IsFailure).IsTrue();
+ await Assert.That(bound.Error).IsEqualTo(error);
+ }
+
+ [Test]
+ public async Task Bind_WhenResultIsSuccess_ItShouldReturnBinderFailureResult()
+ {
+ var result = Result.Ok(5);
+ var bound = result.Bind(_ => Result.Fail(new Error("bind_error", "bound failed")));
+ await Assert.That(bound.IsFailure).IsTrue();
+ await Assert.That(bound.Error.Code).IsEqualTo("bind_error");
+ }
+
+ [Test]
+ public async Task Match_WhenResultIsSuccess_ItShouldReturnOnSuccessValue()
+ {
+ var result = Result.Ok("test");
+ var matched = result.Match(v => v.Length, _ => 0);
+ await Assert.That(matched).IsEqualTo(4);
+ }
+
+ [Test]
+ public async Task Match_WhenResultIsFailure_ItShouldReturnOnFailureValue()
+ {
+ var result = Result.Fail(new Error("code", "message"));
+ var matched = result.Match(v => v.Length, e => -1);
+ await Assert.That(matched).IsEqualTo(-1);
+ }
+
+ [Test]
+ public async Task Match_WhenResultIsSuccess_ItShouldInvokeOnSuccess()
+ {
+ var result = Result.Ok("test");
+ var invoked = false;
+ result.Match(v => { invoked = true; }, _ => { });
+ await Assert.That(invoked).IsTrue();
+ }
+
+ [Test]
+ public async Task Match_WhenResultIsFailure_ItShouldInvokeOnFailure()
+ {
+ var error = new Error("code", "message");
+ var result = Result.Fail(error);
+ var invoked = false;
+ result.Match(_ => { }, e => { invoked = true; });
+ await Assert.That(invoked).IsTrue();
+ }
+
+ [Test]
+ public async Task Try_WhenFuncSucceeds_ItShouldReturnOkWithValue()
+ {
+ var result = Result.Try(() => 42);
+ await Assert.That(result.IsSuccess).IsTrue();
+ await Assert.That(result.Value).IsEqualTo(42);
+ }
+
+ [Test]
+ public async Task Try_WhenFuncThrowsException_ItShouldReturnFailWithUnexpectedError()
+ {
+ var result = Result.Try(() => throw new InvalidOperationException("test error"));
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(result.Error.Code).IsEqualTo("UnexpectedError");
+ }
+
+ [Test]
+ public async Task Try_WhenFuncThrowsException_ItShouldUseCustomError()
+ {
+ var result = Result.Try(() => throw new InvalidOperationException("test"), ex => new Error("custom", ex.Message));
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(result.Error.Code).IsEqualTo("custom");
+ }
+}
diff --git a/tests/StevanFreeborn.Results.Tests/ResultTests.cs b/tests/StevanFreeborn.Results.Tests/ResultTests.cs
new file mode 100644
index 0000000..13a0f00
--- /dev/null
+++ b/tests/StevanFreeborn.Results.Tests/ResultTests.cs
@@ -0,0 +1,146 @@
+namespace StevanFreeborn.Results.Tests;
+
+public class ResultTests
+{
+ [Test]
+ public async Task Ok_WhenCalled_ItShouldReturnSuccessResult()
+ {
+ var result = Result.Ok(default);
+ await Assert.That(result.IsSuccess).IsTrue();
+ await Assert.That(result.IsFailure).IsFalse();
+ }
+
+ [Test]
+ public async Task Fail_WhenCalled_ItShouldReturnFailureResult()
+ {
+ var error = new Error("code", "message");
+ var result = Result.Fail(error);
+ await Assert.That(result.IsSuccess).IsFalse();
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(result.Error).IsEqualTo(error);
+ }
+
+ [Test]
+ public async Task Error_WhenResultIsSuccess_ItShouldThrowInvalidOperationException()
+ {
+ Assert.Throws(() => _ = Result.Ok(default).Error);
+ }
+
+ [Test]
+ public async Task ImplicitConversion_FromError_ItShouldReturnFailureResult()
+ {
+ var error = new Error("code", "message");
+ Result result = error;
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(result.Error).IsEqualTo(error);
+ }
+
+ [Test]
+ public async Task Map_WhenResultIsSuccess_ItShouldInvokeAction()
+ {
+ var result = Result.Ok(default);
+ var invoked = false;
+ result.Map(_ => invoked = true);
+ await Assert.That(invoked).IsTrue();
+ }
+
+ [Test]
+ public async Task Map_WhenResultIsFailure_ItShouldSkipAction()
+ {
+ var result = Result.Fail(new Error("code", "message"));
+ var invoked = false;
+ result.Map(_ => invoked = true);
+ await Assert.That(invoked).IsFalse();
+ }
+
+ [Test]
+ public async Task Map_WhenCalled_ItShouldReturnOriginalResult()
+ {
+ var result = Result.Ok(default);
+ var mapped = result.Map(_ => { });
+ await Assert.That(mapped.IsSuccess).IsTrue();
+ }
+
+ [Test]
+ public async Task Bind_WhenResultIsSuccess_ItShouldInvokeBinder()
+ {
+ var result = Result.Ok(default);
+ var bound = result.Bind(_ => Result.Ok(default));
+ await Assert.That(bound.IsSuccess).IsTrue();
+ }
+
+ [Test]
+ public async Task Bind_WhenResultIsFailure_ItShouldReturnOriginalResult()
+ {
+ var result = Result.Fail(new Error("code", "message"));
+ var bound = result.Bind(_ => Result.Ok(default));
+ await Assert.That(bound.IsFailure).IsTrue();
+ await Assert.That(bound.Error).IsEqualTo(result.Error);
+ }
+
+ [Test]
+ public async Task Bind_WhenResultIsSuccess_ItShouldReturnBinderResult()
+ {
+ var result = Result.Ok(default);
+ var bound = result.Bind(_ => Result.Fail(new Error("new_error", "new message")));
+ await Assert.That(bound.IsFailure).IsTrue();
+ }
+
+ [Test]
+ public async Task Match_WhenResultIsSuccess_ItShouldReturnOnSuccessValue()
+ {
+ var result = Result.Ok(default);
+ var matched = result.Match(_ => "success", _ => "failure");
+ await Assert.That(matched).IsEqualTo("success");
+ }
+
+ [Test]
+ public async Task Match_WhenResultIsFailure_ItShouldReturnOnFailureValue()
+ {
+ var result = Result.Fail(new Error("code", "message"));
+ var matched = result.Match(_ => "success", e => e.Code);
+ await Assert.That(matched).IsEqualTo("code");
+ }
+
+ [Test]
+ public async Task Match_WhenResultIsSuccess_ItShouldInvokeOnSuccess()
+ {
+ var result = Result.Ok(default);
+ var invoked = false;
+ result.Match(_ => invoked = true, _ => { });
+ await Assert.That(invoked).IsTrue();
+ }
+
+ [Test]
+ public async Task Match_WhenResultIsFailure_ItShouldInvokeOnFailure()
+ {
+ var error = new Error("code", "message");
+ var result = Result.Fail(error);
+ var invoked = false;
+ result.Match(_ => { }, e => invoked = true);
+ await Assert.That(invoked).IsTrue();
+ }
+
+ [Test]
+ public async Task Try_WhenActionSucceeds_ItShouldReturnOk()
+ {
+ var result = Result.Try(() => default(Unit));
+ await Assert.That(result.IsSuccess).IsTrue();
+ }
+
+ [Test]
+ public async Task Try_WhenActionThrowsException_ItShouldReturnFailWithUnexpectedError()
+ {
+ var result = Result.Try(() => throw new Exception("test error"));
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(result.Error.Code).IsEqualTo("UnexpectedError");
+ }
+
+ [Test]
+ public async Task Try_WhenActionThrowsException_ItShouldUseCustomError()
+ {
+ var result = Result.Try(() => throw new Exception("test"), ex => new Error("custom", ex.Message));
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(result.Error.Code).IsEqualTo("custom");
+ }
+}
diff --git a/tests/StevanFreeborn.Results.Tests/StevanFreeborn.Results.Tests.csproj b/tests/StevanFreeborn.Results.Tests/StevanFreeborn.Results.Tests.csproj
new file mode 100644
index 0000000..f0dc5b5
--- /dev/null
+++ b/tests/StevanFreeborn.Results.Tests/StevanFreeborn.Results.Tests.csproj
@@ -0,0 +1,13 @@
+
+
+ Exe
+ StevanFreeborn.Results.Tests
+ net10.0
+
+
+
+
+
+
+
+