feat: add inital in memory dispatcher
All checks were successful
default / dotnet-default-workflow (push) Successful in 1m27s

Add a simple in memory dispatcher for scalar requests and stream request.
This commit is contained in:
Louis Seubert 2026-05-08 20:26:26 +02:00
commit 28c07c5c3f
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
145 changed files with 6380 additions and 0 deletions

409
.editorconfig Normal file
View file

@ -0,0 +1,409 @@
root = true
[*]
indent_style = tab
indent_size = 4
tab_width = 4
end_of_line = lf
insert_final_newline = false
trim_trailing_whitespace = true
max_line_length = 120
[*.{md,json,yaml,yml}]
indent_size = 2
indent_style = space
trim_trailing_whitespace = false
[*.{csproj,props,targets,slnx,config}]
indent_size = 2
indent_style = space
[*.{cs,vb}]
#### code quality rule overrides ####
#> Identifiers should not match keywords
dotnet_diagnostic.CA1716.severity = suggestion
#### code style rule default severity ####
dotnet_analyzer_diagnostic.category-style.severity = warning
#> Use top-level statements
dotnet_diagnostic.IDE0210.severity = none
#### .NET Coding Conventions ####
[*.{cs,vb}]
# Organize usings
file_header_template = Copyright (c) The Geekeey Authors\nSPDX-License-Identifier: EUPL-1.2
dotnet_separate_import_directive_groups = true
dotnet_sort_system_directives_first = true
# this. and Me. preferences
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Expression-level preferences
dotnet_diagnostic.IDE0270.severity = none
dotnet_style_coalesce_expression = true # IDE0029,IDE0030,IDE0270
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_compound_assignment = true
dotnet_diagnostic.IDE0045.severity = suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_diagnostic.IDE0046.severity = suggestion
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
dotnet_style_namespace_match_folder = false
# Field preferences
dotnet_style_readonly_field = true
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
# ReSharper preferences
resharper_wrap_object_and_collection_initializer_style = chop_always
resharper_check_namespace_highlighting = none
resharper_csharp_wrap_lines = false
#### C# Coding Conventions ####
[*.cs]
# var preferences
csharp_style_var_for_built_in_types = true
csharp_style_var_when_type_is_apparent = true
csharp_style_var_elsewhere = true
# Expression-bodied members
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = false
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = false
csharp_style_expression_bodied_methods = false
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = true
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Null-checking preferences
csharp_style_conditional_delegate_call = true
# Modifier preferences
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async
# Code-block preferences
csharp_prefer_braces = true
csharp_prefer_simple_using_statement = true
# Expression-level preferences
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_inlined_variable_declaration = true
csharp_style_pattern_local_over_anonymous_function = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_range_operator = true
csharp_style_throw_expression = true
dotnet_diagnostic.IDE0058.severity = suggestion
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace
# 'namespace' preferences
csharp_style_namespace_declarations = file_scoped
# 'constructor' preferences
csharp_style_prefer_primary_constructors = false
#### C# Formatting Rules ####
[*.cs]
# 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
#### .NET Naming styles ####
[*.{cs,vb}]
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.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

View file

@ -0,0 +1,43 @@
name: default
on:
push:
branches: [ "main", "develop" ]
paths-ignore:
- "doc/**"
- "*.md"
pull_request:
branches: [ "main", "develop" ]
paths-ignore:
- "doc/**"
- "*.md"
jobs:
default:
name: dotnet-default-workflow
runs-on: debian-latest
strategy:
matrix:
dotnet-version: [ "10.0" ]
container: mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet-version }}
steps:
- name: checkout
uses: https://code.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 format --verify-no-changes
run: |
dotnet format --no-restore --verify-no-changes --verbosity detailed
- name: dotnet test
run: |
dotnet test -p:ContinuousIntegrationBuild=true

View file

@ -0,0 +1,36 @@
name: release
on:
push:
tags: [ "[0-9]+.[0-9]+.[0-9]+" ]
jobs:
release:
name: dotnet-release-workflow
runs-on: debian-latest
strategy:
matrix:
dotnet-version: [ "10.0" ]
container: mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet-version }}
steps:
- uses: https://code.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 -p:ContinuousIntegrationBuild=true
- name: dotnet nuget push
run: |
# The token used here is only intended to publish packages
dotnet nuget push -k "${{ secrets.geekeey_package_registry }}" \
artifacts/package/release/Geekeey.*.nupkg

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
artifacts/
*.DotSettings.user

18
CHANGELOG.md Normal file
View file

@ -0,0 +1,18 @@
# Changelog
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- This is the initial release of the library.
### Changed
### Removed
[1.0.0]: https://code.geekeey.de/geekeey/request/releases/tag/1.0.0
[Unreleased]: https://code.geekeey.de/geekeey/request/compare/1.0.0...HEAD

38
Directory.Build.props Normal file
View file

@ -0,0 +1,38 @@
<Project>
<PropertyGroup Condition="'$(ArtifactsPath)' == ''">
<ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
</PropertyGroup>
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<VersionPrefix>1.0.0</VersionPrefix>
</PropertyGroup>
<PropertyGroup>
<AnalysisMode>Recommended</AnalysisMode>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<WarningsAsErrors>nullable</WarningsAsErrors>
<WarningsAsErrors Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</WarningsAsErrors>
</PropertyGroup>
<PropertyGroup Label="NuGet Package Info">
<Authors>The Geekeey Team</Authors>
<Copyright>Copyright (c) The Geekeey Team 2026</Copyright>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.Gitea" PrivateAssets="all" />
</ItemGroup>
<PropertyGroup>
<NuGetAuditLevel>moderate</NuGetAuditLevel>
<NuGetAuditMode>all</NuGetAuditMode>
</PropertyGroup>
</Project>

2
Directory.Build.targets Normal file
View file

@ -0,0 +1,2 @@
<Project>
</Project>

12
Directory.Packages.props Normal file
View file

@ -0,0 +1,12 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.6" />
<PackageVersion Include="Microsoft.SourceLink.Gitea" Version="10.0.102" />
<PackageVersion Include="TUnit" Version="1.11.51" />
</ItemGroup>
</Project>

287
LICENSE.md Normal file
View file

@ -0,0 +1,287 @@
EUROPEAN UNION PUBLIC LICENCE v. 1.2
EUPL © the European Union 2007, 2016
This European Union Public Licence (the EUPL) applies to the Work (as defined
below) which is provided under the terms of this Licence. Any use of the Work,
other than as authorised under this Licence is prohibited (to the extent such
use is covered by a right of the copyright holder of the Work).
The Work is provided under the terms of this Licence when the Licensor (as
defined below) has placed the following notice immediately following the
copyright notice for the Work:
Licensed under the EUPL
or has expressed by any other means his willingness to license under the EUPL.
1. Definitions
In this Licence, the following terms have the following meaning:
- The Licence: this Licence.
- The Original Work: the work or software distributed or communicated by the
Licensor under this Licence, available as Source Code and also as Executable
Code as the case may be.
- Derivative Works: the works or software that could be created by the
Licensee, based upon the Original Work or modifications thereof. This Licence
does not define the extent of modification or dependence on the Original Work
required in order to classify a work as a Derivative Work; this extent is
determined by copyright law applicable in the country mentioned in Article 15.
- The Work: the Original Work or its Derivative Works.
- The Source Code: the human-readable form of the Work which is the most
convenient for people to study and modify.
- The Executable Code: any code which has generally been compiled and which is
meant to be interpreted by a computer as a program.
- The Licensor: the natural or legal person that distributes or communicates
the Work under the Licence.
- Contributor(s): any natural or legal person who modifies the Work under the
Licence, or otherwise contributes to the creation of a Derivative Work.
- The Licensee or You: any natural or legal person who makes any usage of
the Work under the terms of the Licence.
- Distribution or Communication: any act of selling, giving, lending,
renting, distributing, communicating, transmitting, or otherwise making
available, online or offline, copies of the Work or providing access to its
essential functionalities at the disposal of any other natural or legal
person.
2. Scope of the rights granted by the Licence
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
sublicensable licence to do the following, for the duration of copyright vested
in the Original Work:
- use the Work in any circumstance and for all usage,
- reproduce the Work,
- modify the Work, and make Derivative Works based upon the Work,
- communicate to the public, including the right to make available or display
the Work or copies thereof to the public and perform publicly, as the case may
be, the Work,
- distribute the Work or copies thereof,
- lend and rent the Work or copies thereof,
- sublicense rights in the Work or copies thereof.
Those rights can be exercised on any media, supports and formats, whether now
known or later invented, as far as the applicable law permits so.
In the countries where moral rights apply, the Licensor waives his right to
exercise his moral right to the extent allowed by law in order to make effective
the licence of the economic rights here above listed.
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
any patents held by the Licensor, to the extent necessary to make use of the
rights granted on the Work under this Licence.
3. Communication of the Source Code
The Licensor may provide the Work either in its Source Code form, or as
Executable Code. If the Work is provided as Executable Code, the Licensor
provides in addition a machine-readable copy of the Source Code of the Work
along with each copy of the Work that the Licensor distributes or indicates, in
a notice following the copyright notice attached to the Work, a repository where
the Source Code is easily and freely accessible for as long as the Licensor
continues to distribute or communicate the Work.
4. Limitations on copyright
Nothing in this Licence is intended to deprive the Licensee of the benefits from
any exception or limitation to the exclusive rights of the rights owners in the
Work, of the exhaustion of those rights or of other applicable limitations
thereto.
5. Obligations of the Licensee
The grant of the rights mentioned above is subject to some restrictions and
obligations imposed on the Licensee. Those obligations are the following:
Attribution right: The Licensee shall keep intact all copyright, patent or
trademarks notices and all notices that refer to the Licence and to the
disclaimer of warranties. The Licensee must include a copy of such notices and a
copy of the Licence with every copy of the Work he/she distributes or
communicates. The Licensee must cause any Derivative Work to carry prominent
notices stating that the Work has been modified and the date of modification.
Copyleft clause: If the Licensee distributes or communicates copies of the
Original Works or Derivative Works, this Distribution or Communication will be
done under the terms of this Licence or of a later version of this Licence
unless the Original Work is expressly distributed only under this version of the
Licence — for example by communicating EUPL v. 1.2 only. The Licensee
(becoming Licensor) cannot offer or impose any additional terms or conditions on
the Work or Derivative Work that alter or restrict the terms of the Licence.
Compatibility clause: If the Licensee Distributes or Communicates Derivative
Works or copies thereof based upon both the Work and another work licensed under
a Compatible Licence, this Distribution or Communication can be done under the
terms of this Compatible Licence. For the sake of this clause, Compatible
Licence refers to the licences listed in the appendix attached to this Licence.
Should the Licensee's obligations under the Compatible Licence conflict with
his/her obligations under this Licence, the obligations of the Compatible
Licence shall prevail.
Provision of Source Code: When distributing or communicating copies of the Work,
the Licensee will provide a machine-readable copy of the Source Code or indicate
a repository where this Source will be easily and freely available for as long
as the Licensee continues to distribute or communicate the Work.
Legal Protection: This Licence does not grant permission to use the trade names,
trademarks, service marks, or names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the copyright notice.
6. Chain of Authorship
The original Licensor warrants that the copyright in the Original Work granted
hereunder is owned by him/her or licensed to him/her and that he/she has the
power and authority to grant the Licence.
Each Contributor warrants that the copyright in the modifications he/she brings
to the Work are owned by him/her or licensed to him/her and that he/she has the
power and authority to grant the Licence.
Each time You accept the Licence, the original Licensor and subsequent
Contributors grant You a licence to their contributions to the Work, under the
terms of this Licence.
7. Disclaimer of Warranty
The Work is a work in progress, which is continuously improved by numerous
Contributors. It is not a finished work and may therefore contain defects or
bugs inherent to this type of development.
For the above reason, the Work is provided under the Licence on an as is basis
and without warranties of any kind concerning the Work, including without
limitation merchantability, fitness for a particular purpose, absence of defects
or errors, accuracy, non-infringement of intellectual property rights other than
copyright as stated in Article 6 of this Licence.
This disclaimer of warranty is an essential part of the Licence and a condition
for the grant of any rights to the Work.
8. Disclaimer of Liability
Except in the cases of wilful misconduct or damages directly caused to natural
persons, the Licensor will in no event be liable for any direct or indirect,
material or moral, damages of any kind, arising out of the Licence or of the use
of the Work, including without limitation, damages for loss of goodwill, work
stoppage, computer failure or malfunction, loss of data or any commercial
damage, even if the Licensor has been advised of the possibility of such damage.
However, the Licensor will be liable under statutory product liability laws as
far such laws apply to the Work.
9. Additional agreements
While distributing the Work, You may choose to conclude an additional agreement,
defining obligations or services consistent with this Licence. However, if
accepting obligations, You may act only on your own behalf and on your sole
responsibility, not on behalf of the original Licensor or any other Contributor,
and only if You agree to indemnify, defend, and hold each Contributor harmless
for any liability incurred by, or claims asserted against such Contributor by
the fact You have accepted any warranty or additional liability.
10. Acceptance of the Licence
The provisions of this Licence can be accepted by clicking on an icon I agree
placed under the bottom of a window displaying the text of this Licence or by
affirming consent in any other similar way, in accordance with the rules of
applicable law. Clicking on that icon indicates your clear and irrevocable
acceptance of this Licence and all of its terms and conditions.
Similarly, you irrevocably accept this Licence and all of its terms and
conditions by exercising any rights granted to You by Article 2 of this Licence,
such as the use of the Work, the creation by You of a Derivative Work or the
Distribution or Communication by You of the Work or copies thereof.
11. Information to the public
In case of any Distribution or Communication of the Work by means of electronic
communication by You (for example, by offering to download the Work from a
remote location) the distribution channel or media (for example, a website) must
at least provide to the public the information requested by the applicable law
regarding the Licensor, the Licence and the way it may be accessible, concluded,
stored and reproduced by the Licensee.
12. Termination of the Licence
The Licence and the rights granted hereunder will terminate automatically upon
any breach by the Licensee of the terms of the Licence.
Such a termination will not terminate the licences of any person who has
received the Work from the Licensee under the Licence, provided such persons
remain in full compliance with the Licence.
13. Miscellaneous
Without prejudice of Article 9 above, the Licence represents the complete
agreement between the Parties as to the Work.
If any provision of the Licence is invalid or unenforceable under applicable
law, this will not affect the validity or enforceability of the Licence as a
whole. Such provision will be construed or reformed so as necessary to make it
valid and enforceable.
The European Commission may publish other linguistic versions or new versions of
this Licence or updated versions of the Appendix, so far this is required and
reasonable, without reducing the scope of the rights granted by the Licence. New
versions of the Licence will be published with a unique version number.
All linguistic versions of this Licence, approved by the European Commission,
have identical value. Parties can take advantage of the linguistic version of
their choice.
14. Jurisdiction
Without prejudice to specific agreement between parties,
- any litigation resulting from the interpretation of this License, arising
between the European Union institutions, bodies, offices or agencies, as a
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
of Justice of the European Union, as laid down in article 272 of the Treaty on
the Functioning of the European Union,
- any litigation arising between other parties and resulting from the
interpretation of this License, will be subject to the exclusive jurisdiction
of the competent court where the Licensor resides or conducts its primary
business.
15. Applicable Law
Without prejudice to specific agreement between parties,
- this Licence shall be governed by the law of the European Union Member State
where the Licensor has his seat, resides or has his registered office,
- this licence shall be governed by Belgian law if the Licensor has no seat,
residence or registered office inside a European Union Member State.
Appendix
Compatible Licences according to Article 5 EUPL are:
- GNU General Public License (GPL) v. 2, v. 3
- GNU Affero General Public License (AGPL) v. 3
- Open Software License (OSL) v. 2.1, v. 3.0
- Eclipse Public License (EPL) v. 1.0
- CeCILL v. 2.0, v. 2.1
- Mozilla Public Licence (MPL) v. 2
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
works other than software
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
Reciprocity (LiLiQ-R+).
The European Commission may update this Appendix to later versions of the above
licences without producing a new version of the EUPL, as long as they provide
the rights granted in Article 2 of this Licence and protect the covered Source
Code from exclusive appropriation.
All other changes or additions to this Appendix require the production of a new
EUPL version.

72
README.md Normal file
View file

@ -0,0 +1,72 @@
# `Geekeey.Request`
Simple mediator implementation in .NET with minimal dependencies.
## Features
- **Simple interfaces:** no complex constraints, just marker interfaces that work.
- **Minmal dependencies:** only depends on `Microsoft.Extensions.DependencyInjection.Abstractions` and the
`Microsoft.Extensions.Options` package.
## Getting Started
### Install the NuGet package:
```shell
dotnet add package Geekeey.Request
```
You may need to add our NuGet feed to your `nuget.config` this can be done by running the following command:
```shell
dotnet nuget add source -n geekeey https://code.geekeey.de/api/packages/geekeey/nuget/index.json
```
### Usage
```csharp
public static Task<int> Main()
{
var collection = new ServiceCollection();
collection.AddRequestDispatcher(builder => builder
.Add(typeof(ScalarHandler))
.Add(typeof(ScalarBehavior)));
await using var provider = collection.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new ScalarRequest { Value = "Hello" };
var result = await dispatcher.DispatchAsync(request);
Console.WriteLine(result);
return 0;
}
public class ScalarRequest : IScalarRequest<string>
{
public string Value { get; set; } = string.Empty;
}
public class ScalarHandler : IScalarRequestHandler<ScalarTestRequest, string>
{
public Task<string> HandleAsync(ScalarTestRequest request, CancellationToken cancellationToken)
{
return Task.FromResult($"{request.Value} World");
}
}
public class ScalarBehavior : IScalarRequestBehavior<ScalarTestRequest, string>
{
public async Task<string> HandleAsync(ScalarTestRequest request, ScalarHandlerDelegate<string> next, CancellationToken cancellationToken)
{
Console.WriteLine("Before");
var result = await next(request, cancellationToken);
Console.WriteLine("After");
return result;
}
}
```
## Behaviour of the Handlers
Handlers are resolved from either the DI conatiner or are created on the fly but can receive arguments from the DI
container when being constructed. The same also applied for the request pipeline behaviours.

11
global.json Normal file
View file

@ -0,0 +1,11 @@
{
"$schema": "https://www.schemastore.org/global.json",
"sdk": {
"version": "10.0.0",
"rollForward": "latestMinor"
},
"msbuild-sdks": {},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}

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://code.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>

6
request.slnx Normal file
View file

@ -0,0 +1,6 @@
<Solution>
<Project Path="src/request/Geekeey.Request.csproj" />
<Project Path="src/request.tests/Geekeey.Request.Tests.csproj" />
<Project Path="src/request.result/Geekeey.Request.Result.csproj" />
<Project Path="src/request.result.tests/Geekeey.Request.Result.Tests.csproj" />
</Solution>

View file

@ -0,0 +1,9 @@
[*.{cs,vb}]
# disable CA1822: Mark members as static
# -> TUnit requiring instance methods for test cases
dotnet_diagnostic.CA1822.severity = none
# disable CA1707: Identifiers should not contain underscores
dotnet_diagnostic.CA1707.severity = none
# disable IDE0060: Remove unused parameter
dotnet_diagnostic.IDE0060.severity = none

View file

@ -0,0 +1,27 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Result.Tests;
internal sealed class ErrorTests
{
[Test]
public async Task I_can_implicitly_convert_from_string_and_get_string_error()
{
Error error = "error";
using var scope = Assert.Multiple();
await Assert.That(error).IsTypeOf<StringError>();
await Assert.That(error.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_implicitly_convert_from_exception_and_get_exception_error()
{
Error error = new CustomTestException();
using var scope = Assert.Multiple();
var instance = await Assert.That(error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
}

View file

@ -0,0 +1,50 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Result.Tests;
internal sealed class ExtensionsEnumerableTests
{
[Test]
public async Task I_can_join_sequence_and_get_all_success_when_all_elements_are_success()
{
IEnumerable<Result<int>> xs = [1, 2, 3, 4, 5];
var result = xs.Join();
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEquivalentTo([1, 2, 3, 4, 5]);
}
[Test]
public async Task I_can_join_sequence_and_get_first_failure_when_sequence_contains_failure()
{
IEnumerable<Result<int>> xs =
[
Prelude.Success(1),
Prelude.Success(2),
Prelude.Failure<int>("error 1"),
Prelude.Success(4),
Prelude.Failure<int>("error 2")
];
var result = xs.Join();
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error 1");
}
[Test]
public async Task I_can_join_empty_sequence_and_get_success()
{
IEnumerable<Result<int>> xs = [];
var result = xs.Join();
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEmpty();
}
}

View file

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TUnit" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\request.result\Geekeey.Request.Result.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,100 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Result.Tests;
internal sealed class PreludeTests
{
[Test]
public async Task I_can_try_with_success_value_and_get_a_success_result()
{
var result = Prelude.Try(() => 2);
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo(2);
}
[Test]
public async Task I_can_try_with_throwing_exception_and_get_a_failure_result()
{
var result = Prelude.Try<int>(() => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_with_async_success_value_and_get_a_success_result()
{
var result = await Prelude.TryAsync(() => Task.FromResult(2));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo(2);
}
[Test]
public async Task I_can_try_with_async_throwing_exception_and_get_a_failure_result()
{
var result = await Prelude.TryAsync(Task<int> () => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_with_async_await_throwing_exception_and_get_a_failure_result()
{
var result = await Prelude.TryAsync(async Task<int> () =>
{
await Task.CompletedTask;
throw new CustomTestException();
});
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_with_async_success_value_and_get_a_success_result_of_type_ValueTask()
{
var result = await Prelude.TryAsync(() => ValueTask.FromResult(2));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo(2);
}
[Test]
public async Task I_can_try_with_async_throwing_exception_and_get_a_failure_result_of_type_ValueTask()
{
var result = await Prelude.TryAsync(ValueTask<int> () => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_with_async_await_throwing_exception_and_get_a_failure_result_of_type_ValueTask()
{
var result = await Prelude.TryAsync(async ValueTask<int> () =>
{
await Task.CompletedTask;
throw new CustomTestException();
});
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
}

View file

@ -0,0 +1,64 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Result.Tests;
internal sealed class ResultConversionTests
{
[Test]
public async Task I_can_implicitly_convert_from_value_and_get_success()
{
var result = Prelude.Success(2);
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.IsFailure).IsFalse();
await Assert.That(result.Value).IsEqualTo(2);
}
[Test]
public async Task I_can_implicitly_convert_from_error_and_get_failure()
{
var error = new CustomTestError();
var result = Prelude.Failure<int>(error);
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.IsFailure).IsTrue();
await Assert.That(result.Error).IsTypeOf<CustomTestError>();
}
[Test]
public async Task I_can_unwrap_and_get_value_for_success()
{
var result = Prelude.Success(2);
var value = result.Unwrap();
await Assert.That(value).IsEqualTo(2);
}
[Test]
public async Task I_can_unwrap_and_get_exception_for_failure()
{
var result = Prelude.Failure<int>("error");
await Assert.That(result.Unwrap).Throws<UnwrapException>();
}
[Test]
public async Task I_can_explicitly_convert_and_get_value_for_success()
{
var result = Prelude.Success(2);
var value = (int)result;
await Assert.That(value).IsEqualTo(2);
}
[Test]
public async Task I_can_explicitly_convert_and_get_exception_for_failure()
{
var result = Prelude.Failure<int>("error");
await Assert.That(() => (int)result).Throws<UnwrapException>();
}
}

View file

@ -0,0 +1,175 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Result.Tests;
internal sealed class ResultEqualityTests
{
[Test]
public async Task I_can_equal_t_and_get_true_for_success_with_equal_value()
{
var a = Prelude.Success(2);
var b = 2;
await Assert.That(a.Equals(b)).IsTrue();
}
[Test]
public async Task I_can_equal_t_and_get_false_for_success_with_unequal_value()
{
var a = Prelude.Success(2);
var b = 3;
await Assert.That(a.Equals(b)).IsFalse();
}
[Test]
public async Task I_can_equal_t_and_get_false_for_failure()
{
var a = Prelude.Failure<int>("error");
var b = 2;
await Assert.That(a.Equals(b)).IsFalse();
}
[Test]
public async Task I_can_equal_result_and_get_true_for_success_and_success_with_equal_value()
{
var a = Prelude.Success(2);
var b = Prelude.Success(2);
await Assert.That(a.Equals(b)).IsTrue();
}
[Test]
public async Task I_can_equal_result_and_get_false_for_success_and_success_with_unequal_value()
{
var a = Prelude.Success(2);
var b = Prelude.Success(3);
await Assert.That(a.Equals(b)).IsFalse();
}
[Test]
public async Task I_can_equals_result_and_get_false_for_success_and_failure()
{
var a = Prelude.Success(2);
var b = Prelude.Failure<int>("error 1");
await Assert.That(a.Equals(b)).IsFalse();
}
[Test]
public async Task I_can_equals_result_and_get_false_for_failure_and_success()
{
var a = Prelude.Failure<int>("error");
var b = Prelude.Success(2);
await Assert.That(a.Equals(b)).IsFalse();
}
[Test]
public async Task I_can_equals_result_and_get_true_for_failure_and_failure()
{
var a = Prelude.Failure<int>("error 1");
var b = Prelude.Failure<int>("error 2");
await Assert.That(a.Equals(b)).IsTrue();
}
[Test]
public async Task I_can_equal_t_and_get_true_for_success_with_equal_value_using_comparer()
{
var a = Prelude.Success(2);
var b = 2;
await Assert.That(a.Equals(b, EqualityComparer<int>.Default)).IsTrue();
}
[Test]
public async Task I_can_equal_t_and_get_false_for_success_with_unequal_value_using_comparer()
{
var a = Prelude.Success(2);
var b = 3;
await Assert.That(a.Equals(b, EqualityComparer<int>.Default)).IsFalse();
}
[Test]
public async Task I_can_equal_t_and_get_false_for_failure_using_comparer()
{
var a = Prelude.Failure<int>("error");
var b = 2;
await Assert.That(a.Equals(b, EqualityComparer<int>.Default)).IsFalse();
}
[Test]
public async Task I_can_equal_result_and_get_true_for_success_and_success_with_equal_value_using_comparer()
{
var a = Prelude.Success(2);
var b = Prelude.Success(2);
await Assert.That(a.Equals(b, EqualityComparer<int>.Default)).IsTrue();
}
[Test]
public async Task I_can_equal_result_and_get_false_for_success_and_success_with_unequal_value_using_comparer()
{
var a = Prelude.Success(2);
var b = Prelude.Success(3);
await Assert.That(a.Equals(b, EqualityComparer<int>.Default)).IsFalse();
}
[Test]
public async Task I_can_equals_result_and_get_false_for_success_and_failure_using_comparer()
{
var a = Prelude.Success(2);
var b = Prelude.Failure<int>("error 1");
await Assert.That(a.Equals(b, EqualityComparer<int>.Default)).IsFalse();
}
[Test]
public async Task I_can_equals_result_and_get_false_for_failure_and_success_using_comparer()
{
var a = Prelude.Failure<int>("error");
var b = Prelude.Success(2);
await Assert.That(a.Equals(b, EqualityComparer<int>.Default)).IsFalse();
}
[Test]
public async Task I_can_equals_result_and_get_true_for_failure_and_failure_using_comparer()
{
var a = Prelude.Failure<int>("error 1");
var b = Prelude.Failure<int>("error 2");
await Assert.That(a.Equals(b, EqualityComparer<int>.Default)).IsTrue();
}
[Test]
public async Task I_can_get_hashcode_and_get_hashcode_for_success()
{
var result = Prelude.Success(2);
await Assert.That(result.GetHashCode()).IsEqualTo(2.GetHashCode());
}
[Test]
public async Task I_can_get_hashcode_and_get_zero_for_null()
{
var result = Prelude.Success<string?>(null);
await Assert.That(result.GetHashCode()).IsZero();
}
[Test]
public async Task I_can_get_hashcode_and_get_zero_for_failure()
{
var result = Prelude.Failure<int>("error");
await Assert.That(result.GetHashCode()).IsZero();
}
}

View file

@ -0,0 +1,232 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Result.Tests;
internal sealed class ResultMatchingTests
{
[Test]
public async Task I_can_match_and_it_calls_success_func_for_success()
{
var result = Prelude.Success(2);
var match = result.Match(
v => v,
_ => throw new InvalidOperationException());
await Assert.That(match).IsEqualTo(2);
}
[Test]
public async Task I_can_match_and_it_calls_failure_func_for_failure()
{
var result = Prelude.Failure<int>("error");
var match = result.Match(
_ => throw new InvalidOperationException(),
e => e);
await Assert.That(match.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_switch_and_it_calls_success_action_for_success()
{
var called = false;
var value = default(int);
var result = Prelude.Success(2);
result.Switch(OnSuccess, OnFailure);
await Assert.That(called).IsTrue();
await Assert.That(value).IsEqualTo(2);
return;
void OnSuccess(int i)
{
value = i;
called = true;
}
void OnFailure(Error e)
{
throw new InvalidOperationException();
}
}
[Test]
public async Task I_can_switch_and_it_calls_failure_action_for_failure()
{
var called = false;
var value = default(Error);
var result = Prelude.Failure<int>("error");
result.Switch(OnSuccess, OnFailure);
await Assert.That(called).IsTrue();
await Assert.That(value?.Message).IsEqualTo("error");
return;
void OnSuccess(int i)
{
throw new InvalidOperationException();
}
void OnFailure(Error e)
{
value = e;
called = true;
}
}
[Test]
public async Task I_can_match_async_and_it_calls_success_func_for_success()
{
var result = Prelude.Success(2);
var match = await result.MatchAsync(
Task.FromResult,
_ => throw new InvalidOperationException());
await Assert.That(match).IsEqualTo(2);
}
[Test]
public async Task I_can_match_async_and_it_calls_failure_func_for_failure()
{
var result = Prelude.Failure<int>("error");
var match = await result.MatchAsync(
_ => throw new InvalidOperationException(),
Task.FromResult);
await Assert.That(match.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_switch_async_and_it_calls_success_action_for_success()
{
var called = false;
var value = default(int);
var result = Prelude.Success(2);
await result.SwitchAsync(OnSuccess, OnFailure);
await Assert.That(called).IsTrue();
await Assert.That(value).IsEqualTo(2);
return;
Task OnSuccess(int i)
{
value = i;
called = true;
return Task.CompletedTask;
}
Task OnFailure(Error e)
{
throw new InvalidOperationException();
}
}
[Test]
public async Task I_can_switch_async_and_it_calls_failure_action_for_failure()
{
var called = false;
var value = default(Error);
var result = Prelude.Failure<int>("error");
await result.SwitchAsync(OnSuccess, OnFailure);
await Assert.That(called).IsTrue();
await Assert.That(value?.Message).IsEqualTo("error");
return;
Task OnSuccess(int i)
{
throw new InvalidOperationException();
}
Task OnFailure(Error e)
{
value = e;
called = true;
return Task.CompletedTask;
}
}
[Test]
public async Task I_can_match_and_it_calls_success_func_for_success_ValueTask()
{
var result = Prelude.Success(2);
var match = await result.MatchAsync(
ValueTask.FromResult,
_ => throw new InvalidOperationException());
await Assert.That(match).IsEqualTo(2);
}
[Test]
public async Task I_can_match_async_and_it_calls_failure_func_for_failure_ValueTask()
{
var result = Prelude.Failure<int>("error");
var match = await result.MatchAsync(
_ => throw new InvalidOperationException(),
ValueTask.FromResult);
await Assert.That(match.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_switch_async_and_it_calls_success_action_for_success_ValueTask()
{
var called = false;
var value = default(int);
var result = Prelude.Success(2);
await result.SwitchAsync(OnSuccess, OnFailure);
await Assert.That(called).IsTrue();
await Assert.That(value).IsEqualTo(2);
return;
ValueTask OnSuccess(int i)
{
value = i;
called = true;
return ValueTask.CompletedTask;
}
ValueTask OnFailure(Error e)
{
throw new InvalidOperationException();
}
}
[Test]
public async Task I_can_switch_async_and_it_calls_failure_action_for_failure_ValueTask()
{
var called = false;
var value = default(Error);
var result = Prelude.Failure<int>("error");
await result.SwitchAsync(OnSuccess, OnFailure);
await Assert.That(called).IsTrue();
await Assert.That(value?.Message).IsEqualTo("error");
return;
ValueTask OnSuccess(int i)
{
throw new InvalidOperationException();
}
ValueTask OnFailure(Error e)
{
value = e;
called = true;
return ValueTask.CompletedTask;
}
}
}

View file

@ -0,0 +1,59 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Result.Tests;
internal sealed class ResultTests
{
[Test]
public async Task I_can_create_new_success_result_from_t()
{
var result = new Result<int>(1);
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.IsFailure).IsFalse();
await Assert.That(result.Value).IsNotEqualTo(default);
await Assert.That(result.Error).IsNull();
}
[Test]
public async Task I_can_create_new_failure_result_from_error()
{
var result = new Result<int>(new CustomTestError());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.IsFailure).IsTrue();
await Assert.That(result.Value).IsEqualTo(default(int));
await Assert.That(result.Error).IsTypeOf<CustomTestError>();
}
[Test]
public async Task I_can_distinguish_default_result_from_created()
{
var result = default(Result<int>);
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.IsFailure).IsTrue();
await Assert.That(result.Value).IsEqualTo(default(int));
await Assert.That(result.Error).IsEqualTo(Error.DefaultValueError);
}
[Test]
public async Task I_can_to_string_success_result_value()
{
Result<int> result = 2;
await Assert.That(result.ToString()).IsEqualTo("Success { 2 }");
}
[Test]
public async Task I_can_to_string_failure_result_value()
{
Result<int> result = new StringError("error");
await Assert.That(result.ToString()).IsEqualTo("Failure { error }");
}
}

View file

@ -0,0 +1,644 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Globalization;
namespace Geekeey.Request.Result.Tests;
internal sealed class ResultTransformTests
{
[Test]
public async Task I_can_map_and_it_returns_success_for_success()
{
var start = Prelude.Success(2);
var result = start.Map(value => value.ToString(CultureInfo.InvariantCulture));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo("2");
}
[Test]
public async Task I_can_map_and_it_returns_failure_for_failure()
{
var start = Prelude.Failure<int>("error");
var result = start.Map(value => value.ToString(CultureInfo.InvariantCulture));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_transform_result_with_then_and_it_returns_success_for_success_and_mapping_returning_success()
{
var start = Prelude.Success(2);
var result = start.Then(value => Prelude.Success(value.ToString(CultureInfo.InvariantCulture)));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo("2");
}
[Test]
public async Task I_can_transform_result_with_then_and_it_returns_failure_for_success_and_mapping_returning_failure()
{
var start = Prelude.Success(2);
var result = start.Then(_ => Prelude.Failure<string>("error"));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_transform_result_with_then_and_it_returns_failure_for_failure_and_mapping_returning_success()
{
var start = Prelude.Failure<int>("error");
var result = start.Then(value => Prelude.Success(value.ToString(CultureInfo.InvariantCulture)));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_transform_result_with_then_and_it_returns_failure_for_failure_and_mapping_returning_failure()
{
var start = Prelude.Failure<int>("error");
var result = start.Then(_ => Prelude.Failure<int>("error 2"));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_map_and_it_returns_success_for_success_without_throwing()
{
var start = Prelude.Success(2);
var result = start.TryMap(value => value.ToString(CultureInfo.InvariantCulture));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo("2");
}
[Test]
public async Task I_can_try_map_and_it_returns_failure_for_failure_without_throwing()
{
var start = Prelude.Failure<int>("error");
var result = start.TryMap(value => value.ToString(CultureInfo.InvariantCulture));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_map_and_it_returns_failure_for_success_with_throwing()
{
var start = Prelude.Success(2);
var result = start.TryMap<string>(_ => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_map_and_it_returns_failure_for_failure_with_throwing()
{
var start = Prelude.Failure<int>("error");
var result = start.TryMap<string>(_ => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error).IsTypeOf<StringError>();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_transform_result_with_then_and_it_returns_success_for_success_and_mapping_returning_success()
{
var start = Prelude.Success(2);
var result = start.ThenTry(value => Prelude.Success(value.ToString(CultureInfo.InvariantCulture)));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo("2");
}
[Test]
public async Task I_can_try_transform_result_with_then_and_it_returns_failure_for_success_and_mapping_returning_failure()
{
var start = Prelude.Success(2);
var result = start.ThenTry(_ => Prelude.Failure<string>("error"));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_transform_result_with_then_and_it_returns_failure_for_failure_and_mapping_returning_failure()
{
var start = Prelude.Failure<int>("error");
var result = start.ThenTry(x => Prelude.Success(x.ToString(CultureInfo.InvariantCulture)));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_transform_result_with_then_and_it_returns_failure_for_success_and_mapping_throwing()
{
var start = Prelude.Success(2);
var result = start.ThenTry<string>(_ => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_transform_result_with_then_and_it_returns_failure_for_failure_and_mapping_throwing()
{
var start = Prelude.Failure<int>("error");
var result = start.ThenTry<string>(_ => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error).IsTypeOf<StringError>();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_map_async_and_it_returns_success_for_success()
{
var start = Prelude.Success(2);
var result = await start.MapAsync(value => Task.FromResult(value.ToString(CultureInfo.InvariantCulture)));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo("2");
}
[Test]
public async Task I_can_map_async_and_it_returns_failure_for_failure()
{
var start = Prelude.Failure<int>("error");
var result = await start.MapAsync(value => Task.FromResult(value.ToString(CultureInfo.InvariantCulture)));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_transform_result_async_with_then_and_it_returns_success_for_success_and_mapping_returning_success()
{
var start = Prelude.Success(2);
var result = await start.ThenAsync(value => Task.FromResult(Prelude.Success(value.ToString(CultureInfo.InvariantCulture))));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo("2");
}
[Test]
public async Task I_can_transform_result_async_with_then_and_it_returns_failure_for_success_and_mapping_returning_failure()
{
var start = Prelude.Success(2);
var result = await start.ThenAsync(_ => Task.FromResult(Prelude.Failure<string>("error")));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_transform_result_async_with_then_and_it_returns_failure_for_failure_and_mapping_returning_success()
{
var start = Prelude.Failure<int>("error");
var result = await start.ThenAsync(value => Task.FromResult(Prelude.Success(value.ToString(CultureInfo.InvariantCulture))));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_transform_result_async_with_then_and_it_returns_failure_for_failure_and_mapping_returning_failure()
{
var start = Prelude.Failure<int>("error");
var result = await start.ThenAsync(_ => Task.FromResult(Prelude.Failure<int>("error 2")));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_map_async_and_it_returns_success_for_success_without_throwing()
{
var start = Prelude.Success(2);
var result = await start.TryMapAsync(value => Task.FromResult(value.ToString(CultureInfo.InvariantCulture)));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo("2");
}
[Test]
public async Task I_can_try_map_async_and_it_returns_failure_for_failure_without_throwing()
{
var start = Prelude.Failure<int>("error");
var result = await start.TryMapAsync(value => Task.FromResult(value.ToString(CultureInfo.InvariantCulture)));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_map_async_and_it_returns_failure_for_success_with_throwing()
{
var start = Prelude.Success(2);
var result = await start.TryMapAsync(Task<string> (_) => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_map_async_and_it_returns_failure_for_success_with_await_throwing()
{
var start = Prelude.Success(2);
var result = await start.TryMapAsync(async Task<string> (_) =>
{
await Task.CompletedTask;
throw new CustomTestException();
});
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_map_async_and_it_returns_failure_for_failure_with_throwing()
{
var start = Prelude.Failure<int>("error");
var result = await start.TryMapAsync(Task<string> (_) => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error).IsTypeOf<StringError>();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_map_async_and_it_returns_failure_for_failure_with_await_throwing()
{
var start = Prelude.Failure<int>("error");
var result = await start.TryMapAsync(async Task<string> (_) =>
{
await Task.CompletedTask;
throw new CustomTestException();
});
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error).IsTypeOf<StringError>();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_success_for_success_and_mapping_returning_success()
{
var start = Prelude.Success(2);
var result = await start.ThenTryAsync(value => Task.FromResult(Prelude.Success(value.ToString(CultureInfo.InvariantCulture))));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo("2");
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_failure_for_success_and_mapping_returning_failure()
{
var start = Prelude.Success(2);
var result = await start.ThenTryAsync(_ => Task.FromResult(Prelude.Failure<string>("error")));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_failure_for_failure_and_mapping_returning_failure()
{
var start = Prelude.Failure<int>("error");
var result = await start.ThenTryAsync(x => Task.FromResult(Prelude.Success(x.ToString(CultureInfo.InvariantCulture))));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_failure_for_success_and_mapping_throwing()
{
var start = Prelude.Success(2);
var result = await start.ThenTryAsync(Task<Result<string>> (_) => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_failure_for_success_and_mapping_await_throwing()
{
var start = Prelude.Success(2);
var result = await start.ThenTryAsync(async Task<Result<string>> (_) =>
{
await Task.CompletedTask;
throw new CustomTestException();
});
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_failure_for_failure_and_mapping_throwing()
{
var start = Prelude.Failure<int>("error");
var result = await start.ThenTryAsync(Task<Result<string>> (_) => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error).IsTypeOf<StringError>();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_failure_for_failure_and_mapping_await_throwing()
{
var start = Prelude.Failure<int>("error");
var result = await start.ThenTryAsync(async Task<Result<string>> (_) =>
{
await Task.CompletedTask;
throw new CustomTestException();
});
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error).IsTypeOf<StringError>();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_map_async_and_it_returns_success_for_success_ValueTask()
{
var start = Prelude.Success(2);
var result = await start.MapAsync(value => ValueTask.FromResult(value.ToString(CultureInfo.InvariantCulture)));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo("2");
}
[Test]
public async Task I_can_map_async_and_it_returns_failure_for_failure_ValueTask()
{
var start = Prelude.Failure<int>("error");
var result = await start.MapAsync(value => ValueTask.FromResult(value.ToString(CultureInfo.InvariantCulture)));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_transform_result_async_with_then_and_it_returns_success_for_success_and_mapping_returning_success_ValueTask()
{
var start = Prelude.Success(2);
var result = await start.ThenAsync(value => ValueTask.FromResult(Prelude.Success(value.ToString(CultureInfo.InvariantCulture))));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo("2");
}
[Test]
public async Task I_can_transform_result_async_with_then_and_it_returns_failure_for_success_and_mapping_returning_failure_ValueTask()
{
var start = Prelude.Success(2);
var result = await start.ThenAsync(_ => ValueTask.FromResult(Prelude.Failure<string>("error")));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_transform_result_async_with_then_and_it_returns_failure_for_failure_and_mapping_returning_success_ValueTask()
{
var start = Prelude.Failure<int>("error");
var result = await start.ThenAsync(value => ValueTask.FromResult(Prelude.Success(value.ToString(CultureInfo.InvariantCulture))));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_transform_result_async_with_then_and_it_returns_failure_for_failure_and_mapping_returning_failure_ValueTask()
{
var start = Prelude.Failure<int>("error");
var result = await start.ThenAsync(_ => ValueTask.FromResult(Prelude.Failure<int>("error 2")));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_map_async_and_it_returns_success_for_success_without_throwing_ValueTask()
{
var start = Prelude.Success(2);
var result = await start.TryMapAsync(value => ValueTask.FromResult(value.ToString(CultureInfo.InvariantCulture)));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo("2");
}
[Test]
public async Task I_can_try_map_async_and_it_returns_failure_for_failure_without_throwing_ValueTask()
{
var start = Prelude.Failure<int>("error");
var result = await start.TryMapAsync(value => ValueTask.FromResult(value.ToString(CultureInfo.InvariantCulture)));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_map_async_and_it_returns_failure_for_success_with_throwing_ValueTask()
{
var start = Prelude.Success(2);
var result = await start.TryMapAsync(ValueTask<string> (_) => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_map_async_and_it_returns_failure_for_success_with_await_throwing_ValueTask()
{
var start = Prelude.Success(2);
var result = await start.TryMapAsync(async ValueTask<string> (_) =>
{
await ValueTask.CompletedTask;
throw new CustomTestException();
});
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_map_async_and_it_returns_failure_for_failure_with_throwing_ValueTask()
{
var start = Prelude.Failure<int>("error");
var result = await start.TryMapAsync(ValueTask<string> (_) => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error).IsTypeOf<StringError>();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_map_async_and_it_returns_failure_for_failure_with_await_throwing_ValueTask()
{
var start = Prelude.Failure<int>("error");
var result = await start.TryMapAsync(async ValueTask<string> (_) =>
{
await ValueTask.CompletedTask;
throw new CustomTestException();
});
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error).IsTypeOf<StringError>();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_success_for_success_and_mapping_returning_success_ValueTask()
{
var start = Prelude.Success(2);
var result = await start.ThenTryAsync(value => ValueTask.FromResult(Prelude.Success(value.ToString(CultureInfo.InvariantCulture))));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsTrue();
await Assert.That(result.Value).IsEqualTo("2");
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_failure_for_success_and_mapping_returning_failure_ValueTask()
{
var start = Prelude.Success(2);
var result = await start.ThenTryAsync(_ => ValueTask.FromResult(Prelude.Failure<string>("error")));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_failure_for_failure_and_mapping_returning_failure_ValueTask()
{
var start = Prelude.Failure<int>("error");
var result = await start.ThenTryAsync(x => ValueTask.FromResult(Prelude.Success(x.ToString(CultureInfo.InvariantCulture))));
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_failure_for_success_and_mapping_throwing_ValueTask()
{
var start = Prelude.Success(2);
var result = await start.ThenTryAsync(ValueTask<Result<string>> (_) => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_failure_for_success_and_mapping_await_throwing_ValueTask()
{
var start = Prelude.Success(2);
var result = await start.ThenTryAsync(async ValueTask<Result<string>> (_) =>
{
await ValueTask.CompletedTask;
throw new CustomTestException();
});
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
var instance = await Assert.That(result.Error).IsTypeOf<ExceptionError>();
await Assert.That(instance?.Exception).IsTypeOf<CustomTestException>();
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_failure_for_failure_and_mapping_throwing_ValueTask()
{
var start = Prelude.Failure<int>("error");
var result = await start.ThenTryAsync(ValueTask<Result<string>> (_) => throw new CustomTestException());
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error).IsTypeOf<StringError>();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_transform_result_async_with_then_and_it_returns_failure_for_failure_and_mapping_await_throwing_ValueTask()
{
var start = Prelude.Failure<int>("error");
var result = await start.ThenTryAsync(async ValueTask<Result<string>> (_) =>
{
await ValueTask.CompletedTask;
throw new CustomTestException();
});
using var scope = Assert.Multiple();
await Assert.That(result.IsSuccess).IsFalse();
await Assert.That(result.Error).IsTypeOf<StringError>();
await Assert.That(result.Error?.Message).IsEqualTo("error");
}
}

View file

@ -0,0 +1,99 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Result.Tests;
internal sealed class ResultUnboxTests
{
[Test]
public async Task I_can_try_get_value_and_it_returns_true_and_sets_value_for_success_with_1_param()
{
var result = Prelude.Success(2);
var ok = result.TryGetValue(out int value);
using var scope = Assert.Multiple();
await Assert.That(ok).IsTrue();
await Assert.That(value).IsEqualTo(2);
}
[Test]
public async Task I_can_try_get_value_and_it_returns_false_for_failure_with_1_param()
{
var result = Prelude.Failure<int>("error");
var ok = result.TryGetValue(out int value);
using var scope = Assert.Multiple();
await Assert.That(ok).IsFalse();
await Assert.That(value).IsEqualTo(default(int));
}
[Test]
public async Task I_can_try_get_value_and_it_returns_true_and_sets_value_for_success_with_2_param()
{
var result = Prelude.Success(2);
var ok = result.TryGetValue(out int value, out var error);
using var scope = Assert.Multiple();
await Assert.That(ok).IsTrue();
await Assert.That(value).IsEqualTo(2);
await Assert.That(error).IsEqualTo(default(Error));
}
[Test]
public async Task I_can_try_get_value_and_it_returns_false_and_sets_error_for_failure_with_2_param()
{
var result = Prelude.Failure<int>("error");
var ok = result.TryGetValue(out int value, out var error);
using var scope = Assert.Multiple();
await Assert.That(ok).IsFalse();
await Assert.That(value).IsEqualTo(default(int));
await Assert.That(error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_get_error_and_it_returns_true_and_sets_error_for_failure_with_1_param()
{
var result = Prelude.Failure<int>("error");
var ok = result.TryGetValue(out Error? error);
using var scope = Assert.Multiple();
await Assert.That(ok).IsTrue();
await Assert.That(error?.Message).IsEqualTo("error");
}
[Test]
public async Task I_can_try_get_error_and_it_returns_false_for_success_with_1_param()
{
var result = Prelude.Success(2);
var ok = result.TryGetValue(out Error? error);
using var scope = Assert.Multiple();
await Assert.That(ok).IsFalse();
await Assert.That(error).IsEqualTo(default(Error));
}
[Test]
public async Task I_can_try_get_error_and_it_returns_true_and_sets_error_for_failure_with_2_param()
{
var result = Prelude.Failure<int>("error");
var ok = result.TryGetValue(out Error? error, out var value);
using var scope = Assert.Multiple();
await Assert.That(ok).IsTrue();
await Assert.That(error?.Message).IsEqualTo("error");
await Assert.That(value).IsEqualTo(default(int));
}
[Test]
public async Task I_can_try_get_error_and_it_returns_false_and_sets_value_for_success_with_2_param()
{
var result = Prelude.Success(2);
var ok = result.TryGetValue(out Error? error, out var value);
using var scope = Assert.Multiple();
await Assert.That(ok).IsFalse();
await Assert.That(error).IsEqualTo(default(Error));
await Assert.That(value).IsEqualTo(2);
}
}

View file

@ -0,0 +1,11 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.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,8 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Result.Tests;
internal sealed class CustomTestException : Exception
{
}

View file

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>true</IsPackable>
</PropertyGroup>
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<InternalsVisibleTo Include="Geekeey.Request.Result.Tests" />
</ItemGroup>
<PropertyGroup>
<PackageReadmeFile>package-readme.md</PackageReadmeFile>
<PackageIcon>package-icon.png</PackageIcon>
<PackageProjectUrl>https://code.geekeey.de/geekeey/request/src/branch/main/src/request.result</PackageProjectUrl>
<PackageLicenseExpression>EUPL-1.2</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include=".\package-icon.png" Pack="true" PackagePath="\" Visible="false" />
<None Include=".\package-readme.md" Pack="true" PackagePath="\" Visible="false" />
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\" Visible="false" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,101 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics.Contracts;
namespace Geekeey.Request.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 a 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)
{
return new Result<T>(value);
}
/// <summary>
/// Creates a result containing a failure value.
/// </summary>
/// <typeparam name="T">The type of 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)
{
return new Result<T>(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 to 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 to 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 to 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));
}
}
}

View file

@ -0,0 +1,51 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics.Contracts;
namespace Geekeey.Request.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)
{
return new Result<T>(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)
{
return new Result<T>(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()
{
return IsSuccess ? Value : throw new UnwrapException();
}
/// <inheritdoc cref="Unwrap"/>
[Pure]
public static explicit operator T(Result<T> result)
{
return result.Unwrap();
}
}

View file

@ -0,0 +1,163 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Numerics;
namespace Geekeey.Request.Result;
public readonly partial struct Result<T> : IEquatable<Result<T>>, IEquatable<T>
{
/// <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)
{
return 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)
{
return 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)
{
return 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)
{
return Equals(this, other, comparer);
}
/// <inheritdoc/>
[Pure]
public override bool Equals(object? obj)
{
return (obj is T x && Equals(x)) || (obj is Result<T> r && Equals(r));
}
/// <inheritdoc/>
[Pure]
public override int GetHashCode()
{
return GetHashCode(this, EqualityComparer<T>.Default);
}
internal static bool Equals(Result<T> a, Result<T> b, IEqualityComparer<T> comparer)
{
if (!a.IsSuccess || !b.IsSuccess)
{
return !a.IsSuccess && !b.IsSuccess;
}
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.IsSuccess)
{
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 { IsSuccess: true, Value: not null })
{
return comparer.GetHashCode(result.Value);
}
return 0;
}
}
public readonly partial struct Result<T> : IEqualityOperators<Result<T>, Result<T>, bool>, IEqualityOperators<Result<T>, T, bool>
{
/// <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)
{
return Equals(a, b, EqualityComparer<T>.Default);
}
/// <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)
{
return !Equals(a, b, EqualityComparer<T>.Default);
}
/// <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)
{
return Equals(a, b, EqualityComparer<T>.Default);
}
/// <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)
{
return !Equals(a, b, EqualityComparer<T>.Default);
}
}

View file

@ -0,0 +1,104 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics.Contracts;
namespace Geekeey.Request.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)
{
return IsSuccess ? 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 (IsSuccess)
{
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 Task<TResult> MatchAsync<TResult>(Func<T, Task<TResult>> success, Func<Error, Task<TResult>> failure)
{
return IsSuccess ? 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 Task SwitchAsync(Func<T, Task> success, Func<Error, Task> failure)
{
return IsSuccess ? success(Value!) : 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 ValueTask<TResult> MatchAsync<TResult>(Func<T, ValueTask<TResult>> success, Func<Error, ValueTask<TResult>> failure)
{
return IsSuccess ? success(Value!) : 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 ValueTask SwitchAsync(Func<T, ValueTask> success, Func<Error, ValueTask> failure)
{
return IsSuccess ? success(Value!) : failure(Error!);
}
}

View file

@ -0,0 +1,366 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics.Contracts;
namespace Geekeey.Request.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)
{
return IsSuccess ? 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)
{
return IsSuccess ? 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="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 (!IsSuccess)
{
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 (!IsSuccess)
{
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 (!IsSuccess)
{
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 (!IsSuccess)
{
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));
}
}
}
}
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 (!IsSuccess)
{
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 (!IsSuccess)
{
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 (!IsSuccess)
{
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 (!IsSuccess)
{
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));
}
}
}
}

View file

@ -0,0 +1,66 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
namespace Geekeey.Request.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 IsSuccess;
}
/// <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 = !IsSuccess ? Error : null;
return IsSuccess;
}
/// <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 TryGetValue([MaybeNullWhen(false)] out Error error)
{
error = !IsSuccess ? Error : null;
return !IsSuccess;
}
/// <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 TryGetValue([MaybeNullWhen(false)] out Error error, [MaybeNullWhen(true)] out T value)
{
error = !IsSuccess ? Error : null;
value = Value;
return !IsSuccess;
}
}

View file

@ -0,0 +1,76 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
namespace Geekeey.Request.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>
{
/// <summary>
/// Creates a new result with a success value.
/// </summary>
/// <param name="value">The success value.</param>
public Result(T value)
{
IsSuccess = 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)
{
IsSuccess = false;
Value = default;
Error = error;
}
internal T? Value { get; }
internal Error? Error => IsSuccess ? null : (field ?? 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 { get; }
/// <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 => !IsSuccess;
/// <summary>
/// Gets a string representation of the result.
/// </summary>
[Pure]
public override string ToString()
{
return IsSuccess ? $"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;
}
}

View file

@ -0,0 +1,27 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Result;
/// <summary>
/// An error which is a combination of other errors.
/// </summary>
public sealed class AggregateError : Error
{
/// <summary>
/// An error which is a combination of other errors.
/// </summary>
/// <param name="errors">The errors the error consists of.</param>
public AggregateError(IEnumerable<Error> errors)
{
Errors = [.. errors];
}
/// <summary>
/// The errors the error consists of.
/// </summary>
public IReadOnlyCollection<Error> Errors { get; }
/// <inheritdoc/>
public override string Message => string.Join(Environment.NewLine, Errors.Select(error => error.Message));
}

View file

@ -0,0 +1,54 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.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()
{
return 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)
{
return 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)
{
return new ExceptionError(exception);
}
}

View file

@ -0,0 +1,29 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Result;
/// <summary>
/// An error which is constructed from an exception.
/// </summary>
public sealed class ExceptionError : Error
{
/// <summary>
/// An error which is constructed from an exception.
/// </summary>
/// <param name="exception">The exception in the error.</param>
public ExceptionError(Exception exception)
{
Exception = exception;
}
/// <summary>
/// The exception in the error.
/// </summary>
public Exception Exception { get; }
/// <summary>
/// The exception in the error.
/// </summary>
public override string Message => Exception.Message;
}

View file

@ -0,0 +1,24 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Result;
/// <summary>
/// An error which displays a simple string.
/// </summary>
public sealed class StringError : Error
{
private readonly string _message;
/// <summary>
/// An error which displays a simple string.
/// </summary>
/// <param name="message">The message to display.</param>
public StringError(string message)
{
_message = message;
}
/// <inheritdoc/>
public override string Message => _message;
}

View file

@ -0,0 +1,26 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.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,90 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.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 T? value, out var error))
{
return new Result<IReadOnlyList<T>>(error);
}
list.Add(value);
}
return list;
}
/// <inheritdoc cref="Join{T}(IEnumerable{Result{T}})"/>
/// <remarks>
/// For parallel execution of the async tasks, one should await the <c>Task.WhenAll()</c> of the provided list
/// before calling this function
/// </remarks>
/// <seealso cref="Join{T}(IEnumerable{Result{T}})"/>
// ReSharper disable once InconsistentNaming
public static async ValueTask<Result<IReadOnlyList<T>>> Join<T>(this IEnumerable<ValueTask<Result<T>>> results)
{
_ = results.TryGetNonEnumeratedCount(out var count);
var list = new List<T>(count);
foreach (var result in results)
{
if (!(await result).TryGetValue(out T? value, out var error))
{
return new Result<IReadOnlyList<T>>(error);
}
list.Add(value);
}
return list;
}
/// <inheritdoc cref="Join{T}(IEnumerable{Result{T}})"/>
/// <remarks>
/// For parallel execution of the async tasks, one should await the <c>Task.WhenAll()</c> of the provided list
/// before calling this function
/// </remarks>
/// <seealso cref="Join{T}(IEnumerable{Result{T}})"/>
// ReSharper disable once InconsistentNaming
public static async Task<Result<IReadOnlyList<T>>> Join<T>(this IEnumerable<Task<Result<T>>> results)
{
_ = results.TryGetNonEnumeratedCount(out var count);
var list = new List<T>(count);
foreach (var result in results)
{
if (!(await result).TryGetValue(out T? value, out var error))
{
return new Result<IReadOnlyList<T>>(error);
}
list.Add(value);
}
return list;
}
}

View file

@ -0,0 +1,100 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace Geekeey.Request.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 returning 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)]
// ReSharper disable once InconsistentNaming
public static async Task<Result<TNew>> Map<T, TNew>(this Task<Result<T>> result, Func<T, TNew> func)
{
return (await result).Map(func);
}
/// <inheritdoc cref="Map{T,TNew}(Task{Result{T}},Func{T,TNew})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async Task<Result<TNew>> MapAsync<T, TNew>(this Task<Result<T>> result, Func<T, ValueTask<TNew>> func)
{
return 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 returning 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)]
// ReSharper disable once InconsistentNaming
public static async Task<Result<TNew>> Then<T, TNew>(this Task<Result<T>> result, Func<T, Result<TNew>> func)
{
return (await result).Then(func);
}
/// <inheritdoc cref="Then{T,TNew}(Task{Result{T}},Func{T,Result{TNew}})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async Task<Result<TNew>> ThenAsync<T, TNew>(this Task<Result<T>> result, Func<T, ValueTask<Result<TNew>>> func)
{
return await (await result).ThenAsync(func);
}
#endregion
#region ValueTask<Result<T>>
/// <inheritdoc cref="Map{T,TNew}(Task{Result{T}},Func{T,TNew})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// ReSharper disable once InconsistentNaming
public static async ValueTask<Result<TNew>> Map<T, TNew>(this ValueTask<Result<T>> result, Func<T, TNew> func)
{
return (await result).Map(func);
}
/// <inheritdoc cref="Map{T,TNew}(Task{Result{T}},Func{T,TNew})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async ValueTask<Result<TNew>> MapAsync<T, TNew>(this ValueTask<Result<T>> result, Func<T, ValueTask<TNew>> func)
{
return await (await result).MapAsync(func);
}
/// <inheritdoc cref="Then{T,TNew}(Task{Result{T}},Func{T,Result{TNew}})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// ReSharper disable once InconsistentNaming
public static async ValueTask<Result<TNew>> Then<T, TNew>(this ValueTask<Result<T>> result, Func<T, Result<TNew>> func)
{
return (await result).Then(func);
}
/// <inheritdoc cref="Then{T,TNew}(Task{Result{T}},Func{T,Result{TNew}})"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static async ValueTask<Result<TNew>> ThenAsync<T, TNew>(this ValueTask<Result<T>> result, Func<T, ValueTask<Result<TNew>>> func)
{
return await (await result).ThenAsync(func);
}
#endregion
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,62 @@
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#.
## Features
- **Success and Failure States:** Represent successful outcomes with a value (`Prelude.Success()`) or failures with an
error (`Prelude.Failure()`).
- **Immutability:** `Result<T>` objects are immutable, ensuring thread safety and preventing accidental modification.
- **Chaining Operations:** Methods like `Map` and `Then` allow for chaining operations on successful results, promoting
a functional programming style.
## Getting Started
### Install the NuGet package:
```
dotnet add package Geekeey.Extensions.Result
```
You may need to add our NuGet Feed to your `nuget.config` this can be done by adding the following lines
```xml
<packageSources>
<add key="geekeey" value="https://git.geekeey.de/api/packages/geekeey/nuget/index.json" />
</packageSources>
```
### Usage
```csharp
public Result<int> Divide(int dividend, int divisor)
{
if (divisor == 0)
{
return Prelude.Failure<int>("Division by zero");
}
return Prelude.Success(dividend / divisor);
}
if (result.IsSuccess)
{
Console.WriteLine("Result: " + result.Value);
}
else
{
Console.WriteLine("Error: " + result.Error);
}
```
```csharp
_ = await Prelude.Try(() => File.ReadAllLines("i_do_not_exist.txt"))
ThenAsync(static async Task<Result<IReadOnlyList<int>>> (list) =>
{
using var client = new HttpClient();
Task<Result<int>> DoSomeThing(string line)
=> Prelude.TryAsync(() => client.GetAsync(line))
.Map(static async response => int.Parse(await response.Content.ReadAsStringAsync()));
var results = await Task.WhenAll(list.Select(DoSomeThing).ToArray());
return results.Join();
});
```

View file

@ -0,0 +1,9 @@
[*.{cs,vb}]
# disable CA1822: Mark members as static
# -> TUnit requiring instance methods for test cases
dotnet_diagnostic.CA1822.severity = none
# disable CA1707: Identifiers should not contain underscores
dotnet_diagnostic.CA1707.severity = none
# disable IDE0060: Remove unused parameter
dotnet_diagnostic.IDE0060.severity = none

View file

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TUnit" />
<!-- additional packages -->
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\request\Geekeey.Request.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,120 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Geekeey.Request.Tests;
internal sealed class RequestDispatcherBuilderExtensionsTests
{
[Test]
public async Task I_can_add_a_type_and_register_the_options()
{
// Arrange
var services = new ServiceCollection();
var builder = services.AddRequestDispatcher();
var type = typeof(TestHandler);
// Act
builder.Add(type);
// Assert
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<RequestDispatcherOptions>>().Value;
var handlers = options.GetRequestHandlers<IScalarRequestHandler<TestRequest, string>>(provider);
await Assert.That(handlers).Count().IsEqualTo(1);
await Assert.That(handlers.First()).IsTypeOf<TestHandler>();
}
[Test]
public async Task I_can_add_a_type_with_a_lifetime_and_register_the_options_and_service()
{
// Arrange
var services = new ServiceCollection();
var builder = services.AddRequestDispatcher();
var type = typeof(TestHandler);
var lifetime = ServiceLifetime.Scoped;
// Act
builder.Add(type, lifetime);
// Assert
var serviceDescriptor = services.FirstOrDefault(sd => sd.ServiceType == type);
await Assert.That(serviceDescriptor).IsNotNull();
await Assert.That(serviceDescriptor.Lifetime).IsEqualTo(lifetime);
await Assert.That(serviceDescriptor.ImplementationType).IsEqualTo(type);
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<RequestDispatcherOptions>>().Value;
var handlers = options.GetRequestHandlers<IScalarRequestHandler<TestRequest, string>>(provider);
await Assert.That(handlers).Count().IsEqualTo(1);
}
[Test]
public async Task I_can_add_an_enumerable_of_types_and_register_the_options()
{
// Arrange
var services = new ServiceCollection();
var builder = services.AddRequestDispatcher();
var types = new[] { typeof(TestHandler), typeof(AnotherTestHandler) };
// Act
builder.Add(types);
// Assert
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<RequestDispatcherOptions>>().Value;
var handlers1 = options.GetRequestHandlers<IScalarRequestHandler<TestRequest, string>>(provider);
await Assert.That(handlers1).Count().IsEqualTo(1);
await Assert.That(handlers1.First()).IsTypeOf<TestHandler>();
var handlers2 = options.GetRequestHandlers<IScalarRequestHandler<AnotherTestRequest, string>>(provider);
await Assert.That(handlers2).Count().IsEqualTo(1);
await Assert.That(handlers2.First()).IsTypeOf<AnotherTestHandler>();
}
[Test]
public async Task I_can_add_an_enumerable_of_types_with_a_lifetime_and_register_the_options_and_services()
{
// Arrange
var services = new ServiceCollection();
var builder = services.AddRequestDispatcher();
var types = new[] { typeof(TestHandler), typeof(AnotherTestHandler) };
var lifetime = ServiceLifetime.Singleton;
// Act
builder.Add(types, lifetime);
// Assert
foreach (var type in types)
{
var serviceDescriptor = services.FirstOrDefault(sd => sd.ServiceType == type);
await Assert.That(serviceDescriptor).IsNotNull();
await Assert.That(serviceDescriptor.Lifetime).IsEqualTo(lifetime);
}
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<RequestDispatcherOptions>>().Value;
await Assert.That(options.GetRequestHandlers<IScalarRequestHandler<TestRequest, string>>(provider)).Count().IsEqualTo(1);
await Assert.That(options.GetRequestHandlers<IScalarRequestHandler<AnotherTestRequest, string>>(provider)).Count().IsEqualTo(1);
}
[Test]
public async Task I_can_see_it_throw_when_the_builder_is_null()
{
IRequestDispatcherBuilder builder = null!;
using (Assert.Multiple())
{
await Assert.That(() => builder.Add(typeof(TestHandler))).Throws<ArgumentNullException>();
await Assert.That(() => builder.Add(typeof(TestHandler), ServiceLifetime.Transient)).Throws<ArgumentNullException>();
await Assert.That(() => builder.Add([typeof(TestHandler)])).Throws<ArgumentNullException>();
await Assert.That(() => builder.Add([typeof(TestHandler)], ServiceLifetime.Transient)).Throws<ArgumentNullException>();
}
}
}

View file

@ -0,0 +1,111 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Microsoft.Extensions.DependencyInjection;
namespace Geekeey.Request.Tests;
internal sealed class ScalarBehaviourTests
{
[Test]
public async Task I_can_execute_the_closed_behaviour()
{
var sc = new ServiceCollection();
sc.AddSingleton<ScalarTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(ScalarTestHandler))
.Add(typeof(ScalarTestBehavior)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<ScalarTestTracker>();
var request = new ScalarTestRequest { Value = "Hello" };
var result = await dispatcher.DispatchAsync(request);
await Assert.That(result).IsEquivalentTo("Hello-Handled");
await Assert.That(tracker.Executed).IsTrue();
}
[Test]
public async Task I_can_execute_the_open_behaviour()
{
var sc = new ServiceCollection();
sc.AddSingleton<ScalarTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(ScalarTestHandler))
.Add(typeof(ScalarOpenBehavior<,>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<ScalarTestTracker>();
var request = new ScalarTestRequest { Value = "Hello" };
var result = await dispatcher.DispatchAsync(request);
await Assert.That(result).IsEquivalentTo("Hello-Handled");
await Assert.That(tracker.Executed).IsTrue();
}
[Test]
public async Task I_can_chain_the_behaviours_in_order()
{
var sc = new ServiceCollection();
sc.AddSingleton<ScalarTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(ScalarTestHandler))
.Add(typeof(ScalarChainedBehaviour1))
.Add(typeof(ScalarChainedBehaviour2)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<ScalarTestTracker>();
var request = new ScalarTestRequest { Value = "Hello" };
await dispatcher.DispatchAsync(request);
// They are discovered in the order they appear in the assembly.
// In this file: ChainedBehaviour1, ChainedBehaviour2
await Assert.That(tracker.Log).Count().IsEqualTo(2);
await Assert.That(tracker.Log[0]).IsEquivalentTo("Behaviour1");
await Assert.That(tracker.Log[1]).IsEquivalentTo("Behaviour2");
}
[Test]
public async Task I_can_work_with_a_generic_wrapper_request_and_the_open_behaviour()
{
var sc = new ServiceCollection();
sc.AddSingleton<ScalarTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(ScalarTestWrapperHandler<>))
.Add(typeof(ScalarWrapperBehavior<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<ScalarTestTracker>();
var request = new ScalarTestWrapperRequest<int> { Item = 42 };
var result = await dispatcher.DispatchAsync(request);
await Assert.That(result).IsEquivalentTo("Handled-42");
await Assert.That(tracker.Executed).IsTrue();
}
[Test]
public async Task I_can_maintain_the_ordering_between_open_and_closed_behaviours()
{
var sc = new ServiceCollection();
sc.AddSingleton<ScalarTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(ScalarTestHandler))
.Add(typeof(ScalarOrderingOpenBehavior<,>))
.Add(typeof(ScalarOrderingClosedBehavior)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<ScalarTestTracker>();
var request = new ScalarTestRequest { Value = "Order" };
await dispatcher.DispatchAsync(request);
await Assert.That(tracker.Log).Contains("OrderingOpen");
await Assert.That(tracker.Log).Contains("OrderingClosed");
}
}
// Moved to _fixtures

View file

@ -0,0 +1,251 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Geekeey.Request.Tests;
internal sealed class ScalarDispatcherTests
{
[Test]
public async Task I_can_dispatch_a_request_async_with_an_open_generic_handler()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(OpenScalarHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new OpenScalarRequest { Data = "Hello" };
var result = await dispatcher.DispatchAsync(request);
await Assert.That(result).IsEquivalentTo("Hello-Handled");
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_open_generic_handler_that_has_constraints()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(ConstrainedScalarHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new ConstrainedScalarRequest { Value = 123 };
var result = await dispatcher.DispatchAsync(request);
await Assert.That(result).IsEquivalentTo("123-Constrained");
}
[Test]
public async Task I_can_see_it_fail_if_no_handler_is_found_even_with_an_open_generic_available()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(OpenScalarHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new UnhandledScalarRequest();
await Assert.ThrowsAsync<InvalidOperationException>(async () => await dispatcher.DispatchAsync(request));
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_inherited_request()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(OpenScalarHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
// InheritedScalarRequest : OpenScalarRequest.
// There is no Handler<InheritedScalarRequest> but there is OpenScalarHandler<TRequest> where TRequest : OpenScalarRequest.
// It should be able to handle InheritedScalarRequest.
var request = new InheritedScalarRequest { Data = "Sub" };
var result = await dispatcher.DispatchAsync(request);
await Assert.That(result).IsEquivalentTo("Sub-Handled");
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_inherited_handler()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(DerivedScalarHandler)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new DerivedScalarRequest { Value = 42 };
var result = await dispatcher.DispatchAsync(request);
await Assert.That(result).IsEquivalentTo("Derived: 42");
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_interface_inherited_request()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(InterfaceInheritedScalarHandler)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new InterfaceInheritedScalarRequest { Name = "InterfaceTest" };
var result = await dispatcher.DispatchAsync(request);
await Assert.That(result).IsEquivalentTo("InterfaceTest-InterfaceHandled");
}
[Test]
public async Task I_can_dispatch_a_request_async_with_deep_inheritance_in_the_request()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(OpenScalarHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new DeepDerivedScalarRequest { Data = "Deep", DeepValue = 99 };
var result = await dispatcher.DispatchAsync(request);
// OpenScalarHandler<TRequest> where TRequest : OpenScalarRequest should handle this
await Assert.That(result).IsEquivalentTo("Deep-Handled");
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_interface_constrained_handler()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(InterfaceInheritedScalarHandler))
.Add(typeof(InterfaceConstrainedScalarHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new InterfaceInheritedScalarRequest { Name = "Constrained" };
var result = await dispatcher.DispatchAsync(request);
// Both InterfaceInheritedScalarHandler and InterfaceConstrainedScalarHandler could match.
// InterfaceInheritedScalarHandler is a concrete match for InterfaceInheritedScalarRequest.
// InterfaceConstrainedScalarHandler is an open generic match.
// Currently Dispatcher.SendAsync checks concrete handlers first.
await Assert.That(result).IsEquivalentTo("Constrained-InterfaceHandled");
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_interface_only_match()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(InterfaceConstrainedScalarHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new AnotherNamedScalarRequest { Name = "InterfaceOnly" };
var result = await dispatcher.DispatchAsync(request);
// No concrete handler for AnotherNamedScalarRequest, but InterfaceConstrainedScalarHandler<T> where T : INamedScalarRequest matches.
await Assert.That(result).IsEquivalentTo("InterfaceOnly-ConstrainedByInterface");
}
[Test]
public async Task I_can_dispatch_a_request_async_with_a_nested_generic_request()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(WrapperScalarHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new WrapperScalarRequest<int> { Item = 42 };
var result = await dispatcher.DispatchAsync(request);
await Assert.That(result).IsEquivalentTo("Handled-42");
}
[Test]
public async Task I_can_handle_multiple_interface_implementations()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(MultiInterfaceScalarHandler)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new MultiInterfaceScalarRequest();
var result1 = await dispatcher.DispatchAsync<int>(request);
var result2 = await dispatcher.DispatchAsync<string>(request);
await Assert.That(result1).IsEqualTo(1);
await Assert.That(result2).IsEquivalentTo("One");
}
[Test]
public async Task I_can_see_it_fail_if_there_are_ambiguous_handle_methods()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(AmbiguousScalarHandler)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new AmbiguousScalarRequest();
var result = await dispatcher.DispatchAsync(request);
await Assert.That(result).IsEquivalentTo("Interface-Handled");
}
[Test]
public async Task I_can_dispatch_a_request_async_with_a_generic_interface_explicit_implementation()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(ExplicitGenericScalarHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new ExplicitGenericScalarRequest { Value = "Explicit" };
var result = await dispatcher.DispatchAsync(request);
await Assert.That(result).IsEquivalentTo("Explicit-ExplicitHandled");
}
[Test]
public async Task I_can_see_it_throw_the_original_exception()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(FailingScalarHandler)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new FailingScalarRequest();
var ex = await Assert.That(async () => await dispatcher.DispatchAsync(request)).Throws<InvalidOperationException>();
using (Assert.Multiple())
{
await Assert.That(ex?.Message).IsEquivalentTo("Handler failed");
// Assert that the stack trace contains the handler's method name,
// which proves the exception was rethrown while preserving its origin.
await Assert.That(ex?.StackTrace).Contains(nameof(FailingScalarHandler.HandleAsync));
}
}
[Test]
public async Task I_can_see_it_throw_if_dispatcher_options_are_modified_after_build()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(FailingScalarHandler)));
var provider = sc.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<RequestDispatcherOptions>>().Value;
options.GetRequestBehaviors<IScalarRequestHandler<FailingScalarRequest, string>>(default!);
await Assert.That(() => options.Inspect([])).Throws<InvalidOperationException>();
}
}
// Moved to _fixtures

View file

@ -0,0 +1,110 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Microsoft.Extensions.DependencyInjection;
namespace Geekeey.Request.Tests;
internal sealed class StreamBehaviourTests
{
[Test]
public async Task I_can_execute_the_closed_behaviour()
{
var sc = new ServiceCollection();
sc.AddSingleton<StreamTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(StreamTestHandler))
.Add(typeof(StreamTestBehavior)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<StreamTestTracker>();
var request = new StreamTestRequest { Value = "Hello" };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(results).IsEquivalentTo(["Hello-Handled-0", "Hello-Handled-1"]);
await Assert.That(tracker.Executed).IsTrue();
}
[Test]
public async Task I_can_execute_the_open_behaviour()
{
var sc = new ServiceCollection();
sc.AddSingleton<StreamTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(StreamTestHandler))
.Add(typeof(StreamOpenBehavior<,>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<StreamTestTracker>();
var request = new StreamTestRequest { Value = "Hello" };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(results).IsEquivalentTo(["Hello-Handled-0", "Hello-Handled-1"]);
await Assert.That(tracker.Executed).IsTrue();
}
[Test]
public async Task I_can_chain_the_behaviours_in_order()
{
var sc = new ServiceCollection();
sc.AddSingleton<StreamTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(StreamTestHandler))
.Add(typeof(StreamChainedBehaviour1))
.Add(typeof(StreamChainedBehaviour2)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<StreamTestTracker>();
var request = new StreamTestRequest { Value = "Hello" };
await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(tracker.Log).Count().IsEqualTo(2);
await Assert.That(tracker.Log[0]).IsEquivalentTo("Behaviour1");
await Assert.That(tracker.Log[1]).IsEquivalentTo("Behaviour2");
}
[Test]
public async Task I_can_work_with_a_generic_wrapper_request_and_the_open_behaviour()
{
var sc = new ServiceCollection();
sc.AddSingleton<StreamTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(StreamTestWrapperHandler<>))
.Add(typeof(StreamWrapperBehavior<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<StreamTestTracker>();
var request = new StreamTestWrapperRequest<int> { Item = 42 };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(results).IsEquivalentTo(["Handled-42-0", "Handled-42-1"]);
await Assert.That(tracker.Executed).IsTrue();
}
[Test]
public async Task I_can_maintain_the_ordering_between_open_and_closed_behaviours()
{
var sc = new ServiceCollection();
sc.AddSingleton<StreamTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(StreamTestHandler))
.Add(typeof(StreamOrderingOpenBehavior<,>))
.Add(typeof(StreamOrderingClosedBehavior)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<StreamTestTracker>();
var request = new StreamTestRequest { Value = "Order" };
await dispatcher.DispatchAsync(request).ToListAsync();
var log = tracker.Log.ToList();
await Assert.That(log).Contains("Open");
await Assert.That(log).Contains("Closed");
}
}
// Moved to _fixtures

View file

@ -0,0 +1,243 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Microsoft.Extensions.DependencyInjection;
namespace Geekeey.Request.Tests;
public class StreamDispatcherTests
{
[Test]
public async Task I_can_dispatch_a_request_async_with_an_open_generic_handler()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(OpenStreamHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new OpenStreamRequest { Data = "Hello" };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(results).IsEquivalentTo(["Hello-Stream-0", "Hello-Stream-1"]);
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_open_generic_handler_that_has_constraints()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(ConstrainedStreamHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new ConstrainedStreamRequest { Value = 123 };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(results).IsEquivalentTo(["123-Constrained-0", "123-Constrained-1"]);
}
[Test]
public async Task I_can_see_it_fail_if_no_handler_is_found_even_with_an_open_generic_available()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(OpenStreamHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new UnhandledStreamRequest();
await Assert.ThrowsAsync<InvalidOperationException>(async () => await dispatcher.DispatchAsync(request).FirstOrDefaultAsync().AsTask());
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_inherited_request()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(OpenStreamHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new InheritedStreamRequest { Data = "Sub" };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(results).IsEquivalentTo(["Sub-Stream-0", "Sub-Stream-1"]);
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_inherited_handler()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(DerivedStreamHandler)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new DerivedStreamRequest { Value = 42 };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(results).IsEquivalentTo(["Derived: 42-0", "Derived: 42-1"]);
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_interface_inherited_request()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(InterfaceInheritedStreamHandler)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new InterfaceInheritedStreamRequest { Name = "InterfaceTest" };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(results).IsEquivalentTo(["InterfaceTest-InterfaceHandled-0", "InterfaceTest-InterfaceHandled-1"]);
}
[Test]
public async Task I_can_dispatch_a_request_async_with_deep_inheritance_in_the_request()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(OpenStreamHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new DeepDerivedStreamRequest { Data = "Deep", DeepValue = 99 };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(results).IsEquivalentTo(["Deep-Stream-0", "Deep-Stream-1"]);
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_interface_constrained_handler()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(InterfaceInheritedStreamHandler))
.Add(typeof(InterfaceConstrainedStreamHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new InterfaceInheritedStreamRequest { Name = "Constrained" };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
// Both InterfaceInheritedStreamHandler and InterfaceConstrainedStreamHandler could match.
// InterfaceInheritedStreamHandler is a concrete match for InterfaceInheritedStreamRequest.
// InterfaceConstrainedStreamHandler is an open generic match.
// Currently Dispatcher checks concrete handlers first.
await Assert.That(results).IsEquivalentTo(["Constrained-InterfaceHandled-0", "Constrained-InterfaceHandled-1"]);
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_interface_only_match()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(InterfaceConstrainedStreamHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new AnotherNamedStreamRequest { Name = "InterfaceOnly" };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(results).IsEquivalentTo(["InterfaceOnly-ConstrainedByInterface-0", "InterfaceOnly-ConstrainedByInterface-1"]);
}
[Test]
public async Task I_can_dispatch_a_request_async_with_a_nested_generic_request()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(WrapperStreamHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new WrapperStreamRequest<int> { Item = 42 };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(results).IsEquivalentTo(["Handled-42-0", "Handled-42-1"]);
}
[Test]
public async Task I_can_handle_multiple_interface_implementations()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(MultiInterfaceStreamHandler)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new MultiInterfaceStreamRequest();
var results1 = await dispatcher.DispatchAsync<int>(request).ToListAsync();
var results2 = await dispatcher.DispatchAsync<string>(request).ToListAsync();
await Assert.That(results1).IsEquivalentTo([1, 2]);
await Assert.That(results2).IsEquivalentTo(["One", "Two"]);
}
[Test]
public async Task I_can_see_it_fail_if_there_are_ambiguous_handle_methods()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(AmbiguousStreamHandler)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new AmbiguousStreamRequest();
var results = await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(results).IsEquivalentTo(["Interface-Handled"]);
}
[Test]
public async Task I_can_dispatch_a_request_async_with_a_generic_interface_explicit_implementation()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(ExplicitGenericStreamHandler<>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new ExplicitGenericStreamRequest { Value = "Explicit" };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
await Assert.That(results).IsEquivalentTo(["Explicit-ExplicitHandled"]);
}
[Test]
public async Task I_can_see_it_throw_the_original_exception()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(FailingStreamHandler)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new FailingStreamRequest();
var enumerable = dispatcher.DispatchAsync(request);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await enumerable.ToListAsync().AsTask());
using (Assert.Multiple())
{
await Assert.That(ex?.Message).IsEquivalentTo("Handler failed");
await Assert.That(ex?.StackTrace).Contains(nameof(FailingStreamHandler.HandleAsync));
}
}
[Test]
public async Task I_can_see_it_throw_if_dispatcher_options_are_modified_after_build()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(FailingStreamHandler)));
var provider = sc.BuildServiceProvider();
var options = provider.GetRequiredService<Microsoft.Extensions.Options.IOptions<RequestDispatcherOptions>>().Value;
options.GetRequestHandlers<IStreamRequestHandler<FailingStreamRequest, string>>(default!);
await Assert.That(() => options.Inspect([])).Throws<InvalidOperationException>();
}
}

View file

@ -0,0 +1,19 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class AmbiguousScalarHandler : IScalarRequestHandler<AmbiguousScalarRequest, string>
{
// Public method with the same name and signature
public Task<string> HandleAsync(AmbiguousScalarRequest request, CancellationToken ct)
{
return Task.FromResult("Public-Handled");
}
// Explicit interface implementation
Task<string> IScalarRequestHandler<AmbiguousScalarRequest, string>.HandleAsync(AmbiguousScalarRequest request, CancellationToken ct)
{
return Task.FromResult("Interface-Handled");
}
}

View file

@ -0,0 +1,8 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class AmbiguousScalarRequest : IScalarRequest<string>
{
}

View file

@ -0,0 +1,19 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class AmbiguousStreamHandler : IStreamRequestHandler<AmbiguousStreamRequest, string>
{
// Public method with the same name and signature
public async IAsyncEnumerable<string> HandleAsync(AmbiguousStreamRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return "Public-Handled";
}
// Explicit interface implementation
async IAsyncEnumerable<string> IStreamRequestHandler<AmbiguousStreamRequest, string>.HandleAsync(AmbiguousStreamRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return "Interface-Handled";
}
}

View file

@ -0,0 +1,8 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class AmbiguousStreamRequest : IStreamRequest<string>
{
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class AnotherNamedScalarRequest : INamedScalarRequest
{
public string Name { get; set; } = string.Empty;
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class AnotherNamedStreamRequest : INamedStreamRequest
{
public string Name { get; set; } = string.Empty;
}

View file

@ -0,0 +1,12 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
internal sealed class AnotherTestHandler : IScalarRequestHandler<AnotherTestRequest, string>
{
public Task<string> HandleAsync(AnotherTestRequest request, CancellationToken ct)
{
return Task.FromResult("ok");
}
}

View file

@ -0,0 +1,6 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
internal sealed class AnotherTestRequest : IScalarRequest<string> { }

View file

@ -0,0 +1,10 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public abstract class BaseScalarHandler<TRequest> : IScalarRequestHandler<TRequest, string>
where TRequest : IScalarRequest<string>
{
public abstract Task<string> HandleAsync(TRequest request, CancellationToken cancellationToken);
}

View file

@ -0,0 +1,10 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public abstract class BaseStreamHandler<TRequest> : IStreamRequestHandler<TRequest, string>
where TRequest : IStreamRequest<string>
{
public abstract IAsyncEnumerable<string> HandleAsync(TRequest request, CancellationToken cancellationToken);
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class ConstrainedScalarHandler<TRequest> : IScalarRequestHandler<TRequest, string>
where TRequest : ConstrainedScalarRequest
{
public Task<string> HandleAsync(TRequest request, CancellationToken cancellationToken)
{
return Task.FromResult($"{request.Value}-Constrained");
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class ConstrainedScalarRequest : IScalarRequest<string>
{
public int Value { get; set; }
}

View file

@ -0,0 +1,14 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class ConstrainedStreamHandler<TRequest> : IStreamRequestHandler<TRequest, string>
where TRequest : ConstrainedStreamRequest
{
public async IAsyncEnumerable<string> HandleAsync(TRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return $"{request.Value}-Constrained-0";
yield return $"{request.Value}-Constrained-1";
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class ConstrainedStreamRequest : IStreamRequest<string>
{
public int Value { get; set; }
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class DeepDerivedScalarRequest : InheritedScalarRequest
{
public int DeepValue { get; set; }
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class DeepDerivedStreamRequest : InheritedStreamRequest
{
public int DeepValue { get; set; }
}

View file

@ -0,0 +1,12 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class DerivedScalarHandler : BaseScalarHandler<DerivedScalarRequest>
{
public override Task<string> HandleAsync(DerivedScalarRequest request, CancellationToken cancellationToken)
{
return Task.FromResult($"Derived: {request.Value}");
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class DerivedScalarRequest : IScalarRequest<string>
{
public int Value { get; set; }
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class DerivedStreamHandler : BaseStreamHandler<DerivedStreamRequest>
{
public override async IAsyncEnumerable<string> HandleAsync(DerivedStreamRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return $"Derived: {request.Value}-0";
yield return $"Derived: {request.Value}-1";
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class DerivedStreamRequest : IStreamRequest<string>
{
public int Value { get; set; }
}

View file

@ -0,0 +1,12 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class ExplicitGenericScalarHandler<T> : IScalarRequestHandler<ExplicitGenericScalarRequest, string>
{
Task<string> IScalarRequestHandler<ExplicitGenericScalarRequest, string>.HandleAsync(ExplicitGenericScalarRequest request, CancellationToken ct)
{
return Task.FromResult($"{request.Value}-ExplicitHandled");
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class ExplicitGenericScalarRequest : IScalarRequest<string>
{
public string Value { get; set; } = string.Empty;
}

View file

@ -0,0 +1,12 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class ExplicitGenericStreamHandler<T> : IStreamRequestHandler<ExplicitGenericStreamRequest, string>
{
async IAsyncEnumerable<string> IStreamRequestHandler<ExplicitGenericStreamRequest, string>.HandleAsync(ExplicitGenericStreamRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
{
yield return $"{request.Value}-ExplicitHandled";
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class ExplicitGenericStreamRequest : IStreamRequest<string>
{
public string Value { get; set; } = string.Empty;
}

View file

@ -0,0 +1,12 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class FailingScalarHandler : IScalarRequestHandler<FailingScalarRequest, string>
{
public Task<string> HandleAsync(FailingScalarRequest request, CancellationToken cancellationToken)
{
throw new InvalidOperationException("Handler failed");
}
}

View file

@ -0,0 +1,8 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class FailingScalarRequest : IScalarRequest<string>
{
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class FailingStreamHandler : IStreamRequestHandler<FailingStreamRequest, string>
{
public async IAsyncEnumerable<string> HandleAsync(FailingStreamRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return "Wait for it...";
throw new InvalidOperationException("Handler failed");
}
}

View file

@ -0,0 +1,8 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class FailingStreamRequest : IStreamRequest<string>
{
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public interface INamedScalarRequest : IScalarRequest<string>
{
string Name { get; }
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public interface INamedStreamRequest : IStreamRequest<string>
{
string Name { get; }
}

View file

@ -0,0 +1,8 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class InheritedScalarRequest : OpenScalarRequest
{
}

View file

@ -0,0 +1,8 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class InheritedStreamRequest : OpenStreamRequest
{
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class InterfaceConstrainedScalarHandler<TRequest> : IScalarRequestHandler<TRequest, string>
where TRequest : INamedScalarRequest
{
public Task<string> HandleAsync(TRequest request, CancellationToken cancellationToken)
{
return Task.FromResult($"{request.Name}-ConstrainedByInterface");
}
}

View file

@ -0,0 +1,14 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class InterfaceConstrainedStreamHandler<TRequest> : IStreamRequestHandler<TRequest, string>
where TRequest : INamedStreamRequest
{
public async IAsyncEnumerable<string> HandleAsync(TRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return $"{request.Name}-ConstrainedByInterface-0";
yield return $"{request.Name}-ConstrainedByInterface-1";
}
}

View file

@ -0,0 +1,12 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class InterfaceInheritedScalarHandler : IScalarRequestHandler<InterfaceInheritedScalarRequest, string>
{
public Task<string> HandleAsync(InterfaceInheritedScalarRequest request, CancellationToken cancellationToken)
{
return Task.FromResult($"{request.Name}-InterfaceHandled");
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class InterfaceInheritedScalarRequest : INamedScalarRequest
{
public string Name { get; set; } = string.Empty;
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class InterfaceInheritedStreamHandler : IStreamRequestHandler<InterfaceInheritedStreamRequest, string>
{
public async IAsyncEnumerable<string> HandleAsync(InterfaceInheritedStreamRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return $"{request.Name}-InterfaceHandled-0";
yield return $"{request.Name}-InterfaceHandled-1";
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class InterfaceInheritedStreamRequest : INamedStreamRequest
{
public string Name { get; set; } = string.Empty;
}

View file

@ -0,0 +1,17 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class MultiInterfaceScalarHandler : IScalarRequestHandler<MultiInterfaceScalarRequest, int>, IScalarRequestHandler<MultiInterfaceScalarRequest, string>
{
public Task<int> HandleAsync(MultiInterfaceScalarRequest request, CancellationToken cancellationToken)
{
return Task.FromResult(1);
}
Task<string> IScalarRequestHandler<MultiInterfaceScalarRequest, string>.HandleAsync(MultiInterfaceScalarRequest request, CancellationToken ct)
{
return Task.FromResult("One");
}
}

View file

@ -0,0 +1,8 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class MultiInterfaceScalarRequest : IScalarRequest<int>, IScalarRequest<string>
{
}

View file

@ -0,0 +1,19 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class MultiInterfaceStreamHandler : IStreamRequestHandler<MultiInterfaceStreamRequest, int>, IStreamRequestHandler<MultiInterfaceStreamRequest, string>
{
public async IAsyncEnumerable<int> HandleAsync(MultiInterfaceStreamRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return 1;
yield return 2;
}
async IAsyncEnumerable<string> IStreamRequestHandler<MultiInterfaceStreamRequest, string>.HandleAsync(MultiInterfaceStreamRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct)
{
yield return "One";
yield return "Two";
}
}

View file

@ -0,0 +1,8 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class MultiInterfaceStreamRequest : IStreamRequest<int>, IStreamRequest<string>
{
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class OpenScalarHandler<TRequest> : IScalarRequestHandler<TRequest, string>
where TRequest : OpenScalarRequest
{
public Task<string> HandleAsync(TRequest request, CancellationToken cancellationToken)
{
return Task.FromResult($"{request.Data}-Handled");
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class OpenScalarRequest : IScalarRequest<string>
{
public string Data { get; set; } = string.Empty;
}

View file

@ -0,0 +1,14 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class OpenStreamHandler<TRequest> : IStreamRequestHandler<TRequest, string>
where TRequest : OpenStreamRequest
{
public async IAsyncEnumerable<string> HandleAsync(TRequest request, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return $"{request.Data}-Stream-0";
yield return $"{request.Data}-Stream-1";
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class OpenStreamRequest : IStreamRequest<string>
{
public string Data { get; set; } = string.Empty;
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class ScalarChainedBehaviour1(ScalarTestTracker tracker) : IScalarRequestBehavior<ScalarTestRequest, string>
{
public async Task<string> HandleAsync(ScalarTestRequest request, ScalarHandlerDelegate<string> next, CancellationToken cancellationToken)
{
tracker.Log.Add("Behaviour1");
return await next(request, cancellationToken);
}
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class ScalarChainedBehaviour2(ScalarTestTracker tracker) : IScalarRequestBehavior<ScalarTestRequest, string>
{
public async Task<string> HandleAsync(ScalarTestRequest request, ScalarHandlerDelegate<string> next, CancellationToken cancellationToken)
{
tracker.Log.Add("Behaviour2");
return await next(request, cancellationToken);
}
}

View file

@ -0,0 +1,14 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class ScalarOpenBehavior<TRequest, TResponse>(ScalarTestTracker tracker) : IScalarRequestBehavior<TRequest, TResponse>
where TRequest : IScalarRequest<TResponse>
{
public async Task<TResponse> HandleAsync(TRequest request, ScalarHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
tracker.Executed = true;
return await next(request, cancellationToken);
}
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Tests;
public class ScalarOrderingClosedBehavior(ScalarTestTracker tracker) : IScalarRequestBehavior<ScalarTestRequest, string>
{
public Task<string> HandleAsync(ScalarTestRequest request, ScalarHandlerDelegate<string> next, CancellationToken cancellationToken)
{
tracker.Log.Add("OrderingClosed");
return next(request, cancellationToken);
}
}

Some files were not shown because too many files have changed in this diff Show more