From 5be6f4293fc8ac62b363f1a82663c6b07b66d1fa Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Fri, 17 Jul 2026 13:23:50 -0300 Subject: [PATCH 1/7] Implementing package --- .editorconfig | 196 +++++++++ .gitignore | 404 ++++++++++++++++++ Directory.Build.props | 15 + Directory.Packages.props | 24 ++ UnionResult.slnx | 21 + src/Directory.Build.props | 25 ++ src/UnionResult/Result.cs | 38 ++ src/UnionResult/UnionResult.csproj | 1 + tests/Directory.Build.props | 25 ++ .../UnionResult.Tests.csproj | 2 + 10 files changed, 751 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 Directory.Build.props create mode 100644 Directory.Packages.props create mode 100644 UnionResult.slnx create mode 100644 src/Directory.Build.props create mode 100644 src/UnionResult/Result.cs create mode 100644 src/UnionResult/UnionResult.csproj create mode 100644 tests/Directory.Build.props create mode 100644 tests/UnionResult.Tests/UnionResult.Tests.csproj diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..01e314d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,196 @@ +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_readonly_field = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent + +[*.cs] +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = file_scoped:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion +csharp_style_expression_bodied_methods = true:warning +csharp_style_expression_bodied_constructors = true:warning +csharp_style_expression_bodied_operators = true:warning +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_lambdas = true:warning +csharp_style_expression_bodied_local_functions = true:warning +csharp_space_around_binary_operators = before_and_after +csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_static_anonymous_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent +csharp_style_var_elsewhere = false:silent +dotnet_diagnostic.SA1600.severity = none +dotnet_diagnostic.SA1633.severity = none +dotnet_diagnostic.SA1200.severity = none +dotnet_diagnostic.IDE0005.severity = warning +dotnet_diagnostic.SA1313.severity = none +dotnet_diagnostic.IDE1006.severity = warning +dotnet_diagnostic.IDE0055.severity = warning +dotnet_diagnostic.RCS1021.severity = warning +dotnet_diagnostic.SA1623.severity = none +dotnet_diagnostic.MA0004.severity = none +dotnet_diagnostic.SA1101.severity = none +dotnet_diagnostic.SA1503.severity = none +dotnet_diagnostic.SA1000.severity = none +dotnet_diagnostic.SA1009.severity = none +dotnet_diagnostic.RCS1194.severity = none +dotnet_diagnostic.SA1106.severity = none +dotnet_diagnostic.SA1602.severity = none +dotnet_diagnostic.SA1601.severity = none +dotnet_diagnostic.IDE0270.severity = warning +dotnet_diagnostic.CA2016.severity = warning +dotnet_diagnostic.MA0011.severity = none +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = none +dotnet_diagnostic.SA1500.severity = none +dotnet_diagnostic.SA1502.severity = none +dotnet_diagnostic.SA1508.severity = none +dotnet_diagnostic.RCS0058.severity = warning +dotnet_diagnostic.SA1309.severity = none +dotnet_diagnostic.RCS0063.severity = warning +dotnet_diagnostic.RCS1251.severity = warning +dotnet_diagnostic.MA0089.severity = warning +dotnet_diagnostic.RCS1163.severity = warning +dotnet_diagnostic.SA1010.severity = none +dotnet_diagnostic.IDE0300.severity = warning +dotnet_diagnostic.MA0026.severity = none +dotnet_diagnostic.S1135.severity = none +dotnet_diagnostic.SA1011.severity = none +dotnet_diagnostic.RCS1097.severity = warning +dotnet_diagnostic.RCS1132.severity = warning +dotnet_diagnostic.RCS1016.severity = warning +dotnet_diagnostic.CA1000.severity = none +dotnet_diagnostic.CA1716.severity = none +dotnet_diagnostic.IDE0130.severity = warning +dotnet_diagnostic.CA1711.severity = none +dotnet_diagnostic.S2326.severity = none +dotnet_diagnostic.RCS1214.severity = warning +dotnet_diagnostic.MA0015.severity = none +dotnet_diagnostic.MA0097.severity = none +dotnet_diagnostic.IDE0008.severity = none +dotnet_diagnostic.SA0001.severity = none +dotnet_diagnostic.IDE0005.severity = none +dotnet_diagnostic.MA0048.severity = none +dotnet_diagnostic.IDE0011.severity = none +dotnet_diagnostic.SA1118.severity = none + +[**/Data/Migrations/*.cs] +generated_code = true +dotnet_analyzer_diagnostic.severity = none \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca328b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,404 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +*.idea + +.qodo +.qwen \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..6475f00 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,15 @@ + + + net11.0 + preview + enable + enable + latest + Recommended + true + true + true + all + moderate + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..93c6920 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,24 @@ + + + true + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UnionResult.slnx b/UnionResult.slnx new file mode 100644 index 0000000..1aac56c --- /dev/null +++ b/UnionResult.slnx @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..bec06e4 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,25 @@ + + + + true + + + + + + + + + + + + + + + <_Parameter1>$(MSBuildProjectName).Tests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + diff --git a/src/UnionResult/Result.cs b/src/UnionResult/Result.cs new file mode 100644 index 0000000..df22e46 --- /dev/null +++ b/src/UnionResult/Result.cs @@ -0,0 +1,38 @@ +namespace UnionResult; + +public readonly union Result(T, Exception) +{ + public bool IsSuccess => Value is not Exception; + + public bool IsFailure => Value is Exception; + + public T AsValue() => Value is T value + ? value + : throw new InvalidOperationException( + "Result does not contain a value."); + + public Exception AsException() => Value is Exception ex + ? ex + : throw new InvalidOperationException( + "Result does not contain an exception."); + + public static Result Success(T value) => new(value); + + public static Result Failure(Exception exception) => new(exception); +} + +public readonly union Result(Exception) +{ + public bool IsSuccess => Value is not Exception; + + public bool IsFailure => Value is Exception; + + public Exception AsException() => Value is Exception ex + ? ex + : throw new InvalidOperationException( + "Result does not contain an exception."); + + public static Result Success() => default; + + public static Result Failure(Exception exception) => new(exception); +} \ No newline at end of file diff --git a/src/UnionResult/UnionResult.csproj b/src/UnionResult/UnionResult.csproj new file mode 100644 index 0000000..2ef1a36 --- /dev/null +++ b/src/UnionResult/UnionResult.csproj @@ -0,0 +1 @@ + diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 0000000..4d515a2 --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,25 @@ + + + + false + true + $(NoWarn);CA1707 + + + + + + + + + + + + + + + + + + + diff --git a/tests/UnionResult.Tests/UnionResult.Tests.csproj b/tests/UnionResult.Tests/UnionResult.Tests.csproj new file mode 100644 index 0000000..a5b9e0e --- /dev/null +++ b/tests/UnionResult.Tests/UnionResult.Tests.csproj @@ -0,0 +1,2 @@ + + \ No newline at end of file From f34cec9981a973ca414c8ed61341a425ba3020cb Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Fri, 17 Jul 2026 13:55:24 -0300 Subject: [PATCH 2/7] Implemented tests --- tests/UnionResult.Tests/ResultOfTTests.cs | 207 ++++++++++++++++++ tests/UnionResult.Tests/ResultTests.cs | 115 ++++++++++ .../ResultUsageScenariosTests.cs | 136 ++++++++++++ .../UnionResult.Tests.csproj | 7 +- 4 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 tests/UnionResult.Tests/ResultOfTTests.cs create mode 100644 tests/UnionResult.Tests/ResultTests.cs create mode 100644 tests/UnionResult.Tests/ResultUsageScenariosTests.cs diff --git a/tests/UnionResult.Tests/ResultOfTTests.cs b/tests/UnionResult.Tests/ResultOfTTests.cs new file mode 100644 index 0000000..eada4e6 --- /dev/null +++ b/tests/UnionResult.Tests/ResultOfTTests.cs @@ -0,0 +1,207 @@ +using System.Diagnostics; + +namespace UnionResult.Tests; + +public class ResultOfTTests +{ + [Fact] + public void Success_ReturnsResultThatIsSuccessAndCarriesTheValue() + { + // Act + var result = Result.Success(42); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.IsFailure.Should().BeFalse(); + result.AsValue().Should().Be(42); + } + + [Fact] + public void Failure_ReturnsResultThatIsFailureAndCarriesTheException() + { + // Arrange + var exception = new InvalidOperationException("boom"); + + // Act + var result = Result.Failure(exception); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.IsFailure.Should().BeTrue(); + result.AsException().Should().BeSameAs(exception); + } + + [Fact] + public void AsValue_OnFailure_ThrowsInvalidOperationException() + { + // Arrange + var result = Result.Failure(new InvalidOperationException("boom")); + + // Act + var act = result.AsValue; + + // Assert + act.Should().Throw() + .WithMessage("Result does not contain a value."); + } + + [Fact] + public void AsException_OnSuccess_ThrowsInvalidOperationException() + { + // Arrange + var result = Result.Success(42); + + // Act + var act = () => result.AsException(); + + // Assert + act.Should().Throw() + .WithMessage("Result does not contain an exception."); + } + + [Fact] + public void Success_WithReferenceTypeValue_RoundTripsTheSameInstance() + { + // Arrange + var payload = new List { "a", "b" }; + + // Act + var result = Result>.Success(payload); + + // Assert + result.AsValue().Should().BeSameAs(payload); + } + + [Fact] + public void Success_WithDefaultStructValue_IsReadableAsValue() + { + // Act + var result = Result.Success(0); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.AsValue().Should().Be(0); + } + + [Fact] + public void Success_WithNullReferenceValue_IsReportedAsSuccessButAsValueThrows() + { + // Documents a current limitation: a null-pattern check ("Value is T value") + // never matches null, so a success created from a null reference cannot be + // read back via AsValue(), even though IsSuccess is true. + + // Act + var result = Result.Success(null); + var act = () => result.AsValue(); + + // Assert + result.IsSuccess.Should().BeTrue(); + act.Should().Throw(); + } + + [Fact] + public void Default_OfValueTypeResult_IsReportedAsSuccessButAsValueThrows() + { + // Documents a current limitation: an uninitialized (default) Result has a + // null backing Value, so it looks like success (no exception present) but + // AsValue() cannot produce a T out of that null. + + // Act + var result = default(Result); + var act = result.AsValue; + + // Assert + result.IsSuccess.Should().BeTrue(); + act.Should().Throw(); + } + + [Fact] + public void Success_WhenTIsAnExceptionType_IsMisclassifiedAsFailure() + { + // Documents a current limitation: the success/failure discriminator is based + // solely on "Value is Exception", so Result cannot safely wrap a T that is + // itself an Exception (or subtype) — it always reads back as a failure. + + // Arrange + var payload = new ArgumentException("payload, not a real failure"); + + // Act + var result = Result.Success(payload); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.IsFailure.Should().BeTrue(); + + // Both accessors still resolve to the same underlying instance. + result.AsValue().Should().BeSameAs(payload); + result.AsException().Should().BeSameAs(payload); + } + + [Fact] + public void Constructor_WithValue_IsEquivalentToSuccess() + { + // Act + var result = new Result(7); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.AsValue().Should().Be(7); + } + + [Fact] + public void Constructor_WithException_IsEquivalentToFailure() + { + // Arrange + var exception = new InvalidOperationException("boom"); + + // Act + var result = new Result(exception); + + // Assert + result.IsFailure.Should().BeTrue(); + result.AsException().Should().BeSameAs(exception); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(int.MaxValue)] + [InlineData(int.MinValue)] + public void Success_RoundTripsVariousIntValues(int value) + { + // Act + var result = Result.Success(value); + + // Assert + result.AsValue().Should().Be(value); + } + + [Fact] + public void Success_WithComplexObjectGraph_PreservesReferenceEquality() + { + // Arrange + var record = new Person("Ada", 36); + + // Act + var result = Result.Success(record); + + // Assert + result.AsValue().Should().Be(record); + result.AsValue().Should().BeSameAs(record); + } + + [Fact] + public void Failure_PreservesTheConcreteExceptionType() + { + // Arrange + var exception = new UnreachableException("Some error message"); + + // Act + var result = Result.Failure(exception); + + // Assert + result.AsException().Should().BeOfType(); + } + + private sealed record Person(string Name, int Age); +} diff --git a/tests/UnionResult.Tests/ResultTests.cs b/tests/UnionResult.Tests/ResultTests.cs new file mode 100644 index 0000000..f508277 --- /dev/null +++ b/tests/UnionResult.Tests/ResultTests.cs @@ -0,0 +1,115 @@ +using System.Diagnostics; + +namespace UnionResult.Tests; + +public class ResultTests +{ + [Fact] + public void Success_ReturnsResultThatIsSuccess() + { + // Act + var result = Result.Success(); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.IsFailure.Should().BeFalse(); + } + + [Fact] + public void Failure_ReturnsResultThatIsFailure() + { + // Arrange + var exception = new InvalidOperationException("boom"); + + // Act + var result = Result.Failure(exception); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.IsFailure.Should().BeTrue(); + } + + [Fact] + public void AsException_OnFailure_ReturnsTheOriginalExceptionInstance() + { + // Arrange + var exception = new InvalidOperationException("boom"); + var result = Result.Failure(exception); + + // Act + var caught = result.AsException(); + + // Assert + caught.Should().BeSameAs(exception); + } + + [Fact] + public void AsException_OnSuccess_ThrowsInvalidOperationException() + { + // Arrange + var result = Result.Success(); + + // Act + var act = () => result.AsException(); + + // Assert + act.Should().Throw() + .WithMessage("Result does not contain an exception."); + } + + [Fact] + public void Default_BehavesAsSuccess() + { + // Act + var result = default(Result); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.IsFailure.Should().BeFalse(); + } + + [Fact] + public void Failure_WithNullException_IsReportedAsSuccessButAsExceptionThrows() + { + // Act + var result = Result.Failure(null!); + var act = () => result.AsException(); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.IsFailure.Should().BeFalse(); + act.Should().Throw(); + } + + [Theory] + [InlineData(typeof(ArgumentException))] + [InlineData(typeof(InvalidOperationException))] + [InlineData(typeof(NotSupportedException))] + public void Failure_PreservesTheConcreteExceptionType(Type exceptionType) + { + // Arrange + var exception = (Exception)Activator.CreateInstance(exceptionType)!; + + // Act + var result = Result.Failure(exception); + + // Assert + result.AsException().Should().BeOfType(exceptionType); + } + + [Fact] + public void Failure_PreservesInnerExceptionAndMessage() + { + // Arrange + var inner = new UnreachableException("inner failure"); + var exception = new InvalidOperationException("outer failure", inner); + + // Act + var result = Result.Failure(exception); + + // Assert + var caught = result.AsException(); + caught.Message.Should().Be("outer failure"); + caught.InnerException.Should().BeSameAs(inner); + } +} diff --git a/tests/UnionResult.Tests/ResultUsageScenariosTests.cs b/tests/UnionResult.Tests/ResultUsageScenariosTests.cs new file mode 100644 index 0000000..3f412ca --- /dev/null +++ b/tests/UnionResult.Tests/ResultUsageScenariosTests.cs @@ -0,0 +1,136 @@ +using NSubstitute.ExceptionExtensions; + +namespace UnionResult.Tests; + +/// +/// Exercises Result/Result<T> the way a consuming application would: a +/// dependency mocked with NSubstitute returns a Result instead of throwing, and +/// business logic branches on IsSuccess/IsFailure. +/// +public class ResultUsageScenariosTests +{ + public interface IUserRepository + { + Result GetById(int id); + + Result Save(User user); + } + + [Fact] + public void GetDisplayName_WhenRepositoryReturnsSuccess_ReturnsUserName() + { + // Arrange + var repository = Substitute.For(); + repository.GetById(1).Returns(Result.Success(new User(1, "Ada Lovelace"))); + var service = new UserService(repository); + + // Act + var name = service.GetDisplayName(1); + + // Assert + name.Should().Be("Ada Lovelace"); + repository.Received(1).GetById(1); + } + + [Fact] + public void GetDisplayName_WhenRepositoryReturnsFailure_ReturnsFallback() + { + // Arrange + var repository = Substitute.For(); + repository.GetById(99).Returns(Result.Failure(new KeyNotFoundException("user 99 not found"))); + var service = new UserService(repository); + + // Act + var name = service.GetDisplayName(99); + + // Assert + name.Should().Be("Unknown user"); + } + + [Fact] + public void TrySave_WhenRepositorySucceeds_ReturnsTrueAndNoError() + { + // Arrange + var repository = Substitute.For(); + repository.Save(Arg.Any()).Returns(Result.Success()); + var service = new UserService(repository); + + // Act + var saved = service.TrySave(new User(2, "Grace Hopper"), out var error); + + // Assert + saved.Should().BeTrue(); + error.Should().BeNull(); + } + + [Fact] + public void TrySave_WhenRepositoryFails_ReturnsFalseAndPropagatesException() + { + // Arrange + var repository = Substitute.For(); + var dbException = new InvalidOperationException("constraint violation"); + repository.Save(Arg.Any()).Returns(Result.Failure(dbException)); + var service = new UserService(repository); + + // Act + var saved = service.TrySave(new User(3, "Grace Hopper"), out var error); + + // Assert + saved.Should().BeFalse(); + error.Should().BeSameAs(dbException); + } + + [Fact] + public void Repository_ThrowingInsteadOfReturningFailure_IsNotSwallowedByTheService() + { + // Result formalizes *expected* failures; it must not mask a dependency that + // throws instead of returning Result.Failure. + + // Arrange + var repository = Substitute.For(); + repository.GetById(Arg.Any()).Throws(new TimeoutException("db timeout")); + var service = new UserService(repository); + + // Act + var act = () => service.GetDisplayName(1); + + // Assert + act.Should().Throw().WithMessage("db timeout"); + } + + [Fact] + public void MultipleConfiguredCalls_EachArgumentYieldsItsOwnConfiguredResult() + { + // Arrange + var repository = Substitute.For(); + repository.GetById(1).Returns(Result.Success(new User(1, "Ada Lovelace"))); + repository.GetById(2).Returns(Result.Failure(new KeyNotFoundException())); + var service = new UserService(repository); + + // Act + var nameForFirstUser = service.GetDisplayName(1); + var nameForSecondUser = service.GetDisplayName(2); + + // Assert + nameForFirstUser.Should().Be("Ada Lovelace"); + nameForSecondUser.Should().Be("Unknown user"); + } + + public sealed record User(int Id, string Name); + + public sealed class UserService(IUserRepository repository) + { + public string GetDisplayName(int id) + { + var result = repository.GetById(id); + return result.IsSuccess ? result.AsValue().Name : "Unknown user"; + } + + public bool TrySave(User user, out Exception? error) + { + var result = repository.Save(user); + error = result.IsFailure ? result.AsException() : null; + return result.IsSuccess; + } + } +} diff --git a/tests/UnionResult.Tests/UnionResult.Tests.csproj b/tests/UnionResult.Tests/UnionResult.Tests.csproj index a5b9e0e..00e678f 100644 --- a/tests/UnionResult.Tests/UnionResult.Tests.csproj +++ b/tests/UnionResult.Tests/UnionResult.Tests.csproj @@ -1,2 +1,7 @@  - \ No newline at end of file + + + + + + From 9a4bdc05c85ed72efd68ee8d834e3f2a9f3c1922 Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Fri, 17 Jul 2026 16:23:22 -0300 Subject: [PATCH 3/7] Implemented benchmarks --- Directory.Packages.props | 45 +++++++------- UnionResult.slnx | 3 + .../ExceptionOnlyResultBenchmarks.cs | 52 ++++++++++++++++ .../UnionResult.Benchmarks/InProcessConfig.cs | 21 +++++++ benchmarks/UnionResult.Benchmarks/Product.cs | 3 + benchmarks/UnionResult.Benchmarks/Program.cs | 9 +++ .../Properties/launchSettings.json | 8 +++ .../ReferenceTypeResultBenchmarks.cs | 56 +++++++++++++++++ .../UnionResult.Benchmarks.csproj | 16 +++++ .../ValueTypeResultBenchmarks.cs | 60 +++++++++++++++++++ 10 files changed, 252 insertions(+), 21 deletions(-) create mode 100644 benchmarks/UnionResult.Benchmarks/ExceptionOnlyResultBenchmarks.cs create mode 100644 benchmarks/UnionResult.Benchmarks/InProcessConfig.cs create mode 100644 benchmarks/UnionResult.Benchmarks/Product.cs create mode 100644 benchmarks/UnionResult.Benchmarks/Program.cs create mode 100644 benchmarks/UnionResult.Benchmarks/Properties/launchSettings.json create mode 100644 benchmarks/UnionResult.Benchmarks/ReferenceTypeResultBenchmarks.cs create mode 100644 benchmarks/UnionResult.Benchmarks/UnionResult.Benchmarks.csproj create mode 100644 benchmarks/UnionResult.Benchmarks/ValueTypeResultBenchmarks.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 93c6920..44d6555 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,24 +1,27 @@ - - true - - + + true + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UnionResult.slnx b/UnionResult.slnx index 1aac56c..557898b 100644 --- a/UnionResult.slnx +++ b/UnionResult.slnx @@ -1,4 +1,7 @@ + + + diff --git a/benchmarks/UnionResult.Benchmarks/ExceptionOnlyResultBenchmarks.cs b/benchmarks/UnionResult.Benchmarks/ExceptionOnlyResultBenchmarks.cs new file mode 100644 index 0000000..701d0e4 --- /dev/null +++ b/benchmarks/UnionResult.Benchmarks/ExceptionOnlyResultBenchmarks.cs @@ -0,0 +1,52 @@ +using BenchmarkDotNet.Attributes; +using UnionResult; + +namespace UnionResult.Benchmarks; + +/// +/// Compares the non-generic Result from each package: no payload, just a +/// success/failure signal plus (on failure) the causing exception. +/// +[Config(typeof(InProcessConfig))] +public class ExceptionOnlyResultBenchmarks +{ + private static readonly InvalidOperationException FailureException = new("boom"); + + private Result _unionSuccess; + private Result _unionFailure; + private OperationResult.Result _operationSuccess; + private OperationResult.Result _operationFailure; + + [GlobalSetup] + public void Setup() + { + _unionSuccess = Result.Success(); + _unionFailure = Result.Failure(FailureException); + _operationSuccess = OperationResult.Result.Success(); + _operationFailure = OperationResult.Result.Error(FailureException); + } + + [Benchmark] + public bool UnionResultCreateSuccess() => Result.Success().IsSuccess; + + [Benchmark] + public bool OperationResultCreateSuccess() => OperationResult.Result.Success().IsSuccess; + + [Benchmark] + public Exception UnionResultCreateFailure() => Result.Failure(FailureException).AsException(); + + [Benchmark] + public Exception OperationResultCreateFailure() => OperationResult.Result.Error(FailureException).Exception!; + + [Benchmark] + public bool UnionResultReadSuccess() => _unionSuccess.IsSuccess; + + [Benchmark] + public bool OperationResultReadSuccess() => _operationSuccess.IsSuccess; + + [Benchmark] + public Exception UnionResultReadFailure() => _unionFailure.AsException(); + + [Benchmark] + public Exception OperationResultReadFailure() => _operationFailure.Exception!; +} diff --git a/benchmarks/UnionResult.Benchmarks/InProcessConfig.cs b/benchmarks/UnionResult.Benchmarks/InProcessConfig.cs new file mode 100644 index 0000000..0eb2f23 --- /dev/null +++ b/benchmarks/UnionResult.Benchmarks/InProcessConfig.cs @@ -0,0 +1,21 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Toolchains.InProcess.Emit; + +namespace UnionResult.Benchmarks; + +/// +/// Runs benchmarks in-process instead of spawning a separate generated project. +/// BenchmarkDotNet 0.15.x does not yet recognize the net11.0 preview runtime moniker, +/// which makes its out-of-process SDK validation throw; running in-process skips +/// that validation entirely while still using MemoryDiagnoser for allocation data. +/// +public sealed class InProcessConfig : ManualConfig +{ + public InProcessConfig() + { + AddJob(Job.Default.WithToolchain(InProcessEmitToolchain.Instance)); + AddDiagnoser(MemoryDiagnoser.Default); + } +} diff --git a/benchmarks/UnionResult.Benchmarks/Product.cs b/benchmarks/UnionResult.Benchmarks/Product.cs new file mode 100644 index 0000000..55271d0 --- /dev/null +++ b/benchmarks/UnionResult.Benchmarks/Product.cs @@ -0,0 +1,3 @@ +namespace UnionResult.Benchmarks; + +public sealed record Product(int Id, string Name, decimal Price); diff --git a/benchmarks/UnionResult.Benchmarks/Program.cs b/benchmarks/UnionResult.Benchmarks/Program.cs new file mode 100644 index 0000000..4cf403e --- /dev/null +++ b/benchmarks/UnionResult.Benchmarks/Program.cs @@ -0,0 +1,9 @@ +using BenchmarkDotNet.Running; +using UnionResult.Benchmarks; + +BenchmarkRunner.Run( +[ + typeof(ValueTypeResultBenchmarks), + typeof(ReferenceTypeResultBenchmarks), + typeof(ExceptionOnlyResultBenchmarks), +]); diff --git a/benchmarks/UnionResult.Benchmarks/Properties/launchSettings.json b/benchmarks/UnionResult.Benchmarks/Properties/launchSettings.json new file mode 100644 index 0000000..9714da5 --- /dev/null +++ b/benchmarks/UnionResult.Benchmarks/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "UnionResult.Benchmarks": { + "commandName": "Project", + "hotReloadEnabled": false + } + } +} \ No newline at end of file diff --git a/benchmarks/UnionResult.Benchmarks/ReferenceTypeResultBenchmarks.cs b/benchmarks/UnionResult.Benchmarks/ReferenceTypeResultBenchmarks.cs new file mode 100644 index 0000000..02bd3b5 --- /dev/null +++ b/benchmarks/UnionResult.Benchmarks/ReferenceTypeResultBenchmarks.cs @@ -0,0 +1,56 @@ +using BenchmarkDotNet.Attributes; +using UnionResult; + +namespace UnionResult.Benchmarks; + +/// +/// Compares the two packages when T is a reference type (Product). Neither package +/// needs to box a reference type, so this isolates the pure wrapping/reading overhead +/// of each Result shape from the boxing cost measured in . +/// Create and Read are benchmarked separately for the same reason as the value-type case: +/// it keeps the two costs (producing vs consuming a Result) from being conflated. +/// +[Config(typeof(InProcessConfig))] +public class ReferenceTypeResultBenchmarks +{ + private static readonly Product SuccessValue = new(1, "Keyboard", 199.90m); + private static readonly InvalidOperationException FailureException = new("boom"); + + private Result _unionSuccess; + private Result _unionFailure; + private OperationResult.Result _operationSuccess; + private OperationResult.Result _operationFailure; + + [GlobalSetup] + public void Setup() + { + _unionSuccess = Result.Success(SuccessValue); + _unionFailure = Result.Failure(FailureException); + _operationSuccess = OperationResult.Result.Success(SuccessValue); + _operationFailure = OperationResult.Result.Error(FailureException); + } + + [Benchmark] + public object UnionResultCreateSuccess() => Result.Success(SuccessValue).Value!; + + [Benchmark] + public Product OperationResultCreateSuccess() => OperationResult.Result.Success(SuccessValue).Value!; + + [Benchmark] + public Exception UnionResultCreateFailure() => Result.Failure(FailureException).AsException(); + + [Benchmark] + public Exception OperationResultCreateFailure() => OperationResult.Result.Error(FailureException).Exception!; + + [Benchmark] + public Product UnionResultReadSuccess() => _unionSuccess.AsValue(); + + [Benchmark] + public Product OperationResultReadSuccess() => _operationSuccess.Value!; + + [Benchmark] + public Exception UnionResultReadFailure() => _unionFailure.AsException(); + + [Benchmark] + public Exception OperationResultReadFailure() => _operationFailure.Exception!; +} diff --git a/benchmarks/UnionResult.Benchmarks/UnionResult.Benchmarks.csproj b/benchmarks/UnionResult.Benchmarks/UnionResult.Benchmarks.csproj new file mode 100644 index 0000000..3f27add --- /dev/null +++ b/benchmarks/UnionResult.Benchmarks/UnionResult.Benchmarks.csproj @@ -0,0 +1,16 @@ + + + + Exe + + + + + + + + + + + + diff --git a/benchmarks/UnionResult.Benchmarks/ValueTypeResultBenchmarks.cs b/benchmarks/UnionResult.Benchmarks/ValueTypeResultBenchmarks.cs new file mode 100644 index 0000000..dfef157 --- /dev/null +++ b/benchmarks/UnionResult.Benchmarks/ValueTypeResultBenchmarks.cs @@ -0,0 +1,60 @@ +using BenchmarkDotNet.Attributes; +using UnionResult; + +namespace UnionResult.Benchmarks; + +/// +/// Compares the two packages when T is a value type (int). UnionResult.Result<T> +/// stores its payload as `object`, so every success value gets boxed; OperationResult.Result<T> +/// stores T directly in a typed field, avoiding the boxing allocation. +/// +/// Create and Read are benchmarked separately: a naive "create + immediately read back" +/// benchmark lets the JIT prove the box never escapes and eliminate it entirely, which would +/// hide the very difference this benchmark exists to show. Returning the raw boxed +/// reference from Create (instead of unboxing it) and reading from an already-built, +/// pre-escaped instance in Read keeps each phase honest. +/// +[Config(typeof(InProcessConfig))] +public class ValueTypeResultBenchmarks +{ + private static readonly int SuccessValue = 42; + private static readonly InvalidOperationException FailureException = new("boom"); + + private Result _unionSuccess; + private Result _unionFailure; + private OperationResult.Result _operationSuccess; + private OperationResult.Result _operationFailure; + + [GlobalSetup] + public void Setup() + { + _unionSuccess = Result.Success(SuccessValue); + _unionFailure = Result.Failure(FailureException); + _operationSuccess = OperationResult.Result.Success(SuccessValue); + _operationFailure = OperationResult.Result.Error(FailureException); + } + + [Benchmark] + public object UnionResultCreateSuccess() => Result.Success(SuccessValue).Value!; + + [Benchmark] + public int OperationResultCreateSuccess() => OperationResult.Result.Success(SuccessValue).Value; + + [Benchmark] + public Exception UnionResultCreateFailure() => Result.Failure(FailureException).AsException(); + + [Benchmark] + public Exception OperationResultCreateFailure() => OperationResult.Result.Error(FailureException).Exception!; + + [Benchmark] + public int UnionResultReadSuccess() => _unionSuccess.AsValue(); + + [Benchmark] + public int OperationResultReadSuccess() => _operationSuccess.Value; + + [Benchmark] + public Exception UnionResultReadFailure() => _unionFailure.AsException(); + + [Benchmark] + public Exception OperationResultReadFailure() => _operationFailure.Exception!; +} From 3630fac3ef19f31363825ee9a2409ffcd89266ff Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Fri, 17 Jul 2026 17:38:28 -0300 Subject: [PATCH 4/7] fix union result boxing/unboxing --- .../ReferenceTypeResultBenchmarks.cs | 5 +- .../ValueTypeResultBenchmarks.cs | 20 +-- src/UnionResult/Result.cs | 118 +++++++++++++++--- tests/UnionResult.Tests/ResultOfTTests.cs | 53 ++++---- tests/UnionResult.Tests/ResultTests.cs | 9 +- 5 files changed, 147 insertions(+), 58 deletions(-) diff --git a/benchmarks/UnionResult.Benchmarks/ReferenceTypeResultBenchmarks.cs b/benchmarks/UnionResult.Benchmarks/ReferenceTypeResultBenchmarks.cs index 02bd3b5..bdb6693 100644 --- a/benchmarks/UnionResult.Benchmarks/ReferenceTypeResultBenchmarks.cs +++ b/benchmarks/UnionResult.Benchmarks/ReferenceTypeResultBenchmarks.cs @@ -8,7 +8,8 @@ namespace UnionResult.Benchmarks; /// needs to box a reference type, so this isolates the pure wrapping/reading overhead /// of each Result shape from the boxing cost measured in . /// Create and Read are benchmarked separately for the same reason as the value-type case: -/// it keeps the two costs (producing vs consuming a Result) from being conflated. +/// it keeps the two costs (producing vs consuming a Result) from being conflated. Both use +/// the non-boxing accessor (AsValue()/.Value), matching normal API usage. /// [Config(typeof(InProcessConfig))] public class ReferenceTypeResultBenchmarks @@ -31,7 +32,7 @@ public void Setup() } [Benchmark] - public object UnionResultCreateSuccess() => Result.Success(SuccessValue).Value!; + public Product UnionResultCreateSuccess() => Result.Success(SuccessValue).AsValue(); [Benchmark] public Product OperationResultCreateSuccess() => OperationResult.Result.Success(SuccessValue).Value!; diff --git a/benchmarks/UnionResult.Benchmarks/ValueTypeResultBenchmarks.cs b/benchmarks/UnionResult.Benchmarks/ValueTypeResultBenchmarks.cs index dfef157..5b0e38d 100644 --- a/benchmarks/UnionResult.Benchmarks/ValueTypeResultBenchmarks.cs +++ b/benchmarks/UnionResult.Benchmarks/ValueTypeResultBenchmarks.cs @@ -4,15 +4,17 @@ namespace UnionResult.Benchmarks; /// -/// Compares the two packages when T is a value type (int). UnionResult.Result<T> -/// stores its payload as `object`, so every success value gets boxed; OperationResult.Result<T> -/// stores T directly in a typed field, avoiding the boxing allocation. +/// Compares the two packages when T is a value type (int). Result<T> stores T and +/// Exception in their own typed fields behind a tag (the union "non-boxing access +/// pattern" via TryGetValue), so AsValue()/AsException() never box; OperationResult.Result<T> +/// likewise stores T directly in a typed field. Both packages still expose a boxing +/// `object? Value` property (required by IUnion for UnionResult; OperationResult has no +/// such requirement but its own `.Value` is already unboxed for value types), but neither +/// AsValue() nor a normal consumer needs to go through it. /// -/// Create and Read are benchmarked separately: a naive "create + immediately read back" -/// benchmark lets the JIT prove the box never escapes and eliminate it entirely, which would -/// hide the very difference this benchmark exists to show. Returning the raw boxed -/// reference from Create (instead of unboxing it) and reading from an already-built, -/// pre-escaped instance in Read keeps each phase honest. +/// Create and Read are benchmarked separately so the cost of producing a Result isn't +/// conflated with the cost of consuming one. Both go through the non-boxing accessor +/// (AsValue()/.Value) so the numbers reflect how the API is actually meant to be used. /// [Config(typeof(InProcessConfig))] public class ValueTypeResultBenchmarks @@ -35,7 +37,7 @@ public void Setup() } [Benchmark] - public object UnionResultCreateSuccess() => Result.Success(SuccessValue).Value!; + public int UnionResultCreateSuccess() => Result.Success(SuccessValue).AsValue(); [Benchmark] public int OperationResultCreateSuccess() => OperationResult.Result.Success(SuccessValue).Value; diff --git a/src/UnionResult/Result.cs b/src/UnionResult/Result.cs index df22e46..896c6eb 100644 --- a/src/UnionResult/Result.cs +++ b/src/UnionResult/Result.cs @@ -1,38 +1,118 @@ -namespace UnionResult; +using System.Runtime.CompilerServices; -public readonly union Result(T, Exception) +namespace UnionResult; + +/// +/// Manually implemented union (rather than the `union Result<T>(T, Exception)` +/// declaration sugar) so value-type T is stored in its own typed field instead of the +/// compiler's single boxing `object Value` field. Discrimination uses an explicit tag +/// instead of `Value is Exception`, and AsValue/AsException go through TryGetValue - the +/// C# union "non-boxing access pattern" - so success/failure checks and reads never box. +/// +/// The type of the success value. +[Union] +public readonly struct Result : IUnion { - public bool IsSuccess => Value is not Exception; + private readonly T _value; + private readonly Exception? _exception; + private readonly byte _tag; + + public Result(T value) + { + _value = value; + _exception = null; + _tag = 1; + } + + public Result(Exception exception) + { + ArgumentNullException.ThrowIfNull(exception); + + _value = default!; + _exception = exception; + _tag = 2; + } - public bool IsFailure => Value is Exception; + public object? Value => _tag switch + { + 1 => _value, + 2 => _exception, + _ => null, + }; - public T AsValue() => Value is T value + public bool HasValue => _tag != 0; + + public bool IsSuccess => _tag == 1; + + public bool IsFailure => _tag == 2; + + public static Result Success(T value) => new(value); + + public static Result Failure(Exception exception) => new(exception); + + public bool TryGetValue(out T value) + { + value = _value!; + return _tag == 1; + } + + public bool TryGetValue(out Exception value) + { + value = _exception!; + return _tag == 2; + } + + public T AsValue() => TryGetValue(out T value) ? value : throw new InvalidOperationException( "Result does not contain a value."); - public Exception AsException() => Value is Exception ex - ? ex + public Exception AsException() => TryGetValue(out Exception value) + ? value : throw new InvalidOperationException( "Result does not contain an exception."); - - public static Result Success(T value) => new(value); - - public static Result Failure(Exception exception) => new(exception); } -public readonly union Result(Exception) +/// +/// Manually implemented union mirroring 's tag-based approach. +/// There is no value-type case here, so boxing was never a concern for this type; the +/// rewrite keeps it consistent with and guards against a null +/// exception being treated as success. +/// +[Union] +public readonly struct Result : IUnion { - public bool IsSuccess => Value is not Exception; + private readonly Exception? _exception; + private readonly byte _tag; - public bool IsFailure => Value is Exception; + public Result(Exception exception) + { + ArgumentNullException.ThrowIfNull(exception); - public Exception AsException() => Value is Exception ex - ? ex - : throw new InvalidOperationException( - "Result does not contain an exception."); + _exception = exception; + _tag = 1; + } + + public object? Value => _tag == 1 ? _exception : null; + + public bool HasValue => _tag != 0; + + public bool IsSuccess => _tag == 0; + + public bool IsFailure => _tag == 1; public static Result Success() => default; public static Result Failure(Exception exception) => new(exception); -} \ No newline at end of file + + public bool TryGetValue(out Exception value) + { + value = _exception!; + return _tag == 1; + } + + public Exception AsException() => TryGetValue(out Exception value) + ? value + : throw new InvalidOperationException( + "Result does not contain an exception."); +} diff --git a/tests/UnionResult.Tests/ResultOfTTests.cs b/tests/UnionResult.Tests/ResultOfTTests.cs index eada4e6..7a6cb53 100644 --- a/tests/UnionResult.Tests/ResultOfTTests.cs +++ b/tests/UnionResult.Tests/ResultOfTTests.cs @@ -31,6 +31,16 @@ public void Failure_ReturnsResultThatIsFailureAndCarriesTheException() result.AsException().Should().BeSameAs(exception); } + [Fact] + public void Failure_WithNullException_ThrowsArgumentNullException() + { + // Act + var act = () => Result.Failure(null!); + + // Assert + act.Should().Throw(); + } + [Fact] public void AsValue_OnFailure_ThrowsInvalidOperationException() { @@ -84,43 +94,42 @@ public void Success_WithDefaultStructValue_IsReadableAsValue() } [Fact] - public void Success_WithNullReferenceValue_IsReportedAsSuccessButAsValueThrows() + public void Success_WithNullReferenceValue_IsReadableAsValue() { - // Documents a current limitation: a null-pattern check ("Value is T value") - // never matches null, so a success created from a null reference cannot be - // read back via AsValue(), even though IsSuccess is true. + // The tag-based discriminator (rather than a "Value is T value" pattern check) + // means a success built from a null reference round-trips correctly. // Act var result = Result.Success(null); - var act = () => result.AsValue(); // Assert result.IsSuccess.Should().BeTrue(); - act.Should().Throw(); + result.AsValue().Should().BeNull(); } [Fact] - public void Default_OfValueTypeResult_IsReportedAsSuccessButAsValueThrows() + public void Default_OfValueTypeResult_IsNeitherSuccessNorFailure() { - // Documents a current limitation: an uninitialized (default) Result has a - // null backing Value, so it looks like success (no exception present) but - // AsValue() cannot produce a T out of that null. + // An uninitialized (default) Result has tag 0, which the tag-based + // discriminator reports as neither success nor failure. // Act var result = default(Result); - var act = result.AsValue; + var asValue = result.AsValue; + var asException = result.AsException; // Assert - result.IsSuccess.Should().BeTrue(); - act.Should().Throw(); + result.IsSuccess.Should().BeFalse(); + result.IsFailure.Should().BeFalse(); + asValue.Should().Throw(); + asException.Should().Throw(); } [Fact] - public void Success_WhenTIsAnExceptionType_IsMisclassifiedAsFailure() + public void Success_WhenTIsAnExceptionType_IsStillClassifiedAsSuccess() { - // Documents a current limitation: the success/failure discriminator is based - // solely on "Value is Exception", so Result cannot safely wrap a T that is - // itself an Exception (or subtype) — it always reads back as a failure. + // The tag-based discriminator (rather than "Value is Exception") means + // Result can safely wrap a T that is itself an Exception (or subtype). // Arrange var payload = new ArgumentException("payload, not a real failure"); @@ -129,12 +138,12 @@ public void Success_WhenTIsAnExceptionType_IsMisclassifiedAsFailure() var result = Result.Success(payload); // Assert - result.IsSuccess.Should().BeFalse(); - result.IsFailure.Should().BeTrue(); - - // Both accessors still resolve to the same underlying instance. + result.IsSuccess.Should().BeTrue(); + result.IsFailure.Should().BeFalse(); result.AsValue().Should().BeSameAs(payload); - result.AsException().Should().BeSameAs(payload); + + var asException = result.AsException; + asException.Should().Throw(); } [Fact] diff --git a/tests/UnionResult.Tests/ResultTests.cs b/tests/UnionResult.Tests/ResultTests.cs index f508277..148a127 100644 --- a/tests/UnionResult.Tests/ResultTests.cs +++ b/tests/UnionResult.Tests/ResultTests.cs @@ -69,16 +69,13 @@ public void Default_BehavesAsSuccess() } [Fact] - public void Failure_WithNullException_IsReportedAsSuccessButAsExceptionThrows() + public void Failure_WithNullException_ThrowsArgumentNullException() { // Act - var result = Result.Failure(null!); - var act = () => result.AsException(); + var act = () => Result.Failure(null!); // Assert - result.IsSuccess.Should().BeTrue(); - result.IsFailure.Should().BeFalse(); - act.Should().Throw(); + act.Should().Throw(); } [Theory] From 50f928833b44bb0ab65d6df502fa95a6c62b0cb8 Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Sun, 19 Jul 2026 10:23:31 -0300 Subject: [PATCH 5/7] Implemented docs and github workflows. --- .github/workflows/main.yml | 34 ++++ .github/workflows/release.yml | 32 +++ README.md | 191 +++++++++++++++++- .../UnionResult.Benchmarks.csproj | 1 + src/UnionResult/Result.cs | 121 +++++++---- src/UnionResult/UnionResult.csproj | 6 +- 6 files changed, 342 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..c4d6ae2 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,34 @@ +name: Main + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + + code-validation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 11.0.x + dotnet-quality: preview + - name: Restore + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build /p:CollectCoverage=true /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="CompilerGeneratedAttribute" /p:CoverletOutputFormat=\"json,cobertura\" /p:CoverletOutput=../../coverage/ /p:MergeWith=../../coverage/coverage.json + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage/coverage.cobertura.xml + fail_ci_if_error: true + verbose: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..24b9edd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: Release + +on: + push: + tags: + - '*.*.*' +jobs: + + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set current Tag + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + - name: Check current Tag + run: | + echo $RELEASE_VERSION + echo ${{ env.RELEASE_VERSION }} + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 11.0.x + dotnet-quality: preview + - name: Generate Package + run: | + dotnet clean -c Release + dotnet build -c Release + dotnet test -c Release --no-build --verbosity normal + dotnet pack src/UnionResult/UnionResult.csproj --output ./nupkgs -c Release --no-build /p:Version=${{ env.RELEASE_VERSION }} + - name: Push to NuGet + run: | + dotnet nuget push ./nupkgs/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json diff --git a/README.md b/README.md index bb2140e..3da6edf 100644 --- a/README.md +++ b/README.md @@ -1 +1,190 @@ -# UnionResult \ No newline at end of file +# UnionResult + +[![NuGet](https://img.shields.io/nuget/v/UnionResult.svg)](https://www.nuget.org/packages/UnionResult/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) +[![.NET 11](https://img.shields.io/badge/.NET-11.0--preview-512BD4)](https://dotnet.microsoft.com/) +[![C# union types](https://img.shields.io/badge/C%23-union%20types-informational)](https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/union) +[![main](https://github.com/Adolfok3/UnionResult/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/Adolfok3/UnionResult/actions/workflows/main.yml) +[![tests](https://img.shields.io/github/actions/workflow/status/Adolfok3/UnionResult/main.yml?branch=main&label=tests)](https://github.com/Adolfok3/UnionResult/actions/workflows/main.yml) +[![tests count](https://img.shields.io/badge/tests-34%20passing-brightgreen)](./tests/UnionResult.Tests) +[![codecov](https://codecov.io/gh/Adolfok3/UnionResult/branch/main/graph/badge.svg)](https://codecov.io/gh/Adolfok3/UnionResult) + +A Result pattern implementation for .NET, built on top of C#'s new native `union` types. + +## Why + +`UnionResult` isn't about functional programming for its own sake - it exists to avoid unnecessary exception overhead in everyday application code. Throwing and catching exceptions is expensive and meant for truly exceptional situations, not for outcomes an operation can expect and a caller should routinely handle (validation failures, "not found", a downstream call that failed). `Result`/`Result` let a method return both possible outcomes explicitly, so the caller decides how to react instead of wrapping every call in `try`/`catch`. + +Several packages already implement this pattern for .NET. What sets `UnionResult` apart is the implementation: instead of a hand-rolled discriminated-union struct, it's built directly on top of C#'s new native `union` type (introduced as a preview language feature in .NET 11) - success/failure is a real, compiler-recognized union with exhaustive pattern-matching support, not a struct that merely looks like one. + +## Requirements + +Union types are a **preview C# language feature**. To use this package your project needs: + +```xml +net11.0 +preview +``` + +and the .NET 11 preview SDK. + +## Installation + +```bash +dotnet add package UnionResult +``` + +Or via the Package Manager Console: + +```powershell +Install-Package UnionResult +``` + +## Usage + +### `Result` - an operation that returns a value + +```csharp +using UnionResult; + +Result Divide(int a, int b) => + b == 0 + ? Result.Failure(new DivideByZeroException()) + : Result.Success(a / b); + +var result = Divide(10, 2); + +var message = result.IsSuccess + ? $"Result: {result.AsValue()}" + : $"Failed: {result.AsException().Message}"; +``` + +### `Result` - an operation with no return value + +```csharp +using UnionResult; + +Result Save(User user) => + repository.TrySave(user, out var error) + ? Result.Success() + : Result.Failure(error); + +var result = Save(user); + +if (result.IsFailure) +{ + logger.LogError(result.AsException(), "Failed to save user"); +} +``` + +### Implicit conversions + +`Success`/`Failure` aren't the only way to build a result - since `Result` and `Result` are real C# unions, a value of a case type converts implicitly, so you can just `return` it directly: + +```csharp +using UnionResult; + +Result Divide(int a, int b) +{ + if (b == 0) + { + return new DivideByZeroException(); // implicitly converts to Result + } + + return a / b; // implicitly converts to Result +} + +Result r = 42; // same conversion, assigned directly +``` + +The same applies to the failure case of the non-generic `Result` (there's no implicit conversion for "success with no value", so that side still calls `Result.Success()` explicitly): + +```csharp +Result Save(User user) +{ + if (!repository.TrySave(user, out var error)) + { + return error; // implicitly converts to Result + } + + return Result.Success(); +} +``` + +### A more realistic example: replacing exceptions at a service boundary + +```csharp +public interface IUserRepository +{ + Result GetById(int id); +} + +public class UserService(IUserRepository repository) +{ + public string GetDisplayName(int id) + { + var result = repository.GetById(id); + return result.IsSuccess ? result.AsValue().Name : "Unknown user"; + } +} +``` + +Here `GetById` communicates "this can fail" through its return type. The caller isn't forced to guess whether the repository throws, and no `try`/`catch` is needed just to keep the app running when a lookup fails. + +### Pattern matching + +Because `Result` is a real C# union, it also supports exhaustive pattern matching: + +```csharp +var message = result switch +{ + int value => $"Result: {value}", + Exception ex => $"Failed: {ex.Message}", +}; +``` + +### API reference + +| Member | Description | +|---|---| +| `Result.Success(T value)` / `Result.Success()` | Creates a successful result. | +| `Result.Failure(Exception)` / `Result.Failure(Exception)` | Creates a failed result. Throws `ArgumentNullException` if `exception` is `null`. | +| `IsSuccess` / `IsFailure` | Whether the result is a success or a failure. | +| `AsValue()` *(`Result` only)* | Returns the success value, or throws `InvalidOperationException` if the result is a failure. | +| `AsException()` | Returns the failure's exception, or throws `InvalidOperationException` if the result is a success. | +| `TryGetValue(out T value)` / `TryGetValue(out Exception value)` | Non-throwing variants of `AsValue()`/`AsException()`. | +| `HasValue` | Whether the result holds any case at all (`false` only for `default(Result)`). | + +## Benchmarks + +`UnionResult` was benchmarked against [`Divino.OperationResult`](https://github.com/victorDivino/operationResult), a similar Result-pattern package, across three scenarios: a value-type payload (`int`), a reference-type payload (`Product`), and no payload at all. Each scenario measures creating a result (`Create`) and reading an already-built one (`Read`), for both the success and failure paths. + +**Environment:** BenchmarkDotNet v0.15.8, Windows 11, AMD Ryzen 7 7800X3D 4.20GHz, .NET SDK 11.0.100-preview.5, in-process toolchain. + +| Category | Operation | UnionResult | OperationResult | Allocated | Winner | +|---|---|---:|---:|:---:|---| +| **Value type (int)** | Create Success | 0.0044 ns | 0.0000 ns | 0 B (both) | tie (noise) | +| | Create Failure | 0.2049 ns | 0.6265 ns | 0 B (both) | UnionResult (~3x) | +| | Read Success | 0.4157 ns | 0.0023 ns | 0 B (both) | OperationResult | +| | Read Failure | 0.6244 ns | 0.2201 ns | 0 B (both) | OperationResult (~3x) | +| **Reference type (Product)** | Create Success | 0.1977 ns | 0.2250 ns | 0 B (both) | tie | +| | Create Failure | 0.2374 ns | 1.1618 ns | 0 B (both) | UnionResult (~5x) | +| | Read Success | 0.6298 ns | 0.2062 ns | 0 B (both) | OperationResult (~3x) | +| | Read Failure | 0.6286 ns | 0.2114 ns | 0 B (both) | OperationResult (~3x) | +| **No payload** | Create Success | 0.0001 ns | 0.0024 ns | 0 B (both) | tie (noise) | +| | Create Failure | 0.2248 ns | 0.6071 ns | 0 B (both) | UnionResult (~2.7x) | +| | Read Success | 0.0327 ns | 0.0005 ns | 0 B (both) | tie (noise) | +| | Read Failure | 0.6227 ns | 0.2338 ns | 0 B (both) | OperationResult (~2.7x) | + +**Takeaways:** + +- Neither package allocates in any scenario measured. +- `UnionResult` matches or beats `OperationResult` on every `Create` scenario - notably ~3-5x faster creating a failure. +- `OperationResult` is consistently faster to *read* an already-built result (~0.2-0.4 ns) - `AsValue()`/`AsException()` validate the result's state before returning, which is the cost of that safety check. +- Values under ~0.03 ns aren't meaningful differences - that's the measurement floor of the benchmark itself. + +The benchmark source lives in [`benchmarks/UnionResult.Benchmarks`](./benchmarks/UnionResult.Benchmarks). + +## License + +MIT - see [LICENSE](./LICENSE). diff --git a/benchmarks/UnionResult.Benchmarks/UnionResult.Benchmarks.csproj b/benchmarks/UnionResult.Benchmarks/UnionResult.Benchmarks.csproj index 3f27add..895694f 100644 --- a/benchmarks/UnionResult.Benchmarks/UnionResult.Benchmarks.csproj +++ b/benchmarks/UnionResult.Benchmarks/UnionResult.Benchmarks.csproj @@ -2,6 +2,7 @@ Exe + false diff --git a/src/UnionResult/Result.cs b/src/UnionResult/Result.cs index 896c6eb..f8a05bd 100644 --- a/src/UnionResult/Result.cs +++ b/src/UnionResult/Result.cs @@ -3,25 +3,41 @@ namespace UnionResult; /// -/// Manually implemented union (rather than the `union Result<T>(T, Exception)` -/// declaration sugar) so value-type T is stored in its own typed field instead of the -/// compiler's single boxing `object Value` field. Discrimination uses an explicit tag -/// instead of `Value is Exception`, and AsValue/AsException go through TryGetValue - the -/// C# union "non-boxing access pattern" - so success/failure checks and reads never box. +/// Represents the outcome of an operation that returns a value of type +/// : either a success carrying that value, or a failure +/// carrying the that caused it. Use it in place of throwing for +/// failures that are an expected, ordinary outcome of the operation - the caller decides +/// how to react instead of having to catch an exception. /// -/// The type of the success value. +/// The type of the value returned on success. +/// +/// +/// Result<int> Divide(int a, int b) => +/// b == 0 +/// ? Result<int>.Failure(new DivideByZeroException()) +/// : Result<int>.Success(a / b); +/// +/// var result = Divide(10, 2); +/// var message = result.IsSuccess +/// ? $"Result: {result.AsValue()}" +/// : $"Failed: {result.AsException().Message}"; +/// +/// +/// +/// Because is a C# union type, it also supports exhaustive +/// pattern matching: result switch { int value => ..., Exception ex => ... }. +/// [Union] public readonly struct Result : IUnion { private readonly T _value; private readonly Exception? _exception; - private readonly byte _tag; + private readonly State _state; public Result(T value) { _value = value; - _exception = null; - _tag = 1; + _state = State.Success; } public Result(Exception exception) @@ -30,21 +46,28 @@ public Result(Exception exception) _value = default!; _exception = exception; - _tag = 2; + _state = State.Failure; } - public object? Value => _tag switch + private enum State : byte { - 1 => _value, - 2 => _exception, + Empty, + Success, + Failure, + } + + public object? Value => _state switch + { + State.Success => _value, + State.Failure => _exception, _ => null, }; - public bool HasValue => _tag != 0; + public bool HasValue => _state != State.Empty; - public bool IsSuccess => _tag == 1; + public bool IsSuccess => _state == State.Success; - public bool IsFailure => _tag == 2; + public bool IsFailure => _state == State.Failure; public static Result Success(T value) => new(value); @@ -53,53 +76,61 @@ public Result(Exception exception) public bool TryGetValue(out T value) { value = _value!; - return _tag == 1; + return _state == State.Success; } public bool TryGetValue(out Exception value) { value = _exception!; - return _tag == 2; + return _state == State.Failure; } - public T AsValue() => TryGetValue(out T value) - ? value - : throw new InvalidOperationException( - "Result does not contain a value."); + public T AsValue() => ResultAccessor.OrThrow( + TryGetValue(out T value), value, "Result does not contain a value."); - public Exception AsException() => TryGetValue(out Exception value) - ? value - : throw new InvalidOperationException( - "Result does not contain an exception."); + public Exception AsException() => ResultAccessor.OrThrow( + TryGetValue(out Exception value), value, "Result does not contain an exception."); } /// -/// Manually implemented union mirroring 's tag-based approach. -/// There is no value-type case here, so boxing was never a concern for this type; the -/// rewrite keeps it consistent with and guards against a null -/// exception being treated as success. +/// Represents the outcome of an operation that has no return value: either a plain +/// success, or a failure carrying the that caused it. Use it in +/// place of throwing for failures that are an expected, ordinary outcome of the +/// operation - the caller decides how to react instead of having to catch an exception. /// +/// +/// +/// Result Save(User user) => +/// repository.TrySave(user, out var error) +/// ? Result.Success() +/// : Result.Failure(error); +/// +/// var result = Save(user); +/// if (result.IsFailure) +/// { +/// logger.LogError(result.AsException(), "Failed to save user"); +/// } +/// +/// [Union] public readonly struct Result : IUnion { private readonly Exception? _exception; - private readonly byte _tag; public Result(Exception exception) { ArgumentNullException.ThrowIfNull(exception); _exception = exception; - _tag = 1; } - public object? Value => _tag == 1 ? _exception : null; + public object? Value => _exception; - public bool HasValue => _tag != 0; + public bool HasValue => _exception is not null; - public bool IsSuccess => _tag == 0; + public bool IsSuccess => _exception is null; - public bool IsFailure => _tag == 1; + public bool IsFailure => _exception is not null; public static Result Success() => default; @@ -108,11 +139,19 @@ public Result(Exception exception) public bool TryGetValue(out Exception value) { value = _exception!; - return _tag == 1; + return _exception is not null; } - public Exception AsException() => TryGetValue(out Exception value) - ? value - : throw new InvalidOperationException( - "Result does not contain an exception."); + public Exception AsException() => ResultAccessor.OrThrow( + TryGetValue(out Exception value), value, "Result does not contain an exception."); +} + +/// +/// Shared "throw if not found" helper for the TryGetValue-based accessors above, so +/// AsValue/AsException don't each repeat their own throw expression. +/// +file static class ResultAccessor +{ + public static TValue OrThrow(bool found, TValue value, string message) => + found ? value : throw new InvalidOperationException(message); } diff --git a/src/UnionResult/UnionResult.csproj b/src/UnionResult/UnionResult.csproj index 2ef1a36..b7afa35 100644 --- a/src/UnionResult/UnionResult.csproj +++ b/src/UnionResult/UnionResult.csproj @@ -1 +1,5 @@ - + + + 1.0.0-preview.1 + + From 3ec2af4b614c463a54f017fdca25894735abeb37 Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Sun, 19 Jul 2026 10:31:16 -0300 Subject: [PATCH 6/7] Added icon --- README.md | 4 ++++ assets/icon.png | Bin 0 -> 41444 bytes src/UnionResult/UnionResult.csproj | 7 +++++++ 3 files changed, 11 insertions(+) create mode 100644 assets/icon.png diff --git a/README.md b/README.md index 3da6edf..418c0e1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +

+ UnionResult +

+ # UnionResult [![NuGet](https://img.shields.io/nuget/v/UnionResult.svg)](https://www.nuget.org/packages/UnionResult/) diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4f044d89c01aa0528bcf779db5bef6bc54e05e09 GIT binary patch literal 41444 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 1.0.0-preview.1 + icon.png + README.md + + + + +
From 888dcc44c3164ce3be98d380af97fb2ad8f2939d Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Sun, 19 Jul 2026 10:37:44 -0300 Subject: [PATCH 7/7] fix identation --- Directory.Packages.props | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 44d6555..33d5336 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,27 +1,27 @@ - - true - - + + true + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + \ No newline at end of file