diff --git a/Directory.Packages.props b/Directory.Packages.props
index 9d036c5..cb84b4c 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,6 +3,7 @@
false
+
diff --git a/README.md b/README.md
index a6d57ed..431ea8b 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,6 @@ public TeamMember? GetTeamMember(Guid memberId)
}
```
-*Note: using railway-oriented programming results in cleaner and shorter code, however, it also brings additional overhead and inability to return early from function, thus making it less performant.*
+*Note: using railway-oriented programming results in cleaner and shorter code, however, it also brings additional overhead and inability to return early from function, thus making it less performant (see [benchmarks](benchmarks/v1.0.2.StatementBenchmark.md)).*
*Learn more about [when to not use railway-oriented programming](https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/).*
diff --git a/RailwayResult.sln b/RailwayResult.sln
index 195da01..fde812a 100644
--- a/RailwayResult.sln
+++ b/RailwayResult.sln
@@ -17,6 +17,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RailwayResult.Tests", "test
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RailwayResult.FunctionalExtensions.Tests", "tests\RailwayResult.FunctionalExtensions.Tests\RailwayResult.FunctionalExtensions.Tests.csproj", "{6A979A1B-CA88-48B4-BF7D-98F0354217F2}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{BA5FCA79-10DF-418F-837D-77B5EC4079EB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RailwayResult.FunctionalExtensions.Benchmarks", "benchmarks\RailwayResult.FunctionalExtensions.Benchmarks\RailwayResult.FunctionalExtensions.Benchmarks.csproj", "{CF09025F-E46F-4B43-9F62-71449DE2D69C}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A8011620-9912-4139-B2A2-72C334F2DB82}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
@@ -50,6 +54,10 @@ Global
{6A979A1B-CA88-48B4-BF7D-98F0354217F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A979A1B-CA88-48B4-BF7D-98F0354217F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A979A1B-CA88-48B4-BF7D-98F0354217F2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CF09025F-E46F-4B43-9F62-71449DE2D69C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CF09025F-E46F-4B43-9F62-71449DE2D69C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CF09025F-E46F-4B43-9F62-71449DE2D69C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CF09025F-E46F-4B43-9F62-71449DE2D69C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -60,6 +68,7 @@ Global
{D13BAF96-7FA1-4FD7-A318-77AA18285D9A} = {D27FC398-A542-46F4-A335-C6D1A7C73F36}
{BF48CBB9-7B8A-4376-9094-D71B093E0660} = {9AF71DD6-A91C-429D-9E17-91DB64571176}
{6A979A1B-CA88-48B4-BF7D-98F0354217F2} = {9AF71DD6-A91C-429D-9E17-91DB64571176}
+ {CF09025F-E46F-4B43-9F62-71449DE2D69C} = {BA5FCA79-10DF-418F-837D-77B5EC4079EB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {54E35DCC-CE9C-4B2C-A050-D7E86E862BDD}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Benchmarks/StatetmantBenchmark.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Benchmarks/StatetmantBenchmark.cs
new file mode 100644
index 0000000..af15cf0
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Benchmarks/StatetmantBenchmark.cs
@@ -0,0 +1,131 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+
+using RailwayResult.FunctionalExtensions.Benchmarks.Data;
+using RailwayResult.FunctionalExtensions.Benchmarks.Seeds;
+
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Benchmarks;
+
+[SimpleJob(RuntimeMoniker.Net80)]
+public class StatementBenchmark
+{
+ public Fraction Fraction { get; set; } = null!;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ Fraction = FractionSeeds.JediOrder;
+ }
+
+ [Benchmark]
+ public int FailEarly_RP()
+ {
+ var result = Fraction.UpdateRank_RP(Guid.Empty, Guid.Empty, Guid.Empty);
+ return result.IsSuccess ? 1 : 0;
+ }
+
+ [Benchmark]
+ public int FailEarly_ROP()
+ {
+ var result = Fraction.UpdateRank_ROP(Guid.Empty, Guid.Empty, Guid.Empty);
+ return result.IsSuccess ? 1 : 0;
+ }
+
+ [Benchmark]
+ public int FailEarly_Exception()
+ {
+ try
+ {
+ Fraction.UpdateRank_Exception(Guid.Empty, Guid.Empty, Guid.Empty);
+ return 1;
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
+ [Benchmark]
+ public int FailLate_RP()
+ {
+ var result = Fraction.UpdateRank_RP(MemberSeeds.JediWindu.Id, MemberSeeds.JediKenobi.Id, RankSeeds.JediGrandMaster.Id);
+ return result.IsSuccess ? 1 : 0;
+ }
+
+ [Benchmark]
+ public int FailLate_ROP()
+ {
+ var result = Fraction.UpdateRank_ROP(MemberSeeds.JediWindu.Id, MemberSeeds.JediKenobi.Id, RankSeeds.JediGrandMaster.Id);
+ return result.IsSuccess ? 1 : 0;
+ }
+
+ [Benchmark]
+ public int FailLate_Exception()
+ {
+ try
+ {
+ Fraction.UpdateRank_Exception(MemberSeeds.JediWindu.Id, MemberSeeds.JediKenobi.Id, RankSeeds.JediGrandMaster.Id);
+ return 1;
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
+ [Benchmark]
+ public async Task FailEarlyAsync_RP()
+ {
+ var result = await Fraction.UpdateRankAsync_RP(Guid.Empty, Guid.Empty, Guid.Empty);
+ return result.IsSuccess ? 1 : 0;
+ }
+
+ [Benchmark]
+ public async Task FailEarlyAsync_ROP()
+ {
+ var result = await Fraction.UpdateRankAsync_ROP(Guid.Empty, Guid.Empty, Guid.Empty);
+ return result.IsSuccess ? 1 : 0;
+ }
+
+ [Benchmark]
+ public async Task FailLateEarly_Exception()
+ {
+ try
+ {
+ await Fraction.UpdateRankAsync_Exception(Guid.Empty, Guid.Empty, Guid.Empty);
+ return 1;
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
+ [Benchmark]
+ public async Task FailLateAsync_RP()
+ {
+ var result = await Fraction.UpdateRankAsync_RP(MemberSeeds.JediWindu.Id, MemberSeeds.JediKenobi.Id, RankSeeds.JediGrandMaster.Id);
+ return result.IsSuccess ? 1 : 0;
+ }
+
+ [Benchmark]
+ public async Task FailLateAsync_ROP()
+ {
+ var result = await Fraction.UpdateRankAsync_ROP(MemberSeeds.JediWindu.Id, MemberSeeds.JediKenobi.Id, RankSeeds.JediGrandMaster.Id);
+ return result.IsSuccess ? 1 : 0;
+ }
+
+ [Benchmark]
+ public async Task FailLateAsync_Exception()
+ {
+ try
+ {
+ await Fraction.UpdateRankAsync_Exception(MemberSeeds.JediWindu.Id, MemberSeeds.JediKenobi.Id, RankSeeds.JediGrandMaster.Id);
+ return 1;
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Being.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Being.cs
new file mode 100644
index 0000000..9f36592
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Being.cs
@@ -0,0 +1,7 @@
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+public sealed record Being
+{
+ public required Guid Id { get; init; }
+ public required string Name { get; set; }
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Fraction.Exceptions.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Fraction.Exceptions.cs
new file mode 100644
index 0000000..f4071ce
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Fraction.Exceptions.cs
@@ -0,0 +1,94 @@
+using RailwayResult.FunctionalExtensions.Benchmarks.Extensions;
+
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+public sealed partial class Fraction
+{
+ public FractionMember GetMemberByBeingId_Exception(Guid beingId)
+ {
+ var member = Members.FirstOrDefault(m => m.BeingId == beingId);
+ if (member is null)
+ throw new Exception(BeingNotMemberOfFraction.Message);
+
+ return member;
+ }
+
+ public FractionMember GetMemberById_Exception(Guid memberId)
+ {
+ var member = Members.FirstOrDefault(m => m.Id == memberId);
+ if (member is null)
+ throw new Exception(MemberNotFound.Message);
+
+ return member;
+ }
+
+ public Rank GetRankById_Exception(Guid rankId)
+ {
+ var rank = Ranks.FirstOrDefault(r => r.Id == rankId);
+ if (rank is null)
+ throw new Exception(RankNotFound.Message);
+
+ return rank;
+ }
+
+ public void UpdateRank_Exception(Guid initiatorBeingId, Guid targetMemberId, Guid newRankId)
+ {
+ var initiator = GetMemberByBeingId_Exception(initiatorBeingId);
+ if (!initiator.Rank.Permissions.HasFlag(Permission.UpdateRank))
+ throw new Exception(CannotUpdateRanks.Message);
+
+ var targetMember = GetMemberById_Exception(targetMemberId);
+ if (targetMember.Rank.IsHeadOfFraction)
+ throw new Exception(CannotUpdateRankOfHeadOfFraction.Message);
+
+ var newRank = GetRankById_Exception(newRankId);
+ if (newRank.IsHeadOfFraction)
+ throw new Exception(CannotHaveMultipleHeads.Message);
+
+ targetMember.Rank = newRank;
+ }
+
+ public async Task GetMemberByBeingIdAsync_Exception(Guid beingId)
+ {
+ var member = await Members.FirstOrDefaultAsync(m => m.BeingId == beingId);
+ if (member is null)
+ throw new Exception(BeingNotMemberOfFraction.Message);
+
+ return member;
+ }
+
+ public async Task GetMemberByIdAsync_Exception(Guid memberId)
+ {
+ var member = await Members.FirstOrDefaultAsync(m => m.Id == memberId);
+ if (member is null)
+ throw new Exception(MemberNotFound.Message);
+
+ return member;
+ }
+
+ public async Task GetRankByIdAsync_Exception(Guid rankId)
+ {
+ var rank = await Ranks.FirstOrDefaultAsync(r => r.Id == rankId);
+ if (rank is null)
+ throw new Exception(RankNotFound.Message);
+
+ return rank;
+ }
+
+ public async Task UpdateRankAsync_Exception(Guid initiatorBeingId, Guid targetMemberId, Guid newRankId)
+ {
+ var initiator = await GetMemberByBeingIdAsync_Exception(initiatorBeingId);
+ if (!initiator.Rank.Permissions.HasFlag(Permission.UpdateRank))
+ throw new Exception(CannotUpdateRanks.Message);
+
+ var targetMember = await GetMemberByIdAsync_Exception(targetMemberId);
+ if (targetMember.Rank.IsHeadOfFraction)
+ throw new Exception(CannotUpdateRankOfHeadOfFraction.Message);
+
+ var newRank = await GetRankByIdAsync_Exception(newRankId);
+ if (newRank.IsHeadOfFraction)
+ throw new Exception(CannotHaveMultipleHeads.Message);
+
+ targetMember.Rank = newRank;
+ }
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Fraction.ROP.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Fraction.ROP.cs
new file mode 100644
index 0000000..1fbf854
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Fraction.ROP.cs
@@ -0,0 +1,72 @@
+using RailwayResult.FunctionalExtensions.Benchmarks.Extensions;
+
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+public sealed partial class Fraction
+{
+ public Result GetMemberByBeingId_ROP(Guid beingId)
+ {
+ return Members
+ .FirstOrDefault(m => m.BeingId == beingId)
+ .EnsureNotNull(BeingNotMemberOfFraction);
+ }
+
+ public Result GetMemberById_ROP(Guid memberId)
+ {
+ return Members
+ .FirstOrDefault(m => m.Id == memberId)
+ .EnsureNotNull(MemberNotFound);
+ }
+
+ public Result GetRankById_ROP(Guid rankId)
+ {
+ return Ranks
+ .FirstOrDefault(r => r.Id == rankId)
+ .EnsureNotNull(RankNotFound);
+ }
+
+ public Result UpdateRank_ROP(Guid initiatorBeingId, Guid targetMemberId, Guid newRankId)
+ {
+ return GetMemberByBeingId_ROP(initiatorBeingId)
+ .Ensure(initiatorMember => initiatorMember.Rank.Permissions.HasFlag(Permission.UpdateRank), CannotUpdateRanks)
+ .Then(_ => GetMemberById_ROP(targetMemberId))
+ .Ensure(target => !target.Rank.IsHeadOfFraction, CannotUpdateRankOfHeadOfFraction)
+ .And(() => GetRankById_ROP(newRankId))
+ .Ensure((_, newRank) => !newRank.IsHeadOfFraction, CannotHaveMultipleHeads)
+ .Tap((targetMember, newRank) => targetMember.Rank = newRank)
+ .ToResult();
+ }
+
+ public Task> GetMemberByBeingIdAsync_ROP(Guid beingId)
+ {
+ return Members
+ .FirstOrDefaultAsync(m => m.BeingId == beingId)
+ .EnsureNotNull(BeingNotMemberOfFraction);
+ }
+
+ public Task> GetMemberByIdAsync_ROP(Guid memberId)
+ {
+ return Members
+ .FirstOrDefaultAsync(m => m.Id == memberId)
+ .EnsureNotNull(MemberNotFound);
+ }
+
+ public Task> GetRankByIdAsync_ROP(Guid rankId)
+ {
+ return Ranks
+ .FirstOrDefaultAsync(r => r.Id == rankId)
+ .EnsureNotNull(RankNotFound);
+ }
+
+ public Task UpdateRankAsync_ROP(Guid initiatorBeingId, Guid targetMemberId, Guid newRankId)
+ {
+ return GetMemberByBeingIdAsync_ROP(initiatorBeingId)
+ .Ensure(initiatorMember => initiatorMember.Rank.Permissions.HasFlag(Permission.UpdateRank), CannotUpdateRanks)
+ .ThenAsync(_ => GetMemberByIdAsync_ROP(targetMemberId))
+ .Ensure(target => !target.Rank.IsHeadOfFraction, CannotUpdateRankOfHeadOfFraction)
+ .AndAsync(_ => GetRankByIdAsync_ROP(newRankId))
+ .Ensure((_, newRank) => !newRank.IsHeadOfFraction, CannotHaveMultipleHeads)
+ .Tap((targetMember, newRank) => targetMember.Rank = newRank)
+ .ToResultAsync();
+ }
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Fraction.RP.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Fraction.RP.cs
new file mode 100644
index 0000000..869ce24
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Fraction.RP.cs
@@ -0,0 +1,114 @@
+using RailwayResult.FunctionalExtensions.Benchmarks.Extensions;
+
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+public sealed partial class Fraction
+{
+ public Result GetMemberByBeingId_RP(Guid beingId)
+ {
+ var member = Members.FirstOrDefault(m => m.BeingId == beingId);
+ if (member is null)
+ return BeingNotMemberOfFraction;
+
+ return member;
+ }
+
+ public Result GetMemberById_RP(Guid memberId)
+ {
+ var member = Members.FirstOrDefault(m => m.Id == memberId);
+ if (member is null)
+ return MemberNotFound;
+
+ return member;
+ }
+
+ public Result GetRankById_RP(Guid rankId)
+ {
+ var rank = Ranks.FirstOrDefault(r => r.Id == rankId);
+ if (rank is null)
+ return RankNotFound;
+
+ return rank;
+ }
+
+ public Result UpdateRank_RP(Guid initiatorBeingId, Guid targetMemberId, Guid newRankId)
+ {
+ var initiatorResult = GetMemberByBeingId_RP(initiatorBeingId);
+ if (initiatorResult.IsFailure)
+ return BeingNotMemberOfFraction;
+
+ if (!initiatorResult.Value.Rank.Permissions.HasFlag(Permission.UpdateRank))
+ return CannotUpdateRanks;
+
+ var targetMemberResult = GetMemberById_RP(targetMemberId);
+ if (targetMemberResult.IsFailure)
+ return MemberNotFound;
+
+ if (targetMemberResult.Value.Rank.IsHeadOfFraction)
+ return CannotUpdateRankOfHeadOfFraction;
+
+ var newRankResult = GetRankById_RP(newRankId);
+ if (newRankResult.IsFailure)
+ return RankNotFound;
+
+ if (newRankResult.Value.IsHeadOfFraction)
+ return CannotHaveMultipleHeads;
+
+ targetMemberResult.Value.Rank = newRankResult.Value;
+ return Result.Success;
+ }
+
+ public async Task> GetMemberByBeingIdAsync_RP(Guid beingId)
+ {
+ var member = await Members.FirstOrDefaultAsync(m => m.BeingId == beingId);
+ if (member is null)
+ return BeingNotMemberOfFraction;
+
+ return member;
+ }
+
+ public async Task> GetMemberByIdAsync_RP(Guid memberId)
+ {
+ var member = await Members.FirstOrDefaultAsync(m => m.Id == memberId);
+ if (member is null)
+ return MemberNotFound;
+
+ return member;
+ }
+
+ public async Task> GetRankByIdAsync_RP(Guid rankId)
+ {
+ var rank = await Ranks.FirstOrDefaultAsync(r => r.Id == rankId);
+ if (rank is null)
+ return RankNotFound;
+
+ return rank;
+ }
+
+ public async Task UpdateRankAsync_RP(Guid initiatorBeingId, Guid targetMemberId, Guid newRankId)
+ {
+ var initiatorResult = await GetMemberByBeingIdAsync_RP(initiatorBeingId);
+ if (initiatorResult.IsFailure)
+ return BeingNotMemberOfFraction;
+
+ if (!initiatorResult.Value.Rank.Permissions.HasFlag(Permission.UpdateRank))
+ return CannotUpdateRanks;
+
+ var targetMemberResult = await GetMemberByIdAsync_RP(targetMemberId);
+ if (targetMemberResult.IsFailure)
+ return MemberNotFound;
+
+ if (targetMemberResult.Value.Rank.IsHeadOfFraction)
+ return CannotUpdateRankOfHeadOfFraction;
+
+ var newRankResult = await GetRankByIdAsync_RP(newRankId);
+ if (newRankResult.IsFailure)
+ return RankNotFound;
+
+ if (newRankResult.Value.IsHeadOfFraction)
+ return CannotHaveMultipleHeads;
+
+ targetMemberResult.Value.Rank = newRankResult.Value;
+ return Result.Success;
+ }
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Fraction.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Fraction.cs
new file mode 100644
index 0000000..cb211ad
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Fraction.cs
@@ -0,0 +1,18 @@
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+public sealed partial class Fraction
+{
+ public static readonly GenericError BeingNotMemberOfFraction = new("Being is not member of fraction.");
+ public static readonly GenericError MemberNotFound = new("Member not found.");
+ public static readonly GenericError RankNotFound = new("Rank not found.");
+ public static readonly GenericError CannotUpdateRanks = new("Cannot update ranks.");
+ public static readonly GenericError CannotUpdateRankOfHeadOfFraction = new("Cannot change rank of head of fraction.");
+ public static readonly GenericError CannotHaveMultipleHeads = new("Fraction cannot have multiple heads.");
+
+ public required Guid Id { get; init; }
+ public required string Name { get; set; }
+
+ public List Members { get; init; } = [];
+ public List Ranks { get; init; } = [];
+ public List Tags { get; init; } = [];
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/FractionMember.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/FractionMember.cs
new file mode 100644
index 0000000..c4543bb
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/FractionMember.cs
@@ -0,0 +1,9 @@
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+public sealed record FractionMember
+{
+ public required Guid Id { get; init; }
+ public required Guid BeingId { get; init; }
+ public required string Name { get; set; }
+ public required Rank Rank { get; set; }
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/GenericError.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/GenericError.cs
new file mode 100644
index 0000000..7cd73cc
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/GenericError.cs
@@ -0,0 +1,3 @@
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+public sealed record GenericError(string Message) : Error("", Message);
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Permission.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Permission.cs
new file mode 100644
index 0000000..9c0c86d
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Permission.cs
@@ -0,0 +1,12 @@
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+[Flags]
+public enum Permission
+{
+ None,
+ InviteCandidate,
+ AcceptCandidate,
+ ExcommunicateMember,
+ UpdateRank,
+ CreateRank
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Rank.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Rank.cs
new file mode 100644
index 0000000..4750a4d
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Rank.cs
@@ -0,0 +1,9 @@
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+public sealed record Rank
+{
+ public required Guid Id { get; init; }
+ public required string Name { get; set; }
+ public required Permission Permissions { get; set; }
+ public bool IsHeadOfFraction { get; init; } = false;
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Tag.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Tag.cs
new file mode 100644
index 0000000..cb9d9e3
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Data/Tag.cs
@@ -0,0 +1,7 @@
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+public sealed record Tag
+{
+ public required Guid Id { get; init; }
+ public required string Name { get; set; }
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Extensions/ListExtensions.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Extensions/ListExtensions.cs
new file mode 100644
index 0000000..6f6f7e4
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Extensions/ListExtensions.cs
@@ -0,0 +1,9 @@
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Extensions;
+
+public static class ListExtensions
+{
+ public static Task FirstOrDefaultAsync(this List list, Func predicate)
+ {
+ return Task.FromResult(list.FirstOrDefault(predicate));
+ }
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Program.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Program.cs
new file mode 100644
index 0000000..ad05108
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Program.cs
@@ -0,0 +1,18 @@
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Exporters;
+using BenchmarkDotNet.Running;
+
+using RailwayResult.FunctionalExtensions.Benchmarks.Benchmarks;
+
+var config = new ManualConfig();
+config.AddColumnProvider([
+ DefaultColumnProviders.Descriptor,
+ DefaultColumnProviders.Statistics,
+ DefaultColumnProviders.Metrics
+]);
+config.AddDiagnoser(new MemoryDiagnoser(new MemoryDiagnoserConfig(false)));
+config.AddExporter(MarkdownExporter.GitHub);
+
+BenchmarkRunner.Run(config);
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/RailwayResult.FunctionalExtensions.Benchmarks.csproj b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/RailwayResult.FunctionalExtensions.Benchmarks.csproj
new file mode 100644
index 0000000..cde653c
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/RailwayResult.FunctionalExtensions.Benchmarks.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+
+
+
+
+
+
+
+
+
+
+
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/BeingSeeds.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/BeingSeeds.cs
new file mode 100644
index 0000000..54d95eb
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/BeingSeeds.cs
@@ -0,0 +1,42 @@
+using RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Seeds;
+
+public static class BeingSeeds
+{
+ public static readonly Being Yoda = new()
+ {
+ Id = new Guid("fd37e883-86fa-4ca5-9c6a-d527fa69d920"),
+ Name = "Yoda"
+ };
+
+ public static readonly Being ObiWanKenobi = new()
+ {
+ Id = new Guid("bda7ed6e-f690-4c4f-94d3-8e4af455d7f9"),
+ Name = "Obi-Wan Kenobi"
+ };
+
+ public static readonly Being Vader = new()
+ {
+ Id = new Guid("1ad5034f-714a-4a8b-97b2-45a6165b029f"),
+ Name = "Vader"
+ };
+
+ public static readonly Being AnakinSkywalker = new()
+ {
+ Id = new Guid("58d11013-8c7e-48cf-ad13-4b9d51d0c319"),
+ Name = "Anakin Skywalker"
+ };
+
+ public static readonly Being KitFisto = new()
+ {
+ Id = new Guid("8a1db189-f8b3-411c-b77e-3f62fa424f84"),
+ Name = "Kit Fisto"
+ };
+
+ public static readonly Being MaceWindu = new()
+ {
+ Id = new Guid("99dbeaee-8a47-4d70-9bdd-219ec66c26e6"),
+ Name = "Mace Windu"
+ };
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/FractionSeeds.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/FractionSeeds.cs
new file mode 100644
index 0000000..042111b
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/FractionSeeds.cs
@@ -0,0 +1,21 @@
+using RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Seeds;
+
+public static class FractionSeeds
+{
+ public static readonly Fraction JediOrder = new()
+ {
+ Id = new("5fef9d92-17b4-4f63-b19d-e81d104422ca"),
+ Name = "Jedi Order",
+ Members = [
+ MemberSeeds.JediSkywalker,
+ MemberSeeds.JediKenobi,
+ MemberSeeds.JediYoda,
+ MemberSeeds.JediFisto,
+ MemberSeeds.JediWindu,
+ ],
+ Ranks = [RankSeeds.JediGrandMaster, RankSeeds.JediMaster, RankSeeds.JediKnight, RankSeeds.JediPadawan],
+ Tags = [TagSeeds.LightSide]
+ };
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/MemberSeeds.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/MemberSeeds.cs
new file mode 100644
index 0000000..3d46a9e
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/MemberSeeds.cs
@@ -0,0 +1,46 @@
+using RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Seeds;
+
+public static class MemberSeeds
+{
+ public static readonly FractionMember JediYoda = new()
+ {
+ Id = new("be8d50e4-fe9d-4833-b9cf-30d01cda5226"),
+ BeingId = BeingSeeds.Yoda.Id,
+ Name = "Master Yoda",
+ Rank = RankSeeds.JediMaster
+ };
+
+ public static readonly FractionMember JediWindu = new()
+ {
+ Id = new("1f52ccfb-92d5-4105-beaa-b3c5aaa1fe58"),
+ BeingId = BeingSeeds.MaceWindu.Id,
+ Name = "Master Windu",
+ Rank = RankSeeds.JediGrandMaster
+ };
+
+ public static readonly FractionMember JediKenobi = new()
+ {
+ Id = new("ce74c501-267d-4338-9274-54a3aea62ad8"),
+ BeingId = BeingSeeds.ObiWanKenobi.Id,
+ Name = "Master Kenobi",
+ Rank = RankSeeds.JediMaster
+ };
+
+ public static readonly FractionMember JediSkywalker = new()
+ {
+ Id = new("484b01c4-b2cb-4ea0-8a5a-2ddbb747c335"),
+ BeingId = BeingSeeds.AnakinSkywalker.Id,
+ Name = "Skywalker",
+ Rank = RankSeeds.JediKnight
+ };
+
+ public static readonly FractionMember JediFisto = new()
+ {
+ Id = new("f21aa018-28ed-4646-9637-d7a1a421995e"),
+ BeingId = BeingSeeds.KitFisto.Id,
+ Name = "Kit Fisto",
+ Rank = RankSeeds.JediKnight
+ };
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/RankSeeds.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/RankSeeds.cs
new file mode 100644
index 0000000..59e8ada
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/RankSeeds.cs
@@ -0,0 +1,35 @@
+using RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Seeds;
+
+public static class RankSeeds
+{
+ public static readonly Rank JediGrandMaster = new()
+ {
+ Id = new("85225774-d24f-4bb7-90e8-a696665b63fd"),
+ Name = "Grand Master",
+ Permissions = Permission.InviteCandidate | Permission.AcceptCandidate | Permission.ExcommunicateMember | Permission.UpdateRank | Permission.CreateRank,
+ IsHeadOfFraction = true,
+ };
+
+ public static readonly Rank JediMaster = new()
+ {
+ Id = new("d612787c-38cd-4fc3-8083-5e357e951b0a"),
+ Name = "Master",
+ Permissions = Permission.InviteCandidate | Permission.AcceptCandidate
+ };
+
+ public static readonly Rank JediKnight = new()
+ {
+ Id = new("17d7660d-7154-408f-b2c8-55fd2d4d6e10"),
+ Name = "Knight",
+ Permissions = Permission.InviteCandidate
+ };
+
+ public static readonly Rank JediPadawan = new()
+ {
+ Id = new("5cfb2ea6-ef46-4495-9e42-afb03df42ec9"),
+ Name = "Padawan",
+ Permissions = Permission.None
+ };
+}
diff --git a/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/TagSeeds.cs b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/TagSeeds.cs
new file mode 100644
index 0000000..8c3ebb3
--- /dev/null
+++ b/benchmarks/RailwayResult.FunctionalExtensions.Benchmarks/Seeds/TagSeeds.cs
@@ -0,0 +1,18 @@
+using RailwayResult.FunctionalExtensions.Benchmarks.Data;
+
+namespace RailwayResult.FunctionalExtensions.Benchmarks.Seeds;
+
+public static class TagSeeds
+{
+ public static readonly Tag LightSide = new()
+ {
+ Id = new("fc5bf11b-ca89-4bd6-b936-55a0aa5bbda4"),
+ Name = "Light Side of the Force"
+ };
+
+ public static readonly Tag DarkSide = new()
+ {
+ Id = new("2215a8b8-36cc-4ed5-8176-a60cc7b8fa12"),
+ Name = "Dark Side of the Force"
+ };
+}
diff --git a/benchmarks/v1.0.2.StatementBenchmark.md b/benchmarks/v1.0.2.StatementBenchmark.md
new file mode 100644
index 0000000..77a6a4e
--- /dev/null
+++ b/benchmarks/v1.0.2.StatementBenchmark.md
@@ -0,0 +1,24 @@
+```
+
+BenchmarkDotNet v0.13.12, Windows 11 (10.0.22000.2538/21H2/SunValley)
+Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores
+.NET SDK 8.0.202
+ [Host] : .NET 8.0.3 (8.0.324.11423), X64 RyuJIT AVX2
+ .NET 8.0 : .NET 8.0.3 (8.0.324.11423), X64 RyuJIT AVX2
+
+
+```
+| Method | Mean | Error | StdDev | Median | Allocated |
+|------------------------ |-------------:|-------------:|-------------:|-------------:|----------:|
+| FailEarly_RP | 76.81 ns | 3.164 ns | 9.330 ns | 72.74 ns | 208 B |
+| FailEarly_ROP | 101.76 ns | 1.695 ns | 1.586 ns | 102.22 ns | 480 B |
+| FailEarly_Exception | 6,244.13 ns | 270.976 ns | 798.979 ns | 6,227.70 ns | 360 B |
+| FailLate_RP | 64.81 ns | 1.333 ns | 1.911 ns | 64.44 ns | 208 B |
+| FailLate_ROP | 105.27 ns | 0.901 ns | 0.752 ns | 105.15 ns | 480 B |
+| FailLate_Exception | 5,339.28 ns | 73.166 ns | 57.123 ns | 5,330.28 ns | 360 B |
+| FailEarlyAsync_RP | 120.50 ns | 4.871 ns | 14.363 ns | 111.27 ns | 352 B |
+| FailEarlyAsync_ROP | 367.89 ns | 3.656 ns | 3.420 ns | 367.12 ns | 1200 B |
+| FailLateEarly_Exception | 25,779.27 ns | 1,075.847 ns | 3,172.159 ns | 23,792.57 ns | 2384 B |
+| FailLateAsync_RP | 109.54 ns | 2.184 ns | 2.043 ns | 108.65 ns | 352 B |
+| FailLateAsync_ROP | 417.21 ns | 17.448 ns | 51.445 ns | 383.51 ns | 1200 B |
+| FailLateAsync_Exception | 26,398.34 ns | 1,236.094 ns | 3,644.651 ns | 23,839.58 ns | 2384 B |
diff --git a/src/RailwayResult.FunctionalExtensions/ResultExtensions.And.cs b/src/RailwayResult.FunctionalExtensions/ResultExtensions.And.cs
index 2f549d7..ade858b 100644
--- a/src/RailwayResult.FunctionalExtensions/ResultExtensions.And.cs
+++ b/src/RailwayResult.FunctionalExtensions/ResultExtensions.And.cs
@@ -83,4 +83,22 @@ public static partial class ResultExtensions
var result = await resultTask;
return await result.AndAsync(asyncFunc);
}
+
+ public static async Task> AndAsync(this Result result, Func>> asyncFunc)
+ {
+ if (result.IsFailure)
+ return result.Error;
+
+ var nestedResult = await asyncFunc(result.Value);
+ if (nestedResult.IsFailure)
+ return nestedResult.Error;
+
+ return (result.Value, nestedResult.Value);
+ }
+
+ public static async Task> AndAsync(this Task> resultTask, Func>> asyncFunc)
+ {
+ var result = await resultTask;
+ return await result.AndAsync(asyncFunc);
+ }
}
diff --git a/tests/RailwayResult.FunctionalExtensions.Tests/AndTests/TheoryData_R1_AndAsync.cs b/tests/RailwayResult.FunctionalExtensions.Tests/AndTests/TheoryData_R1_AndAsync.cs
index df0e075..708c583 100644
--- a/tests/RailwayResult.FunctionalExtensions.Tests/AndTests/TheoryData_R1_AndAsync.cs
+++ b/tests/RailwayResult.FunctionalExtensions.Tests/AndTests/TheoryData_R1_AndAsync.cs
@@ -19,6 +19,33 @@ public TheoryData_R1_AndAsync()
Errors.ErrorA
);
+ Add(
+ result => result.AndAsync(_ => Task.FromResult(O.B)),
+ O.A,
+ (O.A, O.B)
+ );
+
+ //and should return input failure result
+ Add(
+ result => result.AndAsync(_ => Task.FromResult(O.B)),
+ Errors.ErrorA,
+ Errors.ErrorA
+ );
+
+ //and should return nested result error
+ Add(
+ result => result.AndAsync(_ => Task.FromResult(Errors.ErrorD)),
+ O.A,
+ Errors.ErrorD
+ );
+
+ //and should return input failure result
+ Add(
+ result => result.AndAsync(_ => Task.FromResult(Errors.ErrorD)),
+ Errors.ErrorA,
+ Errors.ErrorA
+ );
+
// --- TaskOfR1 ---
Add(
@@ -33,5 +60,32 @@ public TheoryData_R1_AndAsync()
Errors.ErrorA,
Errors.ErrorA
);
+
+ Add(
+ result => result.ToResultTask().AndAsync(_ => Task.FromResult(O.B)),
+ O.A,
+ (O.A, O.B)
+ );
+
+ //and should return input failure result
+ Add(
+ result => result.ToResultTask().AndAsync(_ => Task.FromResult(O.B)),
+ Errors.ErrorA,
+ Errors.ErrorA
+ );
+
+ //and should return nested result error
+ Add(
+ result => result.ToResultTask().AndAsync(_ => Task.FromResult(Errors.ErrorD)),
+ O.A,
+ Errors.ErrorD
+ );
+
+ //and should return input failure result
+ Add(
+ result => result.ToResultTask().AndAsync(_ => Task.FromResult(Errors.ErrorD)),
+ Errors.ErrorA,
+ Errors.ErrorA
+ );
}
}