feat: initial project commit
All checks were successful
default / default (8.0) (push) Successful in 1m7s

This commit is contained in:
Louis Seubert 2024-04-14 17:42:13 +02:00
commit 30ef7bd477
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
40 changed files with 3752 additions and 0 deletions

447
.editorconfig Normal file
View file

@ -0,0 +1,447 @@
root = true
[*]
indent_style = tab
indent_size = 4
tab_width = 4
end_of_line = lf
insert_final_newline = false
trim_trailing_whitespace = true
[.forgejo/workflows/*.yml]
indent_size = 2
indent_style = space
[*.{xml,csproj,props,targets}]
indent_size = 2
indent_style = space
[nuget.config]
indent_size = 2
indent_style = space
[*.{cs,vb}] #### .NET Coding Conventions ####
# 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_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_compound_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true: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
dotnet_style_namespace_match_folder = false # resharper
# 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
# ReSharper preferences
resharper_check_namespace_highlighting = none
[*.cs] #### C# Coding Conventions ####
# var preferences
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = 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_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_local_function = true:warning
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:silent
# Code-block preferences
csharp_prefer_braces = true:silent
csharp_prefer_simple_using_statement = true:suggestion
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = 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
# 'namespace' preferences
csharp_style_namespace_declarations = file_scoped:warning
[*.cs] #### 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
[*.{cs,vb}] #### .NET Naming styles ####
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_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_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_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_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_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_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_symbols.type_parameters.applicable_kinds = namespace
dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
dotnet_naming_symbols.type_parameters.required_modifiers =
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_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_rule.async_methods_end_in_async.severity = suggestion
dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods
dotnet_naming_rule.async_methods_end_in_async.style = end_in_async
dotnet_naming_symbols.any_async_methods.applicable_kinds = method
dotnet_naming_symbols.any_async_methods.applicable_accessibilities = *
dotnet_naming_symbols.any_async_methods.required_modifiers = async
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_symbols.parameters.applicable_kinds = parameter
dotnet_naming_symbols.parameters.applicable_accessibilities = *
dotnet_naming_symbols.parameters.required_modifiers =
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_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_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_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 =
# local
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_symbols.local_variables.applicable_kinds = local
dotnet_naming_symbols.local_variables.applicable_accessibilities = local
dotnet_naming_symbols.local_variables.required_modifiers =
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_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_symbols.local_functions.applicable_accessibilities = *
dotnet_naming_symbols.local_functions.required_modifiers =
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_symbols.local_constants.applicable_kinds = local
dotnet_naming_symbols.local_constants.applicable_accessibilities = local
dotnet_naming_symbols.local_constants.required_modifiers = const
# private
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_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_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_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_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_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_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_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
# public
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_symbols.public_fields.applicable_kinds = field
dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
dotnet_naming_symbols.public_fields.required_modifiers =
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_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_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_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
# others
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
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.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
dotnet_naming_style.end_in_async.required_prefix =
dotnet_naming_style.end_in_async.required_suffix = Async
dotnet_naming_style.end_in_async.capitalization = pascal_case
dotnet_naming_style.end_in_async.word_separator =
[*.{cs.vb}]
dotnet_diagnostic.IDE0055.severity = error
# IDE0051: Remove unused private member
dotnet_diagnostic.IDE0051.severity = error
# IDE0052: Remove unread private member
dotnet_diagnostic.IDE0052.severity = error
# IDE0064: Make struct fields writable
dotnet_diagnostic.IDE0064.severity = error
dotnet_analyzer_diagnostic.severity = error
# CS1591: Missing XML comment for publicly visible type or member
dotnet_diagnostic.CS1591.severity = suggestion
# CA1018: Mark attributes with AttributeUsageAttribute
dotnet_diagnostic.CA1018.severity = error
# CA1304: Specify CultureInfo
dotnet_diagnostic.CA1304.severity = warning
# CA1802: Use literals where appropriate
dotnet_diagnostic.CA1802.severity = warning
# CA1813: Avoid unsealed attributes
dotnet_diagnostic.CA1813.severity = error
# CA1815: Override equals and operator equals on value types
dotnet_diagnostic.CA1815.severity = warning
# CA1820: Test for empty strings using string length
dotnet_diagnostic.CA1820.severity = warning
# CA1821: Remove empty finalizers
dotnet_diagnostic.CA1821.severity = warning
# CA1822: Mark members as static
dotnet_diagnostic.CA1822.severity = suggestion
# CA1823: Avoid unused private fields
dotnet_diagnostic.CA1823.severity = warning
dotnet_code_quality.CA1822.api_surface = private, internal
# CA1825: Avoid zero-length array allocations
dotnet_diagnostic.CA1825.severity = warning
# CA1826: Use property instead of Linq Enumerable method
dotnet_diagnostic.CA1826.severity = suggestion
# CA1827: Do not use Count/LongCount when Any can be used
dotnet_diagnostic.CA1827.severity = warning
# CA1828: Do not use CountAsync/LongCountAsync when AnyAsync can be used
dotnet_diagnostic.CA1828.severity = warning
# CA1829: Use Length/Count property instead of Enumerable.Count method
dotnet_diagnostic.CA1829.severity = warning
#CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
dotnet_diagnostic.CA1847.severity = warning
#CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method
dotnet_diagnostic.CA1854.severity = warning
#CA2211:Non-constant fields should not be visible
dotnet_diagnostic.CA2211.severity = error

View file

@ -0,0 +1,15 @@
# workflows
## default.yml
The default workflow which is ran on every `push` or `pull_request` which will
build and package the project. This will also run the tests as part of the
workflow to ensure the code actually behaves like it should.
When the workflow runs on the `develop` branch of the repository, then an alpha
build is released to the registry.
## release.yml
The release workflow just build the current repository ref and publishes the
artifacts to the registry.

View file

@ -0,0 +1,37 @@
name: default
on:
push:
branches: ["main", "develop"]
paths-ignore:
- "doc/**"
- "*.md"
pull_request:
branches: ["main", "develop"]
paths-ignore:
- "doc/**"
- "*.md"
jobs:
default:
runs-on: debian-latest
strategy:
matrix:
dotnet-version: ["8.0"]
container: mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet-version }}
steps:
- name: checkout
uses: https://git.geekeey.de/actions/checkout@1
- name: nuget login
run: |
# This token is readonly and can only be used for restore
dotnet nuget update source geekeey --store-password-in-clear-text \
--username "${{ github.actor }}" --password "${{ github.token }}"
- name: dotnet pack
run: |
dotnet pack -p:ContinuousIntegrationBuild=true
- name: dotnet test
run: |
dotnet test

View file

@ -0,0 +1,24 @@
name: release
on:
push:
tags: ["[0-9]+.[0-9]+.[0-9]+"]
jobs:
release:
runs-on: debian-latest
strategy:
matrix:
dotnet-version: ["8.0"]
container: mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet-version }}
steps:
- uses: https://git.geekeey.de/actions/checkout@1
- name: nuget login
run: |
# This token is readonly and can only be used for restore
dotnet nuget update source geekeey --store-password-in-clear-text \
--username "${{ github.actor }}" --password "${{ github.token }}"
- name: dotnet pack
run: |
dotnet pack -p:ContinuousIntegrationBuild=true

478
.gitignore vendored Normal file
View file

@ -0,0 +1,478 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# 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
*.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/*
!**/src/packages/*
# 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
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/master/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/master/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

27
Directory.Build.props Normal file
View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<VersionPrefix>1.0.0</VersionPrefix>
<!--<VersionSuffix></VersionSuffix>-->
</PropertyGroup>
<PropertyGroup>
<AssemblyName>Geekeey.Extensions.$(MSBuildProjectName)</AssemblyName>
<RootNamespace>Geekeey.Extensions.$(MSBuildProjectName)</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<Authors>The Geekeey Team</Authors>
<Description>A simple and lightweight result type implementation for C#.</Description>
<PackageTags>geekeey utility result</PackageTags>
<PackageReadmeFile>package-readme.md</PackageReadmeFile>
</PropertyGroup>
</Project>

3
Directory.Build.targets Normal file
View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
</Project>

13
Directory.Packages.props Normal file
View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.1" />
<PackageVersion Include="NUnit" Version="3.14.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="NUnit.Analyzers" Version="4.1.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.1" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,30 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Result", "src\Result\Result.csproj", "{5F1B824C-659D-4A1A-B538-862788681437}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Result.Tests", "src\Result.Tests\Result.Tests.csproj", "{1DC64E48-B1AF-4426-A336-65F89AD10591}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5F1B824C-659D-4A1A-B538-862788681437}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F1B824C-659D-4A1A-B538-862788681437}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F1B824C-659D-4A1A-B538-862788681437}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F1B824C-659D-4A1A-B538-862788681437}.Release|Any CPU.Build.0 = Release|Any CPU
{1DC64E48-B1AF-4426-A336-65F89AD10591}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1DC64E48-B1AF-4426-A336-65F89AD10591}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1DC64E48-B1AF-4426-A336-65F89AD10591}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1DC64E48-B1AF-4426-A336-65F89AD10591}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
EndGlobal

9
global.json Normal file
View file

@ -0,0 +1,9 @@
{
"sdk": {
"version": "8.0.0",
"rollForward": "latestMajor",
"allowPrerelease": true
},
"msbuild-sdks": {
}
}

19
nuget.config Normal file
View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<config>
<add key="defaultPushSource" value="geekeey" />
</config>
<packageSources>
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="geekeey" value="https://git.geekeey.de/api/packages/geekeey/nuget/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget">
<package pattern="*" />
</packageSource>
<packageSource key="geekeey">
<package pattern="Geekeey.*" />
</packageSource>
</packageSourceMapping>
</configuration>

View file

@ -0,0 +1,30 @@
namespace Geekeey.Extensions.Result.Tests;
[TestFixture]
internal sealed class ErrorTests
{
[Test]
public void ImplicitConversion_FromString_ReturnsStringError()
{
Error error = "error";
Assert.Multiple(() =>
{
Assert.That(error, Is.InstanceOf<StringError>());
Assert.That(error.Message, Is.EqualTo("error"));
});
}
[Test]
public void ImplicitConversion_FromException_ReturnsExceptionError()
{
Error error = new CustomTestException();
Assert.Multiple(() =>
{
Assert.That(error, Is.InstanceOf<ExceptionError>());
var instance = error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
}

View file

@ -0,0 +1,54 @@
namespace Geekeey.Extensions.Result.Tests;
[TestFixture]
internal sealed class ExtensionsEnumerableTests
{
[Test]
public void Join_ReturnsAllSuccess_ForSequenceContainingAllSuccess()
{
IEnumerable<Result<int>> xs = [1, 2, 3, 4, 5];
var result = xs.Join();
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EquivalentTo(new[] { 1, 2, 3, 4, 5 }));
});
}
[Test]
public void Join_ReturnsFirstFailure_ForSequenceContainingFailure()
{
IEnumerable<Result<int>> xs =
[
Success(1),
Success(2),
Failure<int>("error 1"),
Success(4),
Failure<int>("error 2")
];
var result = xs.Join();
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error 1"));
});
}
[Test]
public void Join_ReturnsSuccess_ForEmptySequence()
{
IEnumerable<Result<int>> xs = [];
var result = xs.Join();
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.Empty);
});
}
}

View file

@ -0,0 +1,7 @@
namespace Geekeey.Extensions.Result.Tests;
internal sealed class CustomTestError : Error
{
internal const string DefaultMessage = "This is a custom error for test";
public override string Message => DefaultMessage;
}

View file

@ -0,0 +1,5 @@
namespace Geekeey.Extensions.Result.Tests;
internal sealed class CustomTestException : Exception
{
}

View file

@ -0,0 +1,119 @@
namespace Geekeey.Extensions.Result.Tests;
[TestFixture]
internal sealed class PreludeTests
{
[Test]
public void Try_ReturnsSuccess_WithoutThrowing()
{
var result = Try(() => 2);
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo(2));
});
}
[Test]
public void Try_ReturnsFailure_WithThrowing()
{
var result = Try<int>(() => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public async Task TryAsync_ReturnsSuccess_WithoutThrowing_Task()
{
var result = await TryAsync(() => Task.FromResult(2));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo(2));
});
}
[Test]
public async Task TryAsync_ReturnsFailure_WithThrowing_Task()
{
var result = await TryAsync(Task<int> () => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public async Task TryAsync_ReturnsFailure_WithAwaitThrowing_Task()
{
var result = await TryAsync(async Task<int> () =>
{
await Task.CompletedTask;
throw new CustomTestException();
});
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public async Task TryAsync_ReturnsSuccess_WithoutThrowing_ValueTask()
{
var result = await TryAsync(() => ValueTask.FromResult(2));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo(2));
});
}
[Test]
public async Task TryAsync_ReturnsFailure_WithThrowing_ValueTask()
{
var result = await TryAsync(ValueTask<int> () => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public async Task TryAsync_ReturnsFailure_WithAwaitThrowing_ValueTask()
{
var result = await TryAsync(async ValueTask<int> () =>
{
await Task.CompletedTask;
throw new CustomTestException();
});
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
}

View file

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<Using Include="NUnit.Framework" />
<Using Include="Geekeey.Extensions.Result.Prelude" Static="true" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NUnit" />
<PackageReference Include="NUnit3TestAdapter" />
<PackageReference Include="NUnit.Analyzers" />
<PackageReference Include="coverlet.collector" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Result\Result.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,66 @@
namespace Geekeey.Extensions.Result.Tests;
[TestFixture]
internal sealed class ResultConversionTests
{
[Test]
public void ImplicitConversion_FromValue_IsSuccess()
{
var result = Success(2);
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.IsFailure, Is.False);
Assert.That(result.Value, Is.EqualTo(2));
});
}
[Test]
public void ImplicitConversion_FromError_IsFailure()
{
var error = new CustomTestError();
var result = Failure<int>(error);
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.IsFailure, Is.True);
Assert.That(result.Error, Is.InstanceOf<CustomTestError>());
});
}
[Test]
public void Unwrap_ReturnsValue_ForSuccess()
{
var result = Success(2);
var value = result.Unwrap();
Assert.That(value, Is.EqualTo(2));
}
[Test]
public void Unwrap_Throws_ForFailure()
{
var result = Failure<int>("error");
Assert.That(() => result.Unwrap(), Throws.InstanceOf<UnwrapException>());
}
[Test]
public void ExplicitConversion_ReturnsValue_ForSuccess()
{
var result = Success(2);
var value = (int)result;
Assert.That(value, Is.EqualTo(2));
}
[Test]
public void ExplicitConversion_Throws_ForFailure()
{
var result = Failure<int>("error");
Assert.That(() => (int)result, Throws.InstanceOf<UnwrapException>());
}
}

View file

@ -0,0 +1,173 @@
namespace Geekeey.Extensions.Result.Tests;
[TestFixture]
internal sealed class ResultEqualityTests
{
[Test]
public void Equals_T_ReturnsTrue_ForSuccessAndEqualValue()
{
var a = Success(2);
var b = 2;
Assert.That(a.Equals(b), Is.True);
}
[Test]
public void Equals_T_ReturnsFalse_ForSuccessAndUnequalValue()
{
var a = Success(2);
var b = 3;
Assert.That(a.Equals(b), Is.False);
}
[Test]
public void Equals_T_ReturnsFalse_ForFailure()
{
var a = Failure<int>("error");
var b = 2;
Assert.That(a.Equals(b), Is.False);
}
[Test]
public void Equals_Result_ReturnsTrue_ForSuccessAndSuccessAndEqualValue()
{
var a = Success(2);
var b = Success(2);
Assert.That(a.Equals(b), Is.True);
}
[Test]
public void Equals_Result_ReturnsFalse_ForSuccessAndSuccessAndUnequalValue()
{
var a = Success(2);
var b = Success(3);
Assert.That(a.Equals(b), Is.False);
}
[Test]
public void Equals_Result_ReturnsFalse_ForSuccessAndFailure()
{
var a = Success(2);
var b = Failure<int>("error 1");
Assert.That(a.Equals(b), Is.False);
}
[Test]
public void Equals_Result_ReturnsFalse_ForFailureAndSuccess()
{
var a = Failure<int>("error");
var b = Success(2);
Assert.That(a.Equals(b), Is.False);
}
[Test]
public void Equals_Result_ReturnsTrue_ForFailureAndFailure()
{
var a = Failure<int>("error 1");
var b = Failure<int>("error 2");
Assert.That(a.Equals(b), Is.True);
}
[Test]
public void Equals_T_ReturnsTrue_ForSuccessAndEqualValue_WithComparer()
{
var a = Success(2);
var b = 2;
Assert.That(a.Equals(b, EqualityComparer<int>.Default), Is.True);
}
[Test]
public void Equals_T_ReturnsFalse_ForSuccessAndUnequalValue_WithComparer()
{
var a = Success(2);
var b = 3;
Assert.That(a.Equals(b, EqualityComparer<int>.Default), Is.False);
}
[Test]
public void Equals_T_ReturnsFalse_ForFailure_WithComparer()
{
var a = Failure<int>("error");
var b = 2;
Assert.That(a.Equals(b, EqualityComparer<int>.Default), Is.False);
}
[Test]
public void Equals_Result_ReturnsTrue_ForSuccessAndSuccessAndEqualValue_WithComparer()
{
var a = Success(2);
var b = Success(2);
Assert.That(a.Equals(b, EqualityComparer<int>.Default), Is.True);
}
[Test]
public void Equals_Result_ReturnsFalse_ForSuccessAndSuccessAndUnequalValue_WithComparer()
{
var a = Success(2);
var b = Success(3);
Assert.That(a.Equals(b, EqualityComparer<int>.Default), Is.False);
}
[Test]
public void Equals_Result_ReturnsFalse_ForSuccessAndFailure_WithComparer()
{
var a = Success(2);
var b = Failure<int>("error 1");
Assert.That(a.Equals(b, EqualityComparer<int>.Default), Is.False);
}
[Test]
public void Equals_Result_ReturnsFalse_ForFailureAndSuccess_WithComparer()
{
var a = Failure<int>("error");
var b = Success(2);
Assert.That(a.Equals(b, EqualityComparer<int>.Default), Is.False);
}
[Test]
public void Equals_Result_ReturnsTrue_ForFailureAndFailure_WithComparer()
{
var a = Failure<int>("error 1");
var b = Failure<int>("error 2");
Assert.That(a.Equals(b, EqualityComparer<int>.Default), Is.True);
}
[Test]
public void GetHashCode_ReturnsHashCode_ForSuccess()
{
var result = Success(2);
Assert.That(result.GetHashCode(), Is.EqualTo(2.GetHashCode()));
}
[Test]
public void GetHashCode_Returns_Zero_ForNull()
{
var result = Success<string?>(null);
Assert.That(result.GetHashCode(), Is.Zero);
}
[Test]
public void GetHashCode_Returns_Zero_ForFailure()
{
var result = Failure<int>("error");
Assert.That(result.GetHashCode(), Is.Zero);
}
}

View file

@ -0,0 +1,183 @@
namespace Geekeey.Extensions.Result.Tests;
[TestFixture]
internal sealed class ResultMatchingTests
{
[Test]
public void Match_CallsSuccessFunc_ForSuccess()
{
var result = Success(2);
var match = result.Match(
v => v,
_ => throw new InvalidOperationException());
Assert.That(match, Is.EqualTo(2));
}
[Test]
public void Match_CallsFailureFunc_ForFailure()
{
var result = Failure<int>("error");
var match = result.Match(
_ => throw new InvalidOperationException(),
e => e);
Assert.That(match.Message, Is.EqualTo("error"));
}
[Test]
public void Switch_CallsSuccessAction_ForSuccess()
{
var called = false;
var result = Success(2);
result.Switch(
v =>
{
Assert.That(v, Is.EqualTo(2));
called = true;
},
_ => throw new InvalidOperationException()
);
Assert.That(called, Is.True);
}
[Test]
public void Switch_CallsFailureAction_ForFailure()
{
var called = false;
var result = Failure<int>("error");
result.Switch(
_ => throw new InvalidOperationException(),
e =>
{
called = true;
Assert.That(e.Message, Is.EqualTo("error"));
}
);
Assert.That(called, Is.True);
}
[Test]
public async Task MatchAsync_CallsSuccessFunc_ForSuccess_Task()
{
var result = Success(2);
var match = await result.MatchAsync(
Task.FromResult,
_ => throw new InvalidOperationException());
Assert.That(match, Is.EqualTo(2));
}
[Test]
public async Task MatchAsync_CallsFailureFunc_ForFailure_Task()
{
var result = Failure<int>("error");
var match = await result.MatchAsync(
_ => throw new InvalidOperationException(),
Task.FromResult);
Assert.That(match.Message, Is.EqualTo("error"));
}
[Test]
public async Task SwitchAsync_CallsSuccessAction_ForSuccess_Task()
{
var called = false;
var result = Success(2);
await result.SwitchAsync(
v =>
{
Assert.That(v, Is.EqualTo(2));
called = true;
return Task.CompletedTask;
},
_ => throw new InvalidOperationException()
);
Assert.That(called, Is.True);
}
[Test]
public async Task SwitchAsync_CallsFailureAction_ForFailure_Task()
{
var called = false;
var result = Failure<int>("error");
await result.SwitchAsync(
_ => throw new InvalidOperationException(),
e =>
{
called = true;
Assert.That(e.Message, Is.EqualTo("error"));
return Task.CompletedTask;
}
);
Assert.That(called, Is.True);
}
[Test]
public async Task MatchAsync_CallsSuccessFunc_ForSuccess_ValueTask()
{
var result = Success(2);
var match = await result.MatchAsync(
ValueTask.FromResult,
_ => throw new InvalidOperationException());
Assert.That(match, Is.EqualTo(2));
}
[Test]
public async Task MatchAsync_CallsFailureFunc_ForFailure_ValueTask()
{
var result = Failure<int>("error");
var match = await result.MatchAsync(
_ => throw new InvalidOperationException(),
ValueTask.FromResult);
Assert.That(match.Message, Is.EqualTo("error"));
}
[Test]
public async Task SwitchAsync_CallsSuccessAction_ForSuccess_ValueTask()
{
var called = false;
var result = Success(2);
await result.SwitchAsync(
v =>
{
Assert.That(v, Is.EqualTo(2));
called = true;
return ValueTask.CompletedTask;
},
_ => throw new InvalidOperationException()
);
Assert.That(called, Is.True);
}
[Test]
public async Task SwitchAsync_CallsFailureAction_ForFailure_ValueTask()
{
var called = false;
var result = Failure<int>("error");
await result.SwitchAsync(
_ => throw new InvalidOperationException(),
e =>
{
called = true;
Assert.That(e.Message, Is.EqualTo("error"));
return ValueTask.CompletedTask;
}
);
Assert.That(called, Is.True);
}
}

View file

@ -0,0 +1,63 @@
namespace Geekeey.Extensions.Result.Tests;
[TestFixture]
internal sealed class ResultTests
{
[Test]
public void New_T_HasValue()
{
var result = new Result<int>(1);
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.IsFailure, Is.False);
Assert.That(result.Value, Is.Not.EqualTo(default(int)));
Assert.That(result.Error, Is.Null);
});
}
[Test]
public void New_Error_HasError()
{
var result = new Result<int>(new CustomTestError());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.IsFailure, Is.True);
Assert.That(result.Value, Is.EqualTo(default(int)));
Assert.That(result.Error, Is.InstanceOf<CustomTestError>());
});
}
[Test]
public void Default_IsDefault()
{
var result = default(Result<int>);
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.IsFailure, Is.True);
Assert.That(result.Value, Is.EqualTo(default(int)));
Assert.That(result.Error, Is.EqualTo(Error.DefaultValueError));
});
}
[Test]
public void ToString_ReturnsSuccessString()
{
Result<int> result = 2;
Assert.That(result.ToString(), Is.EqualTo("Success { 2 }"));
}
[Test]
public void ToString_ReturnsFailureString()
{
Result<int> result = new StringError("error");
Assert.That(result.ToString(), Is.EqualTo("Failure { error }"));
}
}

View file

@ -0,0 +1,756 @@
namespace Geekeey.Extensions.Result.Tests;
[TestFixture]
internal sealed class ResultTransformTests
{
[Test]
public void Map_ReturnsSuccess_ForSuccess()
{
var start = Success(2);
var result = start.Map(value => value.ToString());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo("2"));
});
}
[Test]
public void Map_ReturnsFailure_ForFailure()
{
var start = Failure<int>("error");
var result = start.Map(value => value.ToString());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public void Then_ReturnsSuccess_ForSuccessAndMappingReturningSuccess()
{
var start = Success(2);
var result = start.Then(value => Success(value.ToString()));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo("2"));
});
}
[Test]
public void Then_ReturnsFailure_ForSuccessAndMappingReturningFailure()
{
var start = Success(2);
var result = start.Then(_ => Failure<string>("error"));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public void Then_ReturnsFailure_ForFailureAndMappingReturningSuccess()
{
var start = Failure<int>("error");
var result = start.Then(value => Success(value.ToString()));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public void Then_ReturnsFailure_ForFailureAndMappingReturningFailure()
{
var start = Failure<int>("error");
var result = start.Then(_ => Failure<int>("error 2"));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public void TryMap_ReturnsSuccess_ForSuccessWithoutThrowing()
{
var start = Success(2);
var result = start.TryMap(value => value.ToString());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo("2"));
});
}
[Test]
public void TryMap_ReturnsFailure_ForFailureWithoutThrowing()
{
var start = Failure<int>("error");
var result = start.TryMap(value => value.ToString());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public void TryMap_ReturnsFailure_ForSuccessWithThrowing()
{
var start = Success(2);
var result = start.TryMap<string>(_ => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public void TryMap_ReturnsFailure_ForFailureWithThrowing()
{
var start = Failure<int>("error");
var result = start.TryMap<string>(_ => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<StringError>());
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public void ThenTry_ReturnsSuccess_ForSuccessAndMappingReturningSuccess()
{
var start = Success(2);
var result = start.ThenTry(value => Success(value.ToString()));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo("2"));
});
}
[Test]
public void ThenTry_ReturnsFailure_ForSuccessAndMappingReturningFailure()
{
var start = Success(2);
var result = start.ThenTry(_ => Failure<string>("error"));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public void ThenTry_ReturnsFailure_ForFailureAndMappingReturningFailure()
{
var start = Failure<int>("error");
var result = start.ThenTry(x => Success(x.ToString()));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public void ThenTry_ReturnsFailure_ForSuccessAndMappingThrowing()
{
var start = Success(2);
var result = start.ThenTry<string>(_ => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public void ThenTry_ReturnsFailure_ForFailureAndMappingThrowing()
{
var start = Failure<int>("error");
var result = start.ThenTry<string>(_ => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<StringError>());
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task MapAsync_ReturnsSuccess_ForSuccess_Task()
{
var start = Success(2);
var result = await start.MapAsync(value => Task.FromResult(value.ToString()));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo("2"));
});
}
[Test]
public async Task MapAsync_ReturnsFailure_ForFailure_Task()
{
var start = Failure<int>("error");
var result = await start.MapAsync(value => Task.FromResult(value.ToString()));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenAsync_ReturnsSuccess_ForSuccessAndMappingReturningSuccess_Task()
{
var start = Success(2);
var result = await start.ThenAsync(value => Task.FromResult(Success(value.ToString())));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo("2"));
});
}
[Test]
public async Task ThenAsync_ReturnsFailure_ForSuccessAndMappingReturningFailure_Task()
{
var start = Success(2);
var result = await start.ThenAsync(_ => Task.FromResult(Failure<string>("error")));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenAsync_ReturnsFailure_ForFailureAndMappingReturningSuccess_Task()
{
var start = Failure<int>("error");
var result = await start.ThenAsync(value => Task.FromResult(Success(value.ToString())));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenAsync_ReturnsFailure_ForFailureAndMappingReturningFailure_Task()
{
var start = Failure<int>("error");
var result = await start.ThenAsync(_ => Task.FromResult(Failure<int>("error 2")));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task TryMapAsync_ReturnsSuccess_ForSuccessWithoutThrowing_Task()
{
var start = Success(2);
var result = await start.TryMapAsync(value => Task.FromResult(value.ToString()));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo("2"));
});
}
[Test]
public async Task TryMapAsync_ReturnsFailure_ForFailureWithoutThrowing_Task()
{
var start = Failure<int>("error");
var result = await start.TryMapAsync(value => Task.FromResult(value.ToString()));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task TryMapAsync_ReturnsFailure_ForSuccessWithThrowing_Task()
{
var start = Success(2);
var result = await start.TryMapAsync(Task<string> (_) => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public async Task TryMapAsync_ReturnsFailure_ForSuccessWithAwaitThrowing_Task()
{
var start = Success(2);
var result = await start.TryMapAsync(async Task<string> (_) =>
{
await Task.CompletedTask;
throw new CustomTestException();
});
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public async Task TryMapAsync_ReturnsFailure_ForFailureWithThrowing_Task()
{
var start = Failure<int>("error");
var result = await start.TryMapAsync(Task<string> (_) => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<StringError>());
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task TryMapAsync_ReturnsFailure_ForFailureWithAwaitThrowing_Task()
{
var start = Failure<int>("error");
var result = await start.TryMapAsync(async Task<string> (_) =>
{
await Task.CompletedTask;
throw new CustomTestException();
});
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<StringError>());
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenTryAsync_ReturnsSuccess_ForSuccessAndMappingReturningSuccess_Task()
{
var start = Success(2);
var result = await start.ThenTryAsync(value => Task.FromResult(Success(value.ToString())));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo("2"));
});
}
[Test]
public async Task ThenTryAsync_ReturnsFailure_ForSuccessAndMappingReturningFailure_Task()
{
var start = Success(2);
var result = await start.ThenTryAsync(_ => Task.FromResult(Failure<string>("error")));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenTryAsync_ReturnsFailure_ForFailureAndMappingReturningFailure_Task()
{
var start = Failure<int>("error");
var result = await start.ThenTryAsync(x => Task.FromResult(Success(x.ToString())));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenTryAsync_ReturnsFailure_ForSuccessAndMappingThrowing_Task()
{
var start = Success(2);
var result = await start.ThenTryAsync(Task<Result<string>> (_) => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public async Task ThenTryAsync_ReturnsFailure_ForSuccessAndMappingAwaitThrowing_Task()
{
var start = Success(2);
var result = await start.ThenTryAsync(async Task<Result<string>> (_) =>
{
await Task.CompletedTask;
throw new CustomTestException();
});
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public async Task ThenTryAsync_ReturnsFailure_ForFailureAndMappingThrowing_Task()
{
var start = Failure<int>("error");
var result = await start.ThenTryAsync(Task<Result<string>> (_) => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<StringError>());
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenTryAsync_ReturnsFailure_ForFailureAndMappingAwaitThrowing_Task()
{
var start = Failure<int>("error");
var result = await start.ThenTryAsync(async Task<Result<string>> (_) =>
{
await Task.CompletedTask;
throw new CustomTestException();
});
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<StringError>());
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task MapAsync_ReturnsSuccess_ForSuccess_ValueTask()
{
var start = Success(2);
var result = await start.MapAsync(value => ValueTask.FromResult(value.ToString()));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo("2"));
});
}
[Test]
public async Task MapAsync_ReturnsFailure_ForFailure_ValueTask()
{
var start = Failure<int>("error");
var result = await start.MapAsync(value => ValueTask.FromResult(value.ToString()));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenAsync_ReturnsSuccess_ForSuccessAndMappingReturningSuccess_ValueTask()
{
var start = Success(2);
var result = await start.ThenAsync(value => ValueTask.FromResult(Success(value.ToString())));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo("2"));
});
}
[Test]
public async Task ThenAsync_ReturnsFailure_ForSuccessAndMappingReturningFailure_ValueTask()
{
var start = Success(2);
var result = await start.ThenAsync(_ => ValueTask.FromResult(Failure<string>("error")));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenAsync_ReturnsFailure_ForFailureAndMappingReturningSuccess_ValueTask()
{
var start = Failure<int>("error");
var result = await start.ThenAsync(value => ValueTask.FromResult(Success(value.ToString())));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenAsync_ReturnsFailure_ForFailureAndMappingReturningFailure_ValueTask()
{
var start = Failure<int>("error");
var result = await start.ThenAsync(_ => ValueTask.FromResult(Failure<int>("error 2")));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task TryMapAsync_ReturnsSuccess_ForSuccessWithoutThrowing_ValueTask()
{
var start = Success(2);
var result = await start.TryMapAsync(value => ValueTask.FromResult(value.ToString()));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo("2"));
});
}
[Test]
public async Task TryMapAsync_ReturnsFailure_ForFailureWithoutThrowing_ValueTask()
{
var start = Failure<int>("error");
var result = await start.TryMapAsync(value => ValueTask.FromResult(value.ToString()));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task TryMapAsync_ReturnsFailure_ForSuccessWithThrowing_ValueTask()
{
var start = Success(2);
var result = await start.TryMapAsync(ValueTask<string> (_) => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public async Task TryMapAsync_ReturnsFailure_ForSuccessWithAwaitThrowing_ValueTask()
{
var start = Success(2);
var result = await start.TryMapAsync(async ValueTask<string> (_) =>
{
await ValueTask.CompletedTask;
throw new CustomTestException();
});
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public async Task TryMapAsync_ReturnsFailure_ForFailureWithThrowing_ValueTask()
{
var start = Failure<int>("error");
var result = await start.TryMapAsync(ValueTask<string> (_) => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<StringError>());
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task TryMapAsync_ReturnsFailure_ForFailureWithAwaitThrowing_ValueTask()
{
var start = Failure<int>("error");
var result = await start.TryMapAsync(async ValueTask<string> (_) =>
{
await ValueTask.CompletedTask;
throw new CustomTestException();
});
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<StringError>());
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenTryAsync_ReturnsSuccess_ForSuccessAndMappingReturningSuccess_ValueTask()
{
var start = Success(2);
var result = await start.ThenTryAsync(value => ValueTask.FromResult(Success(value.ToString())));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Value, Is.EqualTo("2"));
});
}
[Test]
public async Task ThenTryAsync_ReturnsFailure_ForSuccessAndMappingReturningFailure_ValueTask()
{
var start = Success(2);
var result = await start.ThenTryAsync(_ => ValueTask.FromResult(Failure<string>("error")));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenTryAsync_ReturnsFailure_ForFailureAndMappingReturningFailure_ValueTask()
{
var start = Failure<int>("error");
var result = await start.ThenTryAsync(x => ValueTask.FromResult(Success(x.ToString())));
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenTryAsync_ReturnsFailure_ForSuccessAndMappingThrowing_ValueTask()
{
var start = Success(2);
var result = await start.ThenTryAsync(ValueTask<Result<string>> (_) => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public async Task ThenTryAsync_ReturnsFailure_ForSuccessAndMappingAwaitThrowing_ValueTask()
{
var start = Success(2);
var result = await start.ThenTryAsync(async ValueTask<Result<string>> (_) =>
{
await ValueTask.CompletedTask;
throw new CustomTestException();
});
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<ExceptionError>());
var instance = result.Error as ExceptionError;
Assert.That(instance?.Exception, Is.InstanceOf<CustomTestException>());
});
}
[Test]
public async Task ThenTryAsync_ReturnsFailure_ForFailureAndMappingThrowing_ValueTask()
{
var start = Failure<int>("error");
var result = await start.ThenTryAsync(ValueTask<Result<string>> (_) => throw new CustomTestException());
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<StringError>());
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
[Test]
public async Task ThenTryAsync_ReturnsFailure_ForFailureAndMappingAwaitThrowing_ValueTask()
{
var start = Failure<int>("error");
var result = await start.ThenTryAsync(async ValueTask<Result<string>> (_) =>
{
await ValueTask.CompletedTask;
throw new CustomTestException();
});
Assert.Multiple(() =>
{
Assert.That(result.IsSuccess, Is.False);
Assert.That(result.Error, Is.InstanceOf<StringError>());
Assert.That(result.Error?.Message, Is.EqualTo("error"));
});
}
}

View file

@ -0,0 +1,113 @@
namespace Geekeey.Extensions.Result.Tests;
[TestFixture]
internal sealed class ResultUnboxTests
{
[Test]
public void TryGetValue_1_ReturnsTrueAndSetsValue_ForSuccess()
{
var result = Success(2);
var ok = result.TryGetValue(out var value);
Assert.Multiple(() =>
{
Assert.That(ok, Is.True);
Assert.That(value, Is.EqualTo(2));
});
}
[Test]
public void TryGetValue_1_ReturnsFalse_ForFailure()
{
var result = Failure<int>("error");
var ok = result.TryGetValue(out var value);
Assert.Multiple(() =>
{
Assert.That(ok, Is.False);
Assert.That(value, Is.EqualTo(default(int)));
});
}
[Test]
public void TryGetValue_2_ReturnsTrueAndSetsValue_ForSuccess()
{
var result = Success(2);
var ok = result.TryGetValue(out var value, out var error);
Assert.Multiple(() =>
{
Assert.That(ok, Is.True);
Assert.That(value, Is.EqualTo(2));
Assert.That(error, Is.EqualTo(default(Error)));
});
}
[Test]
public void TryGetValue_2_ReturnsFalseAndSetsError_ForFailure()
{
var result = Failure<int>("error");
var ok = result.TryGetValue(out var value, out var error);
Assert.Multiple(() =>
{
Assert.That(ok, Is.False);
Assert.That(value, Is.EqualTo(default(int)));
Assert.That(error?.Message, Is.EqualTo("error"));
});
}
[Test]
public void TryGetError_1_ReturnsTrueAndSetsError_ForFailure()
{
var result = Failure<int>("error");
var ok = result.TryGetError(out var error);
Assert.Multiple(() =>
{
Assert.That(ok, Is.True);
Assert.That(error?.Message, Is.EqualTo("error"));
});
}
[Test]
public void TryGetError_1_ReturnsFalse_ForSuccess()
{
var result = Success(2);
var ok = result.TryGetError(out var error);
Assert.Multiple(() =>
{
Assert.That(ok, Is.False);
Assert.That(error, Is.EqualTo(default(Error)));
});
}
[Test]
public void TryGetError_2_ReturnsTrueAndSetsError_ForFailure()
{
var r = Failure<int>("error");
var ok = r.TryGetError(out var error, out var value);
Assert.Multiple(() =>
{
Assert.That(ok, Is.True);
Assert.That(error?.Message, Is.EqualTo("error"));
Assert.That(value, Is.EqualTo(default(int)));
});
}
[Test]
public void TryGetError_2_ReturnsFalseAndSetsValue_ForSuccess()
{
var r = Success(2);
var ok = r.TryGetError(out var error, out var value);
Assert.Multiple(() =>
{
Assert.That(ok, Is.False);
Assert.That(error, Is.EqualTo(default(Error)));
Assert.That(value, Is.EqualTo(2));
});
}
}

View file

@ -0,0 +1,16 @@
namespace Geekeey.Extensions.Result;
/// <summary>
/// An error which is a combination of other errors.
/// </summary>
/// <param name="errors">The errors the error consists of.</param>
public sealed class AggregateError(IEnumerable<Error> errors) : Error
{
/// <summary>
/// The errors the error consists of.
/// </summary>
public IReadOnlyCollection<Error> Errors { get; } = errors.ToList();
/// <inheritdoc/>
public override string Message => string.Join(Environment.NewLine, Errors.Select(error => error.Message));
}

View file

@ -0,0 +1,44 @@
namespace Geekeey.Extensions.Result;
/// <summary>
/// An error containing a simple message. Makes up the other half of a <see cref="Result{T}"/> which might be an error.
/// </summary>
/// <remarks>
/// An error is conceptually very similar to an exception but without the ability to be thrown, meant to be a more
/// lightweight type meant to be wrapped in a <see cref="Result{T}"/>.
/// An error fundamentally only contains a single string message, however other more concrete types such as
/// <see cref="ExceptionError"/> or <see cref="AggregateError"/> may define other properties.
/// Errors are meant to be small, specific, and descriptive, such that they are easy to match over and provide specific
/// handling for specific kinds of errors.
/// </remarks>
public abstract class Error
{
/// <summary>
/// A statically accessible default "Result has no value." error.
/// </summary>
internal static Error DefaultValueError { get; } = new StringError("The result has no value.");
/// <summary>
/// The message used to display the error.
/// </summary>
public abstract string Message { get; }
/// <summary>
/// Gets a string representation of the error. Returns <see cref="Message"/> by default.
/// </summary>
public override string ToString() => Message;
/// <summary>
/// Implicitly converts a string into a <see cref="StringError"/>.
/// </summary>
/// <param name="message">The message of the error.</param>
public static implicit operator Error(string message)
=> new StringError(message);
/// <summary>
/// Implicitly converts an exception into an <see cref="ExceptionError"/>.
/// </summary>
/// <param name="exception">The exception to convert.</param>
public static implicit operator Error(Exception exception)
=> new ExceptionError(exception);
}

View file

@ -0,0 +1,18 @@
namespace Geekeey.Extensions.Result;
/// <summary>
/// An error which is constructed from an exception.
/// </summary>
/// <param name="exception">The exception in the error.</param>
public sealed class ExceptionError(Exception exception) : Error
{
/// <summary>
/// The exception in the error.
/// </summary>
public Exception Exception { get; } = exception;
/// <summary>
/// The exception in the error.
/// </summary>
public override string Message => Exception.Message;
}

View file

@ -0,0 +1,11 @@
namespace Geekeey.Extensions.Result;
/// <summary>
/// An error which displays a simple string.
/// </summary>
/// <param name="message">The message to display.</param>
public sealed class StringError(string message) : Error
{
/// <inheritdoc/>
public override string Message => message;
}

View file

@ -0,0 +1,18 @@
namespace Geekeey.Extensions.Result;
/// <summary>
/// The exception is thrown when an <see cref="Result{T}"/> is attempted to be unwrapped contains only a failure value.
/// </summary>
public sealed class UnwrapException : Exception
{
/// <summary>
/// Creates a new <see cref="UnwrapException"/>.
/// </summary>
public UnwrapException() : base("Cannot unwrap result because it does not have a value.") { }
/// <summary>
/// Creates a new <see cref="UnwrapException"/>.
/// </summary>
/// <param name="error">An error message.</param>
public UnwrapException(string error) : base(error) { }
}

View file

@ -0,0 +1,37 @@
namespace Geekeey.Extensions.Result;
/// <summary>
/// Extensions for or relating to <see cref="Result{T}"/>.
/// </summary>
public static partial class Extensions
{
/// <summary>
/// Turns a sequence of results into a single result containing the success values in the results only if all the
/// results have success values.
/// </summary>
/// <param name="results">The results to turn into a single sequence.</param>
/// <typeparam name="T">The type of the success values in the results.</typeparam>
/// <returns>A single result containing a sequence of all the success values from the original sequence of results,
/// or the first failure value encountered within the sequence.</returns>
/// <remarks>
/// This method completely enumerates the input sequence before returning and is not lazy. As a consequence of this,
/// the sequence within the returned result is an <see cref="IReadOnlyList{T}"/>.
/// </remarks>
public static Result<IReadOnlyList<T>> Join<T>(this IEnumerable<Result<T>> results)
{
_ = results.TryGetNonEnumeratedCount(out var count);
var list = new List<T>(count);
foreach (var result in results)
{
if (!result.TryGetValue(out var value, out var error))
{
return new Result<IReadOnlyList<T>>(error);
}
list.Add(value);
}
return list;
}
}

View file

@ -0,0 +1,85 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace Geekeey.Extensions.Result;
/// <summary>
/// Extensions for or relating to <see cref="Result{T}"/>.
/// </summary>
[ExcludeFromCodeCoverage]
public static partial class Extensions
{
#region Task<Result<T>>
/// <summary>
/// Maps the success value of the result object of the completed task using a mapping function, or does nothing if
/// the result object of the completed task is a failure.
/// </summary>
/// <param name="result">A task object retunring a result object when completing.</param>
/// <param name="func">The function used to map the success value.</param>
/// <typeparam name="T">The type of the object inside the result returned by the task.</typeparam>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A new result containing either the mapped success value or the failure value of the original
/// result.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async ValueTask<Result<TNew>> Map<T, TNew>(this Task<Result<T>> result,
Func<T, TNew> func)
=> (await result).Map(func);
/// <inheritdoc cref="Map{T,TNew}(System.Threading.Tasks.Task{Geekeey.Extensions.Result.Result{T}},System.Func{T,TNew})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async Task<Result<TNew>> Map<T, TNew>(this Task<Result<T>> result,
Func<T, ValueTask<TNew>> func)
=> await (await result).MapAsync(func);
/// <summary>
/// Maps the success value of the result object of the completed task to a new result using a mapping function, or
/// does nothing if the result object of the completed task is a failure.
/// </summary>
/// <param name="result">A task object retunring a result object when completing.</param>
/// <param name="func">The function used to map the success value.</param>
/// <typeparam name="T">The type of the object inside the result returned by the task.</typeparam>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A new result containing either the mapped success value or the failure value of the original
/// result.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async Task<Result<TNew>> Then<T, TNew>(this Task<Result<T>> result,
Func<T, Result<TNew>> func)
=> (await result).Then(func);
/// <inheritdoc cref="Then{T,TNew}(System.Threading.Tasks.Task{Geekeey.Extensions.Result.Result{T}},System.Func{T,Geekeey.Extensions.Result.Result{TNew}})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async Task<Result<TNew>> Then<T, TNew>(this Task<Result<T>> result,
Func<T, ValueTask<Result<TNew>>> func)
=> await (await result).ThenAsync(func);
#endregion
#region ValueTask<Result<T>>
/// <inheritdoc cref="Map{T,TNew}(System.Threading.Tasks.Task{Geekeey.Extensions.Result.Result{T}},System.Func{T,TNew})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async Task<Result<TNew>> Map<T, TNew>(this ValueTask<Result<T>> result,
Func<T, TNew> func)
=> (await result).Map(func);
/// <inheritdoc cref="Map{T,TNew}(System.Threading.Tasks.Task{Geekeey.Extensions.Result.Result{T}},System.Func{T,TNew})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async ValueTask<Result<TNew>> Map<T, TNew>(this ValueTask<Result<T>> result,
Func<T, ValueTask<TNew>> func)
=> await (await result).MapAsync(func);
/// <inheritdoc cref="Then{T,TNew}(System.Threading.Tasks.Task{Geekeey.Extensions.Result.Result{T}},System.Func{T,Geekeey.Extensions.Result.Result{TNew}})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async ValueTask<Result<TNew>> Then<T, TNew>(this ValueTask<Result<T>> result,
Func<T, Result<TNew>> func)
=> (await result).Then(func);
/// <inheritdoc cref="Then{T,TNew}(System.Threading.Tasks.Task{Geekeey.Extensions.Result.Result{T}},System.Func{T,Geekeey.Extensions.Result.Result{TNew}})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async ValueTask<Result<TNew>> Then<T, TNew>(this ValueTask<Result<T>> result,
Func<T, ValueTask<Result<TNew>>> func)
=> await (await result).ThenAsync(func);
#endregion
}

92
src/Result/Prelude.cs Normal file
View file

@ -0,0 +1,92 @@
using System.Diagnostics.Contracts;
namespace Geekeey.Extensions.Result;
/// <summary>
/// A class containing various utility methods, a 'prelude' to the rest of the library.
/// </summary>
/// <remarks>
/// This class is meant to be imported statically, e.g. <c>using static Geekeey.Extensions.Result.Prelude;</c>.
/// Recommended to be imported globally via a global using statement.
/// </remarks>
public static class Prelude
{
/// <summary>
/// Creates a result containing an success value.
/// </summary>
/// <typeparam name="T">The type of the success value.</typeparam>
/// <param name="value">The success value to create the result from.</param>
[Pure]
public static Result<T> Success<T>(T value) => new(value);
/// <summary>
/// Creates a result containing a failure value.
/// </summary>
/// <typeparam name="T">The type of an success value in the result.</typeparam>
/// <param name="error">The failure value to create the result from.</param>
[Pure]
public static Result<T> Failure<T>(Error error) => new(error);
/// <summary>
/// Tries to execute a function and return the result. If the function throws an exception, the exception will be
/// returned wrapped in an <see cref="ExceptionError"/>.
/// </summary>
/// <typeparam name="T">The type the function returns.</typeparam>
/// <param name="function">The function to try execute.</param>
/// <returns>A result containing the return value of the function or an <see cref="ExceptionError"/> containing the
/// exception thrown by the function.</returns>
[Pure]
public static Result<T> Try<T>(Func<T> function)
{
try
{
return new Result<T>(function());
}
catch (Exception exception)
{
return new Result<T>(new ExceptionError(exception));
}
}
/// <summary>
/// Tries to execute an asynchronous function and return the result. If the function throws an exception, the
/// exception will be returned wrapped in an <see cref="ExceptionError"/>.
/// </summary>
/// <typeparam name="T">The type the function returns.</typeparam>
/// <param name="function">The function to try execute.</param>
/// <returns>A result containing the return value of the function or an <see cref="ExceptionError"/> containing the
/// exception thrown by the function.</returns>
[Pure]
public static async ValueTask<Result<T>> TryAsync<T>(Func<ValueTask<T>> function)
{
try
{
return new Result<T>(await function());
}
catch (Exception exception)
{
return new Result<T>(new ExceptionError(exception));
}
}
/// <summary>
/// Tries to execute an asynchronous function and return the result. If the function throws an exception, the
/// exception will be returned wrapped in an <see cref="ExceptionError"/>.
/// </summary>
/// <typeparam name="T">The type the function returns.</typeparam>
/// <param name="function">The function to try execute.</param>
/// <returns>A result containing the return value of the function or an <see cref="ExceptionError"/> containing the
/// exception thrown by the function.</returns>
[Pure]
public static async Task<Result<T>> TryAsync<T>(Func<Task<T>> function)
{
try
{
return new Result<T>(await function());
}
catch (Exception exception)
{
return new Result<T>(new ExceptionError(exception));
}
}
}

5
src/Result/Project.props Normal file
View file

@ -0,0 +1,5 @@
<Project>
<ItemGroup Condition="'$(ImplicitUsings)' == 'enable'">
<Using Include="Geekeey.Extensions.Result.Prelude" Static="true" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,36 @@
using System.Diagnostics.Contracts;
namespace Geekeey.Extensions.Result;
public readonly partial struct Result<T>
{
/// <summary>
/// Implicitly constructs a result from a success value.
/// </summary>
/// <param name="value">The value to construct the result from.</param>
[Pure]
public static implicit operator Result<T>(T value) => new(value);
/// <summary>
/// Implicitly constructs a result from a failure value.
/// </summary>
/// <param name="error">The error to construct the result from.</param>
[Pure]
public static implicit operator Result<T>(Error error) => new(error);
/// <summary>
/// Unwraps the success value of the result. Throws an <see cref="UnwrapException"/> if the result is a failure.
/// </summary>
/// <remarks>
/// This call is <b>unsafe</b> in the sense that it might intentionally throw an exception. Please only use this
/// call if the caller knows that this operation is safe, or that an exception is acceptable to be thrown.
/// </remarks>
/// <returns>The success value of the result.</returns>
/// <exception cref="UnwrapException">The result is not a success.</exception>
[Pure]
public T Unwrap() => IsSuccess ? Value! : throw new UnwrapException();
/// <inheritdoc cref="Unwrap"/>
[Pure]
public static explicit operator T(Result<T> result) => result.Unwrap();
}

View file

@ -0,0 +1,115 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Numerics;
namespace Geekeey.Extensions.Result;
public readonly partial struct Result<T> : IEquatable<Result<T>>, IEquatable<T>,
IEqualityOperators<Result<T>, Result<T>, bool>, IEqualityOperators<Result<T>, T, bool>
{
/// <summary>
/// Checks whether the result is equal to another result. Results are equal if both results are success values and
/// the success values are equal, or if both results are failures.
/// </summary>
/// <param name="other">The result to check for equality with the current result.</param>
[Pure]
public bool Equals(Result<T> other)
=> Equals(this, other, EqualityComparer<T>.Default);
/// <summary>
/// Checks whether the result is equal to another result. Results are equal if both results are success values and
/// the success values are equal, or if both results are failures.
/// </summary>
/// <param name="other">The result to check for equality with the current result.</param>
/// <param name="comparer">The equality comparer to use for comparing values.</param>
[Pure]
public bool Equals(Result<T> other, IEqualityComparer<T> comparer)
=> Equals(this, other, comparer);
/// <summary>
/// Checks whether the result is a success value and the success value is equal to another value.
/// </summary>
/// <param name="other">The value to check for equality with the success value of the result.</param>
[Pure]
public bool Equals(T? other)
=> Equals(this, other, EqualityComparer<T>.Default);
/// <summary>
/// Checks whether the result is a success value and the success value is equal to another value using a specified
/// equality comparer.
/// </summary>
/// <param name="other">The value to check for equality with the success value of the result.</param>
/// <param name="comparer">The equality comparer to use for comparing values.</param>
[Pure]
public bool Equals(T? other, IEqualityComparer<T> comparer)
=> Equals(this, other, comparer);
/// <inheritdoc/>
[Pure]
public override bool Equals(object? other)
=> other is T x && Equals(x) || other is Result<T> r && Equals(r);
/// <inheritdoc/>
[Pure]
public override int GetHashCode()
=> GetHashCode(this, EqualityComparer<T>.Default);
internal static bool Equals(Result<T> a, Result<T> b, IEqualityComparer<T> comparer)
{
if (!a._success || !b._success) return !a._success && !b._success;
if (a.Value is null || b.Value is null) return a.Value is null && b.Value is null;
return comparer.Equals(a.Value, b.Value);
}
internal static bool Equals(Result<T> a, T? b, IEqualityComparer<T> comparer)
{
if (!a._success) return false;
if (a.Value is null || b is null) return a.Value is null && b is null;
return comparer.Equals(a.Value, b);
}
internal static int GetHashCode(Result<T> result, IEqualityComparer<T> comparer)
{
if (result is { _success: true, Value: not null })
return comparer.GetHashCode(result.Value);
return 0;
}
/// <summary>
/// Checks whether two results are equal. Results are equal if both results are success values and the success
/// values are equal, or if both results are failures.
/// </summary>
/// <param name="a">The first result to compare.</param>
/// <param name="b">The second result to compare.</param>
[Pure]
[ExcludeFromCodeCoverage]
public static bool operator ==(Result<T> a, Result<T> b) => a.Equals(b);
/// <summary>
/// Checks whether two results are not equal. Results are equal if both results are success values and the success
/// values are equal, or if both results are failures.
/// </summary>
/// <param name="a">The first result to compare.</param>
/// <param name="b">The second result to compare.</param>
[Pure]
[ExcludeFromCodeCoverage]
public static bool operator !=(Result<T> a, Result<T> b) => !a.Equals(b);
/// <summary>
/// Checks whether a result is a success value and the success value is equal to another value.
/// </summary>
/// <param name="a">The result to compare.</param>
/// <param name="b">The value to check for equality with the success value in the result.</param>
[Pure]
[ExcludeFromCodeCoverage]
public static bool operator ==(Result<T> a, T? b) => a.Equals(b);
/// <summary>
/// Checks whether a result either does not have a value, or the value is not equal to another value.
/// </summary>
/// <param name="a">The result to compare.</param>
/// <param name="b">The value to check for inequality with the success value in the result.</param>
[Pure]
[ExcludeFromCodeCoverage]
public static bool operator !=(Result<T> a, T? b) => !a.Equals(b);
}

View file

@ -0,0 +1,91 @@
using System.Diagnostics.Contracts;
namespace Geekeey.Extensions.Result;
public readonly partial struct Result<T>
{
/// <summary>
/// Matches over the success value or failure value of the result and returns another value. Can be conceptualized
/// as an exhaustive <c>switch</c> expression matching all possible values of the type.
/// </summary>
/// <typeparam name="TResult">The type to return from the match.</typeparam>
/// <param name="success">The function to apply to the success value of the result if the result is a success.</param>
/// <param name="failure">The function to apply to the failure value of the result if the result is a failure.</param>
/// <returns>The result of applying either <paramref name="success"/> or <paramref name="failure"/> on the success
/// value or failure value of the result.</returns>
[Pure]
public TResult Match<TResult>(Func<T, TResult> success, Func<Error, TResult> failure)
=> _success ? success(Value!) : failure(Error!);
/// <summary>
/// Matches over the success value or failure value of the result and invokes an effectful action onto the success
/// value or failure value. Can be conceptualized as an exhaustive <c>switch</c> statement matching all possible
/// values of the type.
/// </summary>
/// <param name="success">The function to call with the success value of the result if the result is a success.</param>
/// <param name="failure">The function to call with the failure value of the result if the result is a failure.</param>
public void Switch(Action<T> success, Action<Error> failure)
{
if (_success) success(Value!);
else failure(Error!);
}
}
public readonly partial struct Result<T>
{
/// <summary>
/// Asynchronously matches over the success value or failure value of the result and returns another value. Can be
/// conceptualized as an exhaustive <c>switch</c> expression matching all possible values of the type.
/// </summary>
/// <typeparam name="TResult">The type to return from the match.</typeparam>
/// <param name="success">The function to apply to the success value of the result if the result is a success.</param>
/// <param name="failure">The function to apply to the failure value of the result if the result is a failure.</param>
/// <returns>A task completing with the result of applying either <paramref name="success"/> or
/// <paramref name="failure"/> on the success value or failure value of the result.</returns>
[Pure]
public async ValueTask<TResult> MatchAsync<TResult>(Func<T, ValueTask<TResult>> success, Func<Error, ValueTask<TResult>> failure)
=> _success ? await success(Value!) : await failure(Error!);
/// <summary>
/// Asynchronously matches over the success value or failure value of the result and invokes an effectful action
/// onto the success value or the failure value. Can be conceptualized as an exhaustive <c>switch</c> statement
/// matching all possible values of the type.
/// </summary>
/// <param name="success">The function to call with the success value of the result if the result is a success.</param>
/// <param name="failure">The function to call with the failure value of the result if the result is a failure.</param>
public async ValueTask SwitchAsync(Func<T, ValueTask> success, Func<Error, ValueTask> failure)
{
if (_success) await success(Value!);
else await failure(Error!);
}
}
public readonly partial struct Result<T>
{
/// <summary>
/// Asynchronously matches over the success value or failure value of the result and returns another value. Can be
/// conceptualized as an exhaustive <c>switch</c> expression matching all possible values of the type.
/// </summary>
/// <typeparam name="TResult">The type to return from the match.</typeparam>
/// <param name="success">The function to apply to the success value of the result if the result is a success.</param>
/// <param name="failure">The function to apply to the failure value of the result if the result is a failure.</param>
/// <returns>A task completing with the result of applying either <paramref name="success"/> or
/// <paramref name="failure"/> on the success value or failure value of the result.</returns>
[Pure]
public async Task<TResult> MatchAsync<TResult>(Func<T, Task<TResult>> success, Func<Error, Task<TResult>> failure)
=> _success ? await success(Value!) : await failure(Error!);
/// <summary>
/// Asynchronously matches over the success value or failure value of the result and invokes an effectful action
/// onto the success value or failure value. Can be conceptualized as an exhaustive <c>switch</c> statement matching
/// all possible values of
/// the type.
/// </summary>
/// <param name="success">The function to call with the success value of the result if the result is a success.</param>
/// <param name="failure">The function to call with the failure value of the result if the result is a failure.</param>
public async Task SwitchAsync(Func<T, Task> success, Func<Error, Task> failure)
{
if (_success) await success(Value!);
else await failure(Error!);
}
}

View file

@ -0,0 +1,335 @@
using System.Diagnostics.Contracts;
namespace Geekeey.Extensions.Result;
public readonly partial struct Result<T>
{
/// <summary>
/// Maps the success value of the result using a mapping function, or does nothing if the result is a failure.
/// </summary>
/// <param name="func">The function used to map the success value.</param>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A new result containing either the mapped success value or the failure value of the original
/// result.</returns>
[Pure]
public Result<TNew> Map<TNew>(Func<T, TNew> func)
=> _success ? new Result<TNew>(func(Value!)) : new Result<TNew>(Error!);
/// <summary>
/// Tries to map the success value of the result using a mapping function, or does nothing if the result is a
/// failure. If the mapping function throws an exception, the exception will be returned wrapped in an
/// <see cref="ExceptionError"/>.
/// </summary>
/// <param name="func">The function used to map the success value.</param>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A new result containing either the mapped value, the exception thrown by <paramref name="func"/>
/// wrapped in an <see cref="ExceptionError"/>, or the failure value of the original result.</returns>
[Pure]
public Result<TNew> TryMap<TNew>(Func<T, TNew> func)
{
try
{
return Map(func);
}
catch (Exception exception)
{
return new Result<TNew>(new ExceptionError(exception));
}
}
/// <summary>
/// Maps the success value of the result to a new result using a mapping function, or does nothing if the result is
/// a failure.
/// </summary>
/// <param name="func">The function used to map the success value to a new result.</param>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A result which is either the mapped result or a new result containing the failure value of the original
/// result.</returns>
[Pure]
public Result<TNew> Then<TNew>(Func<T, Result<TNew>> func)
=> _success ? func(Value!) : new Result<TNew>(Error!);
/// <summary>
/// Tries to map the success value of the result to a new result using a mapping function, or does nothing if the result
/// is a failure. If the mapping function throws an exception, the exception will be returned wrapped in an
/// <see cref="ExceptionError"/>.
/// </summary>
/// <param name="func">The function used to map the success value to a new result.</param>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A result which is either the mapped result, the exception thrown by <paramref name="func"/> wrapped in
/// an <see cref="ExceptionError"/>, or a new result containing the failure value of the original result.</returns>
[Pure]
public Result<TNew> ThenTry<TNew>(Func<T, Result<TNew>> func)
{
try
{
return Then(func);
}
catch (Exception exception)
{
return new Result<TNew>(new ExceptionError(exception));
}
}
}
public readonly partial struct Result<T>
{
/// <summary>
/// Maps the success value of the result using an asynchronous mapping function, or does nothing if the result is
/// a failure.
/// </summary>
/// <param name="func">The function used to map the success value.</param>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A <see cref="ValueTask{T}"/> which either completes asynchronously by invoking the mapping function on
/// the success value of the result and constructing a new result containing the mapped value, or completes
/// synchronously by returning a new result containing the failure value of the original result.</returns>
[Pure]
public ValueTask<Result<TNew>> MapAsync<TNew>(Func<T, ValueTask<TNew>> func)
{
if (!_success) return ValueTask.FromResult(new Result<TNew>(Error!));
var task = func(Value!);
return CreateResult(task);
static async ValueTask<Result<TNew>> CreateResult(ValueTask<TNew> task)
{
var value = await task;
return new Result<TNew>(value);
}
}
/// <summary>
/// Maps the success value of the result using an asynchronous mapping function, or does nothing if the result is a
/// failure. If the mapping function throws an exception, the exception will be returned wrapped in an
/// <see cref="ExceptionError"/>.
/// </summary>
/// <param name="func">The function used to map the success value.</param>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A <see cref="ValueTask{T}"/> which either completes asynchronously by invoking the mapping function on
/// the success value of the result and constructing a new result containing the mapped value, returning any exception
/// thrown by <paramref name="func"/> wrapped in an <see cref="ExceptionError"/> or completes synchronously by
/// returning a new result containing the failure value of the original result.</returns>
[Pure]
public ValueTask<Result<TNew>> TryMapAsync<TNew>(Func<T, ValueTask<TNew>> func)
{
if (!_success) return ValueTask.FromResult(new Result<TNew>(Error!));
try
{
var task = func(Value!);
return CreateResult(task);
}
catch (Exception exception)
{
return ValueTask.FromResult(new Result<TNew>(new ExceptionError(exception)));
}
static async ValueTask<Result<TNew>> CreateResult(ValueTask<TNew> task)
{
try
{
var value = await task;
return new Result<TNew>(value);
}
catch (Exception exception)
{
return new Result<TNew>(new ExceptionError(exception));
}
}
}
/// <summary>
/// Maps the success value of the result to a new result using an asynchronous mapping function, or does nothing if
/// the result is a failure.
/// </summary>
/// <param name="func">The function used to map the success value to a new result.</param>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A <see cref="ValueTask{T}"/> which either completes asynchronously by invoking the mapping function on
/// the success value of the result, or completes synchronously by returning a new result containing the failure
/// value of the original result.</returns>
[Pure]
public ValueTask<Result<TNew>> ThenAsync<TNew>(Func<T, ValueTask<Result<TNew>>> func)
{
if (!_success) return ValueTask.FromResult(new Result<TNew>(Error!));
var task = func(Value!);
return CreateResult(task);
static async ValueTask<Result<TNew>> CreateResult(ValueTask<Result<TNew>> task)
{
var result = await task;
return result;
}
}
/// <summary>
/// Maps the success value of the result to a new result using an asynchronous mapping function, or does nothing if
/// the result is a failure. If the mapping function throws an exception, the exception will be returned wrapped in
/// an <see cref="ExceptionError"/>.
/// </summary>
/// <param name="func">The function used to map the success value to a new result.</param>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A <see cref="ValueTask{T}"/> which either completes asynchronously by invoking the mapping function on
/// the success value of the result, returning any exception thrown by <paramref name="func"/> wrapped in an
/// <see cref="ExceptionError"/>, or completes synchronously by returning a new result containing the failure value
/// of the original result.</returns>
[Pure]
public ValueTask<Result<TNew>> ThenTryAsync<TNew>(Func<T, ValueTask<Result<TNew>>> func)
{
if (!_success) return ValueTask.FromResult(new Result<TNew>(Error!));
try
{
var task = func(Value!);
return CreateResult(task);
}
catch (Exception exception)
{
return ValueTask.FromResult(new Result<TNew>(new ExceptionError(exception)));
}
static async ValueTask<Result<TNew>> CreateResult(ValueTask<Result<TNew>> task)
{
try
{
var value = await task;
return value;
}
catch (Exception exception)
{
return new Result<TNew>(new ExceptionError(exception));
}
}
}
}
public readonly partial struct Result<T>
{
/// <summary>
/// Maps the success value of the result using an asynchronous mapping function, or does nothing if the result is
/// a failure.
/// </summary>
/// <param name="func">The function used to map the success value.</param>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A <see cref="Task{T}"/> which either completes asynchronously by invoking the mapping function on
/// the success value of the result and constructing a new result containing the mapped value, or completes
/// synchronously by returning a new result containing the failure value of the original result.</returns>
[Pure]
public Task<Result<TNew>> MapAsync<TNew>(Func<T, Task<TNew>> func)
{
if (!_success) return Task.FromResult(new Result<TNew>(Error!));
var task = func(Value!);
return CreateResult(task);
static async Task<Result<TNew>> CreateResult(Task<TNew> task)
{
var value = await task;
return new Result<TNew>(value);
}
}
/// <summary>
/// Maps the success value of the result using an asynchronous mapping function, or does nothing if the result is a
/// failure. If the mapping function throws an exception, the exception will be returned wrapped in an
/// <see cref="ExceptionError"/>.
/// </summary>
/// <param name="func">The function used to map the success value.</param>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A <see cref="Task{T}"/> which either completes asynchronously by invoking the mapping function on
/// the success value of the result and constructing a new result containing the mapped value, returning any exception
/// thrown by <paramref name="func"/> wrapped in an <see cref="ExceptionError"/> or completes synchronously by
/// returning a new result containing the failure value of the original result.</returns>
[Pure]
public Task<Result<TNew>> TryMapAsync<TNew>(Func<T, Task<TNew>> func)
{
if (!_success) return Task.FromResult(new Result<TNew>(Error!));
try
{
var task = func(Value!);
return CreateResult(task);
}
catch (Exception exception)
{
return Task.FromResult(new Result<TNew>(new ExceptionError(exception)));
}
static async Task<Result<TNew>> CreateResult(Task<TNew> task)
{
try
{
var value = await task;
return new Result<TNew>(value);
}
catch (Exception exception)
{
return new Result<TNew>(new ExceptionError(exception));
}
}
}
/// <summary>
/// Maps the success value of the result to a new result using an asynchronous mapping function, or does nothing if
/// the result is a failure.
/// </summary>
/// <param name="func">The function used to map the success value to a new result.</param>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A <see cref="Task{T}"/> which either completes asynchronously by invoking the mapping function on
/// the success value of the result, or completes synchronously by returning a new result containing the failure
/// value of the original result.</returns>
[Pure]
public Task<Result<TNew>> ThenAsync<TNew>(Func<T, Task<Result<TNew>>> func)
{
if (!_success) return Task.FromResult(new Result<TNew>(Error!));
var task = func(Value!);
return CreateResult(task);
static async Task<Result<TNew>> CreateResult(Task<Result<TNew>> task)
{
var result = await task;
return result;
}
}
/// <summary>
/// Maps the success value of the result to a new result using an asynchronous mapping function, or does nothing if
/// the result is a failure. If the mapping function throws an exception, the exception will be returned wrapped in
/// an <see cref="ExceptionError"/>.
/// </summary>
/// <param name="func">The function used to map the success value to a new result.</param>
/// <typeparam name="TNew">The type of the new value.</typeparam>
/// <returns>A <see cref="Task{T}"/> which either completes asynchronously by invoking the mapping function on
/// the success value of the result, returning any exception thrown by <paramref name="func"/> wrapped in an
/// <see cref="ExceptionError"/>, or completes synchronously by returning a new result containing the failure value
/// of the original result.</returns>
[Pure]
public Task<Result<TNew>> ThenTryAsync<TNew>(Func<T, Task<Result<TNew>>> func)
{
if (!_success) return Task.FromResult(new Result<TNew>(Error!));
try
{
var task = func(Value!);
return CreateResult(task);
}
catch (Exception exception)
{
return Task.FromResult(new Result<TNew>(new ExceptionError(exception)));
}
static async Task<Result<TNew>> CreateResult(Task<Result<TNew>> task)
{
try
{
var value = await task;
return value;
}
catch (Exception exception)
{
return new Result<TNew>(new ExceptionError(exception));
}
}
}
}

View file

@ -0,0 +1,63 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
namespace Geekeey.Extensions.Result;
public readonly partial struct Result<T>
{
/// <summary>
/// Tries to get the success value from the result.
/// </summary>
/// <param name="value">The success value of the result.</param>
/// <returns>Whether the result has success value.</returns>
[Pure]
public bool TryGetValue([MaybeNullWhen(false)] out T value)
{
value = Value!;
return _success;
}
/// <summary>
/// Tries to get the success value from the result.
/// </summary>
/// <param name="value">The success value of the result.</param>
/// <param name="error">The failure value of the result.</param>
/// <returns>Whether the result has a success value.</returns>
[Pure]
public bool TryGetValue([MaybeNullWhen(false)] out T value, [MaybeNullWhen(true)] out Error error)
{
value = Value!;
error = !_success ? Error : null!;
return _success;
}
/// <summary>
/// Tries to get the failure value from the result.
/// </summary>
/// <param name="error">The failure value of the result.</param>
/// <returns>Whether the result has a failure value.</returns>
[Pure]
public bool TryGetError([MaybeNullWhen(false)] out Error error)
{
error = !_success ? Error : null!;
return !_success;
}
/// <summary>
/// Tries to get the failure value from the result.
/// </summary>
/// <param name="error">The failure value of the result.</param>
/// <param name="value">The success value of the result.</param>
/// <returns>Whether the result a failure value.</returns>
[Pure]
public bool TryGetError([MaybeNullWhen(false)] out Error error, [MaybeNullWhen(true)] out T value)
{
error = !_success ? Error : null!;
value = Value!;
return !_success;
}
}

76
src/Result/Result.cs Normal file
View file

@ -0,0 +1,76 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
namespace Geekeey.Extensions.Result;
/// <summary>
/// A type which contains either a success value or a failure value, which is represented by an <see cref="Error"/>.
/// </summary>
/// <typeparam name="T">The type of the success value.</typeparam>
[DebuggerTypeProxy(typeof(Result<>.ResultDebugProxy))]
public readonly partial struct Result<T>
{
private readonly bool _success;
private readonly T? _value;
private readonly Error? _error;
/// <summary>
/// Creates a new result with a success value.
/// </summary>
/// <param name="value">The success value.</param>
public Result(T value)
{
_success = true;
_value = value;
_error = default;
}
/// <summary>
/// Creates a new result with a failure value.
/// </summary>
/// <param name="error">The error of the result.</param>
public Result(Error error)
{
_success = false;
_value = default;
_error = error;
}
internal T? Value => _value;
internal Error? Error => _success ? null : (_error ?? Error.DefaultValueError);
/// <summary>
/// Whether the result is a success.
/// </summary>
/// <remarks>
/// This is always the inverse of <see cref="IsFailure"/> but is more specific about intent.
/// </remarks>
[MemberNotNullWhen(true, nameof(Value))]
public bool IsSuccess => _success;
/// <summary>
/// Whether the result is a failure.
/// </summary>
/// <remarks>
/// This is always the inverse of <see cref="IsSuccess"/> but is more specific about intent.
/// </remarks>
[MemberNotNullWhen(true, nameof(Error))]
public bool IsFailure => !_success;
/// <summary>
/// Gets a string representation of the result.
/// </summary>
[Pure]
public override string ToString()
=> _success ? $"Success {{ {Value} }}" : $"Failure {{ {Error} }}";
private sealed class ResultDebugProxy(Result<T> result)
{
public bool IsSuccess => result.IsSuccess;
public object? Value => result.IsSuccess ? result.Value : result.Error;
}
}

14
src/Result/Result.csproj Normal file
View file

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
</ItemGroup>
<ItemGroup>
<None Include=".\package-readme.md" Pack="true" PackagePath="\" />
<None Include="Project.props" Pack="true" PackagePath="build\$(AssemblyName).props" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,2 @@
Result is a simple yet powerful [result type](https://doc.rust-lang.org/std/result/) implementation for C#, containing a
variety of utilities and standard functions for working with result types and integrating them into the rest of C#.