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 + ); } }