From 4f75a73be24fd1df51c01a1e966d4a97fcf0ed62 Mon Sep 17 00:00:00 2001 From: Erdem Date: Wed, 27 May 2026 17:27:39 +0300 Subject: [PATCH 1/5] feat(collections): add batch APIs for Result, Maybe, and Any --- .../AnyCollectionExtensions.cs | 137 ++++++++++++++++++ CSharpEssentials.Any/AnyExtensions.cs | 2 +- .../Extensions/MaybeCollectionExtensions.cs | 38 +++++ .../Extensions/ResultCollectionExtensions.cs | 97 +++++++++++++ .../Any/AnyCollectionExtensionsTests.cs | 57 ++++++++ .../Maybe/MaybeCollectionExtensionsTests.cs | 50 +++++++ .../Results/ResultCollectionBatchTests.cs | 106 ++++++++++++++ README.MD | 2 +- 8 files changed, 487 insertions(+), 2 deletions(-) create mode 100644 CSharpEssentials.Any/AnyCollectionExtensions.cs create mode 100644 CSharpEssentials.Maybe/Extensions/MaybeCollectionExtensions.cs create mode 100644 CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs create mode 100644 CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs create mode 100644 CSharpEssentials.Tests/Maybe/MaybeCollectionExtensionsTests.cs create mode 100644 CSharpEssentials.Tests/Results/ResultCollectionBatchTests.cs diff --git a/CSharpEssentials.Any/AnyCollectionExtensions.cs b/CSharpEssentials.Any/AnyCollectionExtensions.cs new file mode 100644 index 0000000..a51ab98 --- /dev/null +++ b/CSharpEssentials.Any/AnyCollectionExtensions.cs @@ -0,0 +1,137 @@ +namespace CSharpEssentials.Any; + +public static partial class AnyExtensions +{ + public static (T0[] First, T1[] Second) Sequence(this IEnumerable> source) => source.Partition(); + + public static (T0[] First, T1[] Second) Traverse(this IEnumerable source, Func> selector) => + source.Select(selector).Partition(); + + public static (T0[] First, T1[] Second) Partition(this IEnumerable> source) + { + List first = []; + List second = []; + + foreach (Any item in source) + item.Switch(first: first.Add, second: second.Add); + + return (first.ToArray(), second.ToArray()); + } + + public static (T0[] First, T1[] Second, T2[] Third) Sequence(this IEnumerable> source) => source.Partition(); + + public static (T0[] First, T1[] Second, T2[] Third) Traverse(this IEnumerable source, Func> selector) => + source.Select(selector).Partition(); + + public static (T0[] First, T1[] Second, T2[] Third) Partition(this IEnumerable> source) + { + List first = []; + List second = []; + List third = []; + + foreach (Any item in source) + item.Switch(first: first.Add, second: second.Add, third: third.Add); + + return (first.ToArray(), second.ToArray(), third.ToArray()); + } + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth) Sequence(this IEnumerable> source) => source.Partition(); + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth) Traverse(this IEnumerable source, Func> selector) => + source.Select(selector).Partition(); + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth) Partition(this IEnumerable> source) + { + List first = []; + List second = []; + List third = []; + List fourth = []; + + foreach (Any item in source) + item.Switch(first: first.Add, second: second.Add, third: third.Add, fourth: fourth.Add); + + return (first.ToArray(), second.ToArray(), third.ToArray(), fourth.ToArray()); + } + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth) Sequence(this IEnumerable> source) => source.Partition(); + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth) Traverse(this IEnumerable source, Func> selector) => + source.Select(selector).Partition(); + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth) Partition(this IEnumerable> source) + { + List first = []; + List second = []; + List third = []; + List fourth = []; + List fifth = []; + + foreach (Any item in source) + item.Switch(first: first.Add, second: second.Add, third: third.Add, fourth: fourth.Add, fifth: fifth.Add); + + return (first.ToArray(), second.ToArray(), third.ToArray(), fourth.ToArray(), fifth.ToArray()); + } + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth) Sequence(this IEnumerable> source) => source.Partition(); + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth) Traverse(this IEnumerable source, Func> selector) => + source.Select(selector).Partition(); + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth) Partition(this IEnumerable> source) + { + List first = []; + List second = []; + List third = []; + List fourth = []; + List fifth = []; + List sixth = []; + + foreach (Any item in source) + item.Switch(first: first.Add, second: second.Add, third: third.Add, fourth: fourth.Add, fifth: fifth.Add, sixth: sixth.Add); + + return (first.ToArray(), second.ToArray(), third.ToArray(), fourth.ToArray(), fifth.ToArray(), sixth.ToArray()); + } + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh) Sequence(this IEnumerable> source) => source.Partition(); + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh) Traverse(this IEnumerable source, Func> selector) => + source.Select(selector).Partition(); + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh) Partition(this IEnumerable> source) + { + List first = []; + List second = []; + List third = []; + List fourth = []; + List fifth = []; + List sixth = []; + List seventh = []; + + foreach (Any item in source) + item.Switch(first: first.Add, second: second.Add, third: third.Add, fourth: fourth.Add, fifth: fifth.Add, sixth: sixth.Add, seventh: seventh.Add); + + return (first.ToArray(), second.ToArray(), third.ToArray(), fourth.ToArray(), fifth.ToArray(), sixth.ToArray(), seventh.ToArray()); + } + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh, T7[] Eighth) Sequence(this IEnumerable> source) => source.Partition(); + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh, T7[] Eighth) Traverse(this IEnumerable source, Func> selector) => + source.Select(selector).Partition(); + + public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh, T7[] Eighth) Partition(this IEnumerable> source) + { + List first = []; + List second = []; + List third = []; + List fourth = []; + List fifth = []; + List sixth = []; + List seventh = []; + List eighth = []; + + foreach (Any item in source) + item.Switch(first: first.Add, second: second.Add, third: third.Add, fourth: fourth.Add, fifth: fifth.Add, sixth: sixth.Add, seventh: seventh.Add, eighth: eighth.Add); + + return (first.ToArray(), second.ToArray(), third.ToArray(), fourth.ToArray(), fifth.ToArray(), sixth.ToArray(), seventh.ToArray(), eighth.ToArray()); + } +} diff --git a/CSharpEssentials.Any/AnyExtensions.cs b/CSharpEssentials.Any/AnyExtensions.cs index aab67be..9fc1db7 100644 --- a/CSharpEssentials.Any/AnyExtensions.cs +++ b/CSharpEssentials.Any/AnyExtensions.cs @@ -1,6 +1,6 @@ namespace CSharpEssentials.Any; -public static class AnyExtensions +public static partial class AnyExtensions { #region Any diff --git a/CSharpEssentials.Maybe/Extensions/MaybeCollectionExtensions.cs b/CSharpEssentials.Maybe/Extensions/MaybeCollectionExtensions.cs new file mode 100644 index 0000000..3f624c2 --- /dev/null +++ b/CSharpEssentials.Maybe/Extensions/MaybeCollectionExtensions.cs @@ -0,0 +1,38 @@ +namespace CSharpEssentials.Maybe; + +public static partial class MaybeExtensions +{ + public static Maybe Sequence(this IEnumerable> source) + { + List values = []; + + foreach (Maybe maybe in source) + { + if (maybe.HasNoValue) + return Maybe.None; + + values.Add(maybe.Value); + } + + return values.ToArray(); + } + + public static Maybe Traverse(this IEnumerable source, Func> selector) => + source.Select(selector).Sequence(); + + public static (TValue[] Values, int NoneCount) Partition(this IEnumerable> source) + { + List values = []; + int noneCount = 0; + + foreach (Maybe maybe in source) + { + if (maybe.HasValue) + values.Add(maybe.Value); + else + noneCount++; + } + + return (values.ToArray(), noneCount); + } +} diff --git a/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs b/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs new file mode 100644 index 0000000..7ec05a2 --- /dev/null +++ b/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs @@ -0,0 +1,97 @@ +using CSharpEssentials.Errors; +using CSharpEssentials.ResultPattern.Interfaces; + +namespace CSharpEssentials.ResultPattern; + +public static partial class ResultExtensions +{ + public static Result CombineAll(this IEnumerable source) => source.Cast().CombineAll(); + + public static Result CombineAll(this IEnumerable source) + { + List errors = []; + + foreach (IResultBase result in source) + { + if (result.IsFailure) + errors.AddRange(result.ErrorsOrEmptyArray); + } + + return errors.Count == 0 ? Result.Success() : errors; + } + + public static Result CombineAll(this IEnumerable> source) => source.Cast>().CombineAll(); + + public static Result CombineAll(this IEnumerable> source) => source.Sequence(); + + public static Result Sequence(this IEnumerable> source) => source.Cast>().Sequence(); + + public static Result Sequence(this IEnumerable> source) + { + List successes = []; + List errors = []; + + foreach (IResult result in source) + { + if (result.IsSuccess) + successes.Add(result.Value); + else + errors.AddRange(result.ErrorsOrEmptyArray); + } + + return errors.Count == 0 ? successes.ToArray() : errors; + } + + public static Result Traverse(this IEnumerable source, Func> selector) => + source.Select(selector).Sequence(); + + public static (TValue[] Successes, Error[] Errors) Partition(this IEnumerable> source) => + source.Cast>().Partition(); + + public static (TValue[] Successes, Error[] Errors) Partition(this IEnumerable> source) + { + List successes = []; + List errors = []; + + foreach (IResult result in source) + { + if (result.IsSuccess) + successes.Add(result.Value); + else + errors.AddRange(result.ErrorsOrEmptyArray); + } + + return (successes.ToArray(), errors.ToArray()); + } + + public static Result FirstFailureOrSuccesses(this IEnumerable source) => source.Cast().FirstFailureOrSuccesses(); + + public static Result FirstFailureOrSuccesses(this IEnumerable source) + { + foreach (IResultBase result in source) + { + if (result.IsFailure) + return result.ErrorsOrEmptyArray; + } + + return Result.Success(); + } + + public static Result FirstFailureOrSuccesses(this IEnumerable> source) => + source.Cast>().FirstFailureOrSuccesses(); + + public static Result FirstFailureOrSuccesses(this IEnumerable> source) + { + List successes = []; + + foreach (IResult result in source) + { + if (result.IsFailure) + return result.ErrorsOrEmptyArray; + + successes.Add(result.Value); + } + + return successes.ToArray(); + } +} diff --git a/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs b/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs new file mode 100644 index 0000000..8221521 --- /dev/null +++ b/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs @@ -0,0 +1,57 @@ +using CSharpEssentials.Any; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Any; + +public class AnyCollectionExtensionsTests +{ + [Fact] + public void Partition_T2_ShouldSplitBranches() + { + Any[] source = [1, "two", 3, "four"]; + + (int[] first, string[] second) = source.Partition(); + + first.Should().Equal(1, 3); + second.Should().Equal("two", "four"); + } + + [Fact] + public void Sequence_T3_ShouldSplitAllBranches() + { + Any[] source = [1, "two", true, 3, false]; + + (int[] first, string[] second, bool[] third) = source.Sequence(); + + first.Should().Equal(1, 3); + second.Should().Equal("two"); + third.Should().Equal(true, false); + } + + [Fact] + public void Traverse_T8_ShouldProjectAndPartition() + { + int[] source = [0, 1, 2, 3, 4, 5, 6, 7]; + + var result = source.Traverse(value => value switch + { + 0 => value, + 1 => $"item-{value}", + 2 => true, + 3 => 3.5d, + 4 => 4.5m, + 5 => 5L, + 6 => Guid.Parse("11111111-1111-1111-1111-111111111111"), + _ => new DateTime(2026, 5, 27, 0, 0, 0, DateTimeKind.Utc) + }); + + result.First.Should().Equal(0); + result.Second.Should().Equal("item-1"); + result.Third.Should().Equal(true); + result.Fourth.Should().Equal(3.5d); + result.Fifth.Should().Equal(4.5m); + result.Sixth.Should().Equal(5L); + result.Seventh.Should().Equal(Guid.Parse("11111111-1111-1111-1111-111111111111")); + result.Eighth.Should().Equal(new DateTime(2026, 5, 27, 0, 0, 0, DateTimeKind.Utc)); + } +} diff --git a/CSharpEssentials.Tests/Maybe/MaybeCollectionExtensionsTests.cs b/CSharpEssentials.Tests/Maybe/MaybeCollectionExtensionsTests.cs new file mode 100644 index 0000000..f99d84b --- /dev/null +++ b/CSharpEssentials.Tests/Maybe/MaybeCollectionExtensionsTests.cs @@ -0,0 +1,50 @@ +using CSharpEssentials.Maybe; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Maybe; + +public class MaybeCollectionExtensionsTests +{ + [Fact] + public void Sequence_AllValues_ShouldReturnMaybeArray() + { + Maybe[] source = [1, 2, 3]; + + Maybe result = source.Sequence(); + + result.HasValue.Should().BeTrue(); + result.Value.Should().Equal(1, 2, 3); + } + + [Fact] + public void Sequence_WithNone_ShouldReturnNone() + { + Maybe[] source = [1, Maybe.None, 3]; + + Maybe result = source.Sequence(); + + result.HasNoValue.Should().BeTrue(); + } + + [Fact] + public void Traverse_ShouldProjectAndSequence() + { + int[] source = [1, 2, 3]; + + Maybe result = source.Traverse(value => Maybe.From($"item-{value}")); + + result.HasValue.Should().BeTrue(); + result.Value.Should().Equal("item-1", "item-2", "item-3"); + } + + [Fact] + public void Partition_ShouldReturnValuesAndNoneCount() + { + Maybe[] source = [1, Maybe.None, 3, Maybe.None]; + + (int[] values, int noneCount) = source.Partition(); + + values.Should().Equal(1, 3); + noneCount.Should().Be(2); + } +} diff --git a/CSharpEssentials.Tests/Results/ResultCollectionBatchTests.cs b/CSharpEssentials.Tests/Results/ResultCollectionBatchTests.cs new file mode 100644 index 0000000..1680b4a --- /dev/null +++ b/CSharpEssentials.Tests/Results/ResultCollectionBatchTests.cs @@ -0,0 +1,106 @@ +using CSharpEssentials.Errors; +using CSharpEssentials.ResultPattern; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Results; + +public class ResultCollectionBatchTests +{ + [Fact] + public void Sequence_AllSuccesses_ShouldReturnValues() + { + Result[] source = [1, 2, 3]; + + Result result = source.Sequence(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Equal(1, 2, 3); + } + + [Fact] + public void Sequence_WithFailures_ShouldAggregateErrors() + { + Result[] source = + [ + 1, + Error.Validation("First.Error", "First"), + Error.Validation("Second.Error", "Second") + ]; + + Result result = source.Sequence(); + + result.IsFailure.Should().BeTrue(); + result.Errors.Select(x => x.Code).Should().Equal("First.Error", "Second.Error"); + } + + [Fact] + public void Traverse_ShouldProjectAndSequence() + { + int[] source = [1, 2, 3]; + + Result result = source.Traverse(value => $"item-{value}".ToResult()); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Equal("item-1", "item-2", "item-3"); + } + + [Fact] + public void Partition_ShouldReturnSuccessesAndFlattenedErrors() + { + Result[] source = + [ + 10, + Error.Validation("First.Error", "First"), + 20, + Error.Validation("Second.Error", "Second") + ]; + + (int[] successes, Error[] errors) = source.Partition(); + + successes.Should().Equal(10, 20); + errors.Select(x => x.Code).Should().Equal("First.Error", "Second.Error"); + } + + [Fact] + public void CombineAll_NonGeneric_ShouldAggregateErrors() + { + Result[] source = + [ + Result.Success(), + Error.Validation("First.Error", "First"), + Error.Validation("Second.Error", "Second") + ]; + + Result result = source.CombineAll(); + + result.IsFailure.Should().BeTrue(); + result.Errors.Select(x => x.Code).Should().Equal("First.Error", "Second.Error"); + } + + [Fact] + public void FirstFailureOrSuccesses_WithSuccesses_ShouldReturnValues() + { + Result[] source = [1, 2, 3]; + + Result result = source.FirstFailureOrSuccesses(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Equal(1, 2, 3); + } + + [Fact] + public void FirstFailureOrSuccesses_ShouldShortCircuitOnFirstFailure() + { + static IEnumerable> Source() + { + yield return 1; + yield return Error.Validation("Stop.Error", "Stop"); + throw new InvalidOperationException("Enumeration should stop at first failure."); + } + + Result result = Source().FirstFailureOrSuccesses(); + + result.IsFailure.Should().BeTrue(); + result.Errors.Should().ContainSingle(x => x.Code == "Stop.Error"); + } +} diff --git a/README.MD b/README.MD index 08cd369..906ef86 100644 --- a/README.MD +++ b/README.MD @@ -5,7 +5,7 @@ # CSharpEssentials [![Build](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml/badge.svg)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) -[![Tests](https://img.shields.io/badge/tests-2708%20passing-brightgreen)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) +[![Tests](https://img.shields.io/badge/tests-2722%20passing-brightgreen)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) [![NuGet](https://img.shields.io/nuget/v/CSharpEssentials.svg)](https://www.nuget.org/packages/CSharpEssentials) [![Downloads](https://img.shields.io/nuget/dt/CSharpEssentials.svg)](https://www.nuget.org/packages/CSharpEssentials) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/senrecep/CSharpEssentials/blob/main/LICENCE) From 4f22639205b1df0c1e2a4d949f5e25ac593ebb32 Mon Sep 17 00:00:00 2001 From: Erdem Date: Wed, 27 May 2026 18:31:52 +0300 Subject: [PATCH 2/5] fix(collections): remove boxing from batch result APIs --- .../AnyCollectionExtensions.cs | 14 --- .../Extensions/ResultCollectionExtensions.cs | 75 ++++++++++++++-- .../Any/AnyCollectionExtensionsTests.cs | 86 ++++++++++++++++++- README.MD | 2 +- 4 files changed, 152 insertions(+), 25 deletions(-) diff --git a/CSharpEssentials.Any/AnyCollectionExtensions.cs b/CSharpEssentials.Any/AnyCollectionExtensions.cs index a51ab98..fa8cd1e 100644 --- a/CSharpEssentials.Any/AnyCollectionExtensions.cs +++ b/CSharpEssentials.Any/AnyCollectionExtensions.cs @@ -2,8 +2,6 @@ namespace CSharpEssentials.Any; public static partial class AnyExtensions { - public static (T0[] First, T1[] Second) Sequence(this IEnumerable> source) => source.Partition(); - public static (T0[] First, T1[] Second) Traverse(this IEnumerable source, Func> selector) => source.Select(selector).Partition(); @@ -18,8 +16,6 @@ public static (T0[] First, T1[] Second) Partition(this IEnumerable(this IEnumerable> source) => source.Partition(); - public static (T0[] First, T1[] Second, T2[] Third) Traverse(this IEnumerable source, Func> selector) => source.Select(selector).Partition(); @@ -35,8 +31,6 @@ public static (T0[] First, T1[] Second, T2[] Third) Partition(this I return (first.ToArray(), second.ToArray(), third.ToArray()); } - public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth) Sequence(this IEnumerable> source) => source.Partition(); - public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth) Traverse(this IEnumerable source, Func> selector) => source.Select(selector).Partition(); @@ -53,8 +47,6 @@ public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth) Partition(this IEnumerable> source) => source.Partition(); - public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth) Traverse(this IEnumerable source, Func> selector) => source.Select(selector).Partition(); @@ -72,8 +64,6 @@ public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth) Par return (first.ToArray(), second.ToArray(), third.ToArray(), fourth.ToArray(), fifth.ToArray()); } - public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth) Sequence(this IEnumerable> source) => source.Partition(); - public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth) Traverse(this IEnumerable source, Func> selector) => source.Select(selector).Partition(); @@ -92,8 +82,6 @@ public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[ return (first.ToArray(), second.ToArray(), third.ToArray(), fourth.ToArray(), fifth.ToArray(), sixth.ToArray()); } - public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh) Sequence(this IEnumerable> source) => source.Partition(); - public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh) Traverse(this IEnumerable source, Func> selector) => source.Select(selector).Partition(); @@ -113,8 +101,6 @@ public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[ return (first.ToArray(), second.ToArray(), third.ToArray(), fourth.ToArray(), fifth.ToArray(), sixth.ToArray(), seventh.ToArray()); } - public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh, T7[] Eighth) Sequence(this IEnumerable> source) => source.Partition(); - public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh, T7[] Eighth) Traverse(this IEnumerable source, Func> selector) => source.Select(selector).Partition(); diff --git a/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs b/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs index 7ec05a2..00fecb5 100644 --- a/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs +++ b/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs @@ -5,7 +5,18 @@ namespace CSharpEssentials.ResultPattern; public static partial class ResultExtensions { - public static Result CombineAll(this IEnumerable source) => source.Cast().CombineAll(); + public static Result CombineAll(this IEnumerable source) + { + List errors = []; + + foreach (Result result in source) + { + if (result.IsFailure) + errors.AddRange(result.ErrorsOrEmptyArray); + } + + return errors.Count == 0 ? Result.Success() : errors; + } public static Result CombineAll(this IEnumerable source) { @@ -20,11 +31,25 @@ public static Result CombineAll(this IEnumerable source) return errors.Count == 0 ? Result.Success() : errors; } - public static Result CombineAll(this IEnumerable> source) => source.Cast>().CombineAll(); + public static Result CombineAll(this IEnumerable> source) => source.Sequence(); public static Result CombineAll(this IEnumerable> source) => source.Sequence(); - public static Result Sequence(this IEnumerable> source) => source.Cast>().Sequence(); + public static Result Sequence(this IEnumerable> source) + { + List successes = []; + List errors = []; + + foreach (Result result in source) + { + if (result.IsSuccess) + successes.Add(result.Value); + else + errors.AddRange(result.ErrorsOrEmptyArray); + } + + return errors.Count == 0 ? successes.ToArray() : errors; + } public static Result Sequence(this IEnumerable> source) { @@ -45,8 +70,21 @@ public static Result Sequence(this IEnumerable public static Result Traverse(this IEnumerable source, Func> selector) => source.Select(selector).Sequence(); - public static (TValue[] Successes, Error[] Errors) Partition(this IEnumerable> source) => - source.Cast>().Partition(); + public static (TValue[] Successes, Error[] Errors) Partition(this IEnumerable> source) + { + List successes = []; + List errors = []; + + foreach (Result result in source) + { + if (result.IsSuccess) + successes.Add(result.Value); + else + errors.AddRange(result.ErrorsOrEmptyArray); + } + + return (successes.ToArray(), errors.ToArray()); + } public static (TValue[] Successes, Error[] Errors) Partition(this IEnumerable> source) { @@ -64,7 +102,16 @@ public static (TValue[] Successes, Error[] Errors) Partition(this IEnume return (successes.ToArray(), errors.ToArray()); } - public static Result FirstFailureOrSuccesses(this IEnumerable source) => source.Cast().FirstFailureOrSuccesses(); + public static Result FirstFailureOrSuccesses(this IEnumerable source) + { + foreach (Result result in source) + { + if (result.IsFailure) + return result.ErrorsOrEmptyArray; + } + + return Result.Success(); + } public static Result FirstFailureOrSuccesses(this IEnumerable source) { @@ -77,8 +124,20 @@ public static Result FirstFailureOrSuccesses(this IEnumerable sourc return Result.Success(); } - public static Result FirstFailureOrSuccesses(this IEnumerable> source) => - source.Cast>().FirstFailureOrSuccesses(); + public static Result FirstFailureOrSuccesses(this IEnumerable> source) + { + List successes = []; + + foreach (Result result in source) + { + if (result.IsFailure) + return result.ErrorsOrEmptyArray; + + successes.Add(result.Value); + } + + return successes.ToArray(); + } public static Result FirstFailureOrSuccesses(this IEnumerable> source) { diff --git a/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs b/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs index 8221521..7a19ba0 100644 --- a/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs +++ b/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs @@ -17,17 +17,99 @@ public void Partition_T2_ShouldSplitBranches() } [Fact] - public void Sequence_T3_ShouldSplitAllBranches() + public void Partition_T3_ShouldSplitAllBranches() { Any[] source = [1, "two", true, 3, false]; - (int[] first, string[] second, bool[] third) = source.Sequence(); + (int[] first, string[] second, bool[] third) = source.Partition(); first.Should().Equal(1, 3); second.Should().Equal("two"); third.Should().Equal(true, false); } + [Fact] + public void Partition_T4_ShouldSplitAllBranches() + { + Any[] source = [1, "two", true, 4.5m]; + + var result = source.Partition(); + + result.First.Should().Equal(1); + result.Second.Should().Equal("two"); + result.Third.Should().Equal(true); + result.Fourth.Should().Equal(4.5m); + } + + [Fact] + public void Partition_T5_ShouldSplitAllBranches() + { + Any[] source = + [ + 1, + "two", + true, + 4.5m, + Guid.Parse("11111111-1111-1111-1111-111111111111") + ]; + + var result = source.Partition(); + + result.First.Should().Equal(1); + result.Second.Should().Equal("two"); + result.Third.Should().Equal(true); + result.Fourth.Should().Equal(4.5m); + result.Fifth.Should().Equal(Guid.Parse("11111111-1111-1111-1111-111111111111")); + } + + [Fact] + public void Partition_T6_ShouldSplitAllBranches() + { + Any[] source = + [ + 1, + "two", + true, + 4.5m, + Guid.Parse("11111111-1111-1111-1111-111111111111"), + 6L + ]; + + var result = source.Partition(); + + result.First.Should().Equal(1); + result.Second.Should().Equal("two"); + result.Third.Should().Equal(true); + result.Fourth.Should().Equal(4.5m); + result.Fifth.Should().Equal(Guid.Parse("11111111-1111-1111-1111-111111111111")); + result.Sixth.Should().Equal(6L); + } + + [Fact] + public void Partition_T7_ShouldSplitAllBranches() + { + Any[] source = + [ + 1, + "two", + true, + 4.5m, + Guid.Parse("11111111-1111-1111-1111-111111111111"), + 6L, + new DateTime(2026, 5, 27, 0, 0, 0, DateTimeKind.Utc) + ]; + + var result = source.Partition(); + + result.First.Should().Equal(1); + result.Second.Should().Equal("two"); + result.Third.Should().Equal(true); + result.Fourth.Should().Equal(4.5m); + result.Fifth.Should().Equal(Guid.Parse("11111111-1111-1111-1111-111111111111")); + result.Sixth.Should().Equal(6L); + result.Seventh.Should().Equal(new DateTime(2026, 5, 27, 0, 0, 0, DateTimeKind.Utc)); + } + [Fact] public void Traverse_T8_ShouldProjectAndPartition() { diff --git a/README.MD b/README.MD index 906ef86..4406821 100644 --- a/README.MD +++ b/README.MD @@ -5,7 +5,7 @@ # CSharpEssentials [![Build](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml/badge.svg)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) -[![Tests](https://img.shields.io/badge/tests-2722%20passing-brightgreen)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) +[![Tests](https://img.shields.io/badge/tests-2726%20passing-brightgreen)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) [![NuGet](https://img.shields.io/nuget/v/CSharpEssentials.svg)](https://www.nuget.org/packages/CSharpEssentials) [![Downloads](https://img.shields.io/nuget/dt/CSharpEssentials.svg)](https://www.nuget.org/packages/CSharpEssentials) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/senrecep/CSharpEssentials/blob/main/LICENCE) From ee8594ac25b12613af2dee56484c2f36fdff96bc Mon Sep 17 00:00:00 2001 From: Erdem Date: Wed, 27 May 2026 19:05:21 +0300 Subject: [PATCH 3/5] fix(collections): guard enumerable batch APIs --- .../AnyCollectionExtensions.cs | 33 ++++-- .../Extensions/MaybeCollectionExtensions.cs | 16 ++- .../Extensions/ResultCollectionExtensions.cs | 26 ++++- .../Any/AnyCollectionExtensionsTests.cs | 30 +++++ .../Maybe/MaybeCollectionExtensionsTests.cs | 17 +++ .../Results/ResultCollectionBatchTests.cs | 108 ++++++++++++++++++ README.MD | 2 +- 7 files changed, 221 insertions(+), 11 deletions(-) diff --git a/CSharpEssentials.Any/AnyCollectionExtensions.cs b/CSharpEssentials.Any/AnyCollectionExtensions.cs index fa8cd1e..f436b15 100644 --- a/CSharpEssentials.Any/AnyCollectionExtensions.cs +++ b/CSharpEssentials.Any/AnyCollectionExtensions.cs @@ -2,11 +2,24 @@ namespace CSharpEssentials.Any; public static partial class AnyExtensions { + private static IEnumerable EnsureSource(IEnumerable source) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + throw new ArgumentNullException(nameof(source)); +#endif + + return source; + } + public static (T0[] First, T1[] Second) Traverse(this IEnumerable source, Func> selector) => - source.Select(selector).Partition(); + EnsureSource(source).Select(selector).Partition(); public static (T0[] First, T1[] Second) Partition(this IEnumerable> source) { + source = EnsureSource(source); List first = []; List second = []; @@ -17,10 +30,11 @@ public static (T0[] First, T1[] Second) Partition(this IEnumerable(this IEnumerable source, Func> selector) => - source.Select(selector).Partition(); + EnsureSource(source).Select(selector).Partition(); public static (T0[] First, T1[] Second, T2[] Third) Partition(this IEnumerable> source) { + source = EnsureSource(source); List first = []; List second = []; List third = []; @@ -32,10 +46,11 @@ public static (T0[] First, T1[] Second, T2[] Third) Partition(this I } public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth) Traverse(this IEnumerable source, Func> selector) => - source.Select(selector).Partition(); + EnsureSource(source).Select(selector).Partition(); public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth) Partition(this IEnumerable> source) { + source = EnsureSource(source); List first = []; List second = []; List third = []; @@ -48,10 +63,11 @@ public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth) Partition(this IEnumerable source, Func> selector) => - source.Select(selector).Partition(); + EnsureSource(source).Select(selector).Partition(); public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth) Partition(this IEnumerable> source) { + source = EnsureSource(source); List first = []; List second = []; List third = []; @@ -65,10 +81,11 @@ public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth) Par } public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth) Traverse(this IEnumerable source, Func> selector) => - source.Select(selector).Partition(); + EnsureSource(source).Select(selector).Partition(); public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth) Partition(this IEnumerable> source) { + source = EnsureSource(source); List first = []; List second = []; List third = []; @@ -83,10 +100,11 @@ public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[ } public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh) Traverse(this IEnumerable source, Func> selector) => - source.Select(selector).Partition(); + EnsureSource(source).Select(selector).Partition(); public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh) Partition(this IEnumerable> source) { + source = EnsureSource(source); List first = []; List second = []; List third = []; @@ -102,10 +120,11 @@ public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[ } public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh, T7[] Eighth) Traverse(this IEnumerable source, Func> selector) => - source.Select(selector).Partition(); + EnsureSource(source).Select(selector).Partition(); public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh, T7[] Eighth) Partition(this IEnumerable> source) { + source = EnsureSource(source); List first = []; List second = []; List third = []; diff --git a/CSharpEssentials.Maybe/Extensions/MaybeCollectionExtensions.cs b/CSharpEssentials.Maybe/Extensions/MaybeCollectionExtensions.cs index 3f624c2..9977488 100644 --- a/CSharpEssentials.Maybe/Extensions/MaybeCollectionExtensions.cs +++ b/CSharpEssentials.Maybe/Extensions/MaybeCollectionExtensions.cs @@ -2,8 +2,21 @@ namespace CSharpEssentials.Maybe; public static partial class MaybeExtensions { + private static IEnumerable EnsureSource(IEnumerable source) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + throw new ArgumentNullException(nameof(source)); +#endif + + return source; + } + public static Maybe Sequence(this IEnumerable> source) { + source = EnsureSource(source); List values = []; foreach (Maybe maybe in source) @@ -18,10 +31,11 @@ public static Maybe Sequence(this IEnumerable> s } public static Maybe Traverse(this IEnumerable source, Func> selector) => - source.Select(selector).Sequence(); + EnsureSource(source).Select(selector).Sequence(); public static (TValue[] Values, int NoneCount) Partition(this IEnumerable> source) { + source = EnsureSource(source); List values = []; int noneCount = 0; diff --git a/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs b/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs index 00fecb5..2f0d8a2 100644 --- a/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs +++ b/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs @@ -5,8 +5,21 @@ namespace CSharpEssentials.ResultPattern; public static partial class ResultExtensions { + private static IEnumerable EnsureSource(IEnumerable source) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(source); +#else + if (source is null) + throw new ArgumentNullException(nameof(source)); +#endif + + return source; + } + public static Result CombineAll(this IEnumerable source) { + source = EnsureSource(source); List errors = []; foreach (Result result in source) @@ -20,6 +33,7 @@ public static Result CombineAll(this IEnumerable source) public static Result CombineAll(this IEnumerable source) { + source = EnsureSource(source); List errors = []; foreach (IResultBase result in source) @@ -33,10 +47,11 @@ public static Result CombineAll(this IEnumerable source) public static Result CombineAll(this IEnumerable> source) => source.Sequence(); - public static Result CombineAll(this IEnumerable> source) => source.Sequence(); + public static Result CombineAll(this IEnumerable> source) => EnsureSource(source).Sequence(); public static Result Sequence(this IEnumerable> source) { + source = EnsureSource(source); List successes = []; List errors = []; @@ -53,6 +68,7 @@ public static Result Sequence(this IEnumerable> public static Result Sequence(this IEnumerable> source) { + source = EnsureSource(source); List successes = []; List errors = []; @@ -68,10 +84,11 @@ public static Result Sequence(this IEnumerable } public static Result Traverse(this IEnumerable source, Func> selector) => - source.Select(selector).Sequence(); + EnsureSource(source).Select(selector).Sequence(); public static (TValue[] Successes, Error[] Errors) Partition(this IEnumerable> source) { + source = EnsureSource(source); List successes = []; List errors = []; @@ -88,6 +105,7 @@ public static (TValue[] Successes, Error[] Errors) Partition(this IEnume public static (TValue[] Successes, Error[] Errors) Partition(this IEnumerable> source) { + source = EnsureSource(source); List successes = []; List errors = []; @@ -104,6 +122,7 @@ public static (TValue[] Successes, Error[] Errors) Partition(this IEnume public static Result FirstFailureOrSuccesses(this IEnumerable source) { + source = EnsureSource(source); foreach (Result result in source) { if (result.IsFailure) @@ -115,6 +134,7 @@ public static Result FirstFailureOrSuccesses(this IEnumerable source) public static Result FirstFailureOrSuccesses(this IEnumerable source) { + source = EnsureSource(source); foreach (IResultBase result in source) { if (result.IsFailure) @@ -126,6 +146,7 @@ public static Result FirstFailureOrSuccesses(this IEnumerable sourc public static Result FirstFailureOrSuccesses(this IEnumerable> source) { + source = EnsureSource(source); List successes = []; foreach (Result result in source) @@ -141,6 +162,7 @@ public static Result FirstFailureOrSuccesses(this IEnumerable< public static Result FirstFailureOrSuccesses(this IEnumerable> source) { + source = EnsureSource(source); List successes = []; foreach (IResult result in source) diff --git a/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs b/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs index 7a19ba0..593b24a 100644 --- a/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs +++ b/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs @@ -1,5 +1,6 @@ using CSharpEssentials.Any; using FluentAssertions; +using System.Globalization; namespace CSharpEssentials.Tests.Any; @@ -136,4 +137,33 @@ public void Traverse_T8_ShouldProjectAndPartition() result.Seventh.Should().Equal(Guid.Parse("11111111-1111-1111-1111-111111111111")); result.Eighth.Should().Equal(new DateTime(2026, 5, 27, 0, 0, 0, DateTimeKind.Utc)); } + + [Fact] + public void CollectionExtensions_WithNullSource_ShouldThrowArgumentNullException() + { + IEnumerable> any2 = null!; + IEnumerable> any8 = null!; + IEnumerable values = null!; + + Action[] actions = + [ + () => any2.Partition(), + () => any8.Partition(), + () => values.Traverse(value => value), + () => values.Traverse(value => value switch + { + 0 => value, + 1 => value.ToString(CultureInfo.InvariantCulture), + 2 => true, + 3 => 3.5m, + 4 => Guid.Empty, + 5 => 5L, + 6 => DateTime.UnixEpoch, + _ => TimeSpan.Zero + }) + ]; + + foreach (Action action in actions) + action.Should().Throw().WithParameterName("source"); + } } diff --git a/CSharpEssentials.Tests/Maybe/MaybeCollectionExtensionsTests.cs b/CSharpEssentials.Tests/Maybe/MaybeCollectionExtensionsTests.cs index f99d84b..4ff95a7 100644 --- a/CSharpEssentials.Tests/Maybe/MaybeCollectionExtensionsTests.cs +++ b/CSharpEssentials.Tests/Maybe/MaybeCollectionExtensionsTests.cs @@ -47,4 +47,21 @@ public void Partition_ShouldReturnValuesAndNoneCount() values.Should().Equal(1, 3); noneCount.Should().Be(2); } + + [Fact] + public void CollectionExtensions_WithNullSource_ShouldThrowArgumentNullException() + { + IEnumerable> maybes = null!; + IEnumerable values = null!; + + Action[] actions = + [ + () => maybes.Sequence(), + () => maybes.Partition(), + () => values.Traverse(value => Maybe.From(value)) + ]; + + foreach (Action action in actions) + action.Should().Throw().WithParameterName("source"); + } } diff --git a/CSharpEssentials.Tests/Results/ResultCollectionBatchTests.cs b/CSharpEssentials.Tests/Results/ResultCollectionBatchTests.cs index 1680b4a..4ce59dd 100644 --- a/CSharpEssentials.Tests/Results/ResultCollectionBatchTests.cs +++ b/CSharpEssentials.Tests/Results/ResultCollectionBatchTests.cs @@ -1,5 +1,6 @@ using CSharpEssentials.Errors; using CSharpEssentials.ResultPattern; +using CSharpEssentials.ResultPattern.Interfaces; using FluentAssertions; namespace CSharpEssentials.Tests.Results; @@ -77,6 +78,29 @@ public void CombineAll_NonGeneric_ShouldAggregateErrors() result.Errors.Select(x => x.Code).Should().Equal("First.Error", "Second.Error"); } + [Fact] + public void CombineAll_IResultBase_ShouldAggregateErrors() + { + IResultBase[] source = + [ + Result.Success(), + Result.Failure(Error.Validation("First.Error", "First")), + Result.Failure(Error.Validation("Second.Error", "Second")) + ]; + + Result result = source.CombineAll(); + + result.IsFailure.Should().BeTrue(); + result.Errors.Select(x => x.Code).Should().Equal("First.Error", "Second.Error"); + } + + [Fact] + public void CombineAll_WithEmptyCollections_ShouldReturnSuccess() + { + Array.Empty().CombineAll().IsSuccess.Should().BeTrue(); + Array.Empty().CombineAll().IsSuccess.Should().BeTrue(); + } + [Fact] public void FirstFailureOrSuccesses_WithSuccesses_ShouldReturnValues() { @@ -88,6 +112,45 @@ public void FirstFailureOrSuccesses_WithSuccesses_ShouldReturnValues() result.Value.Should().Equal(1, 2, 3); } + [Fact] + public void Sequence_IResultOverload_ShouldReturnValues() + { + IResult[] source = [Result.Success(1), Result.Success(2), Result.Success(3)]; + + Result result = source.Sequence(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Equal(1, 2, 3); + } + + [Fact] + public void Partition_IResultOverload_ShouldReturnSuccessesAndFlattenedErrors() + { + IResult[] source = + [ + Result.Success(10), + Result.Failure(Error.Validation("First.Error", "First")), + Result.Success(20), + Result.Failure(Error.Validation("Second.Error", "Second")) + ]; + + (int[] successes, Error[] errors) = source.Partition(); + + successes.Should().Equal(10, 20); + errors.Select(x => x.Code).Should().Equal("First.Error", "Second.Error"); + } + + [Fact] + public void FirstFailureOrSuccesses_IResultOverload_WithSuccesses_ShouldReturnValues() + { + IResult[] source = [Result.Success(1), Result.Success(2), Result.Success(3)]; + + Result result = source.FirstFailureOrSuccesses(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Equal(1, 2, 3); + } + [Fact] public void FirstFailureOrSuccesses_ShouldShortCircuitOnFirstFailure() { @@ -103,4 +166,49 @@ static IEnumerable> Source() result.IsFailure.Should().BeTrue(); result.Errors.Should().ContainSingle(x => x.Code == "Stop.Error"); } + + [Fact] + public void ResultCollectionExtensions_WithEmptyCollections_ShouldReturnEmptySuccesses() + { + Array.Empty>().Sequence().Value.Should().BeEmpty(); + Array.Empty>().Sequence().Value.Should().BeEmpty(); + Array.Empty>().FirstFailureOrSuccesses().Value.Should().BeEmpty(); + Array.Empty>().FirstFailureOrSuccesses().Value.Should().BeEmpty(); + + (int[] structValues, Error[] structErrors) = Array.Empty>().Partition(); + (int[] interfaceValues, Error[] interfaceErrors) = Array.Empty>().Partition(); + + structValues.Should().BeEmpty(); + structErrors.Should().BeEmpty(); + interfaceValues.Should().BeEmpty(); + interfaceErrors.Should().BeEmpty(); + } + + [Fact] + public void ResultCollectionExtensions_WithNullSource_ShouldThrowArgumentNullException() + { + IEnumerable results = null!; + IEnumerable resultBases = null!; + IEnumerable> genericResults = null!; + IEnumerable> genericInterfaces = null!; + IEnumerable values = null!; + + Action[] actions = + [ + () => results.CombineAll(), + () => resultBases.CombineAll(), + () => genericResults.Sequence(), + () => genericInterfaces.Sequence(), + () => genericResults.Partition(), + () => genericInterfaces.Partition(), + () => results.FirstFailureOrSuccesses(), + () => resultBases.FirstFailureOrSuccesses(), + () => genericResults.FirstFailureOrSuccesses(), + () => genericInterfaces.FirstFailureOrSuccesses(), + () => values.Traverse(value => value.ToResult()) + ]; + + foreach (Action action in actions) + action.Should().Throw().WithParameterName("source"); + } } diff --git a/README.MD b/README.MD index 4406821..2289def 100644 --- a/README.MD +++ b/README.MD @@ -5,7 +5,7 @@ # CSharpEssentials [![Build](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml/badge.svg)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) -[![Tests](https://img.shields.io/badge/tests-2726%20passing-brightgreen)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) +[![Tests](https://img.shields.io/badge/tests-2735%20passing-brightgreen)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) [![NuGet](https://img.shields.io/nuget/v/CSharpEssentials.svg)](https://www.nuget.org/packages/CSharpEssentials) [![Downloads](https://img.shields.io/nuget/dt/CSharpEssentials.svg)](https://www.nuget.org/packages/CSharpEssentials) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/senrecep/CSharpEssentials/blob/main/LICENCE) From 5d47acbb4ec5e28975cd79df0fa39e758bfe41e6 Mon Sep 17 00:00:00 2001 From: Erdem Date: Wed, 27 May 2026 19:23:29 +0300 Subject: [PATCH 4/5] fix(collections): guard traverse selectors --- .../AnyCollectionExtensions.cs | 26 ++++++++++++++----- .../Extensions/MaybeCollectionExtensions.cs | 14 +++++++++- .../Extensions/ResultCollectionExtensions.cs | 14 +++++++++- .../Any/AnyCollectionExtensionsTests.cs | 20 ++++++++++++++ .../Maybe/MaybeCollectionExtensionsTests.cs | 23 ++++++++++++++++ .../Results/ResultCollectionBatchTests.cs | 15 +++++++++++ 6 files changed, 103 insertions(+), 9 deletions(-) diff --git a/CSharpEssentials.Any/AnyCollectionExtensions.cs b/CSharpEssentials.Any/AnyCollectionExtensions.cs index f436b15..e282c27 100644 --- a/CSharpEssentials.Any/AnyCollectionExtensions.cs +++ b/CSharpEssentials.Any/AnyCollectionExtensions.cs @@ -14,8 +14,20 @@ private static IEnumerable EnsureSource(IEnumerable source) return source; } + private static Func EnsureSelector(Func selector) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(selector); +#else + if (selector is null) + throw new ArgumentNullException(nameof(selector)); +#endif + + return selector; + } + public static (T0[] First, T1[] Second) Traverse(this IEnumerable source, Func> selector) => - EnsureSource(source).Select(selector).Partition(); + EnsureSource(source).Select(EnsureSelector(selector)).Partition(); public static (T0[] First, T1[] Second) Partition(this IEnumerable> source) { @@ -30,7 +42,7 @@ public static (T0[] First, T1[] Second) Partition(this IEnumerable(this IEnumerable source, Func> selector) => - EnsureSource(source).Select(selector).Partition(); + EnsureSource(source).Select(EnsureSelector(selector)).Partition(); public static (T0[] First, T1[] Second, T2[] Third) Partition(this IEnumerable> source) { @@ -46,7 +58,7 @@ public static (T0[] First, T1[] Second, T2[] Third) Partition(this I } public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth) Traverse(this IEnumerable source, Func> selector) => - EnsureSource(source).Select(selector).Partition(); + EnsureSource(source).Select(EnsureSelector(selector)).Partition(); public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth) Partition(this IEnumerable> source) { @@ -63,7 +75,7 @@ public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth) Partition(this IEnumerable source, Func> selector) => - EnsureSource(source).Select(selector).Partition(); + EnsureSource(source).Select(EnsureSelector(selector)).Partition(); public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth) Partition(this IEnumerable> source) { @@ -81,7 +93,7 @@ public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth) Par } public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth) Traverse(this IEnumerable source, Func> selector) => - EnsureSource(source).Select(selector).Partition(); + EnsureSource(source).Select(EnsureSelector(selector)).Partition(); public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth) Partition(this IEnumerable> source) { @@ -100,7 +112,7 @@ public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[ } public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh) Traverse(this IEnumerable source, Func> selector) => - EnsureSource(source).Select(selector).Partition(); + EnsureSource(source).Select(EnsureSelector(selector)).Partition(); public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh) Partition(this IEnumerable> source) { @@ -120,7 +132,7 @@ public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[ } public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh, T7[] Eighth) Traverse(this IEnumerable source, Func> selector) => - EnsureSource(source).Select(selector).Partition(); + EnsureSource(source).Select(EnsureSelector(selector)).Partition(); public static (T0[] First, T1[] Second, T2[] Third, T3[] Fourth, T4[] Fifth, T5[] Sixth, T6[] Seventh, T7[] Eighth) Partition(this IEnumerable> source) { diff --git a/CSharpEssentials.Maybe/Extensions/MaybeCollectionExtensions.cs b/CSharpEssentials.Maybe/Extensions/MaybeCollectionExtensions.cs index 9977488..0e0a416 100644 --- a/CSharpEssentials.Maybe/Extensions/MaybeCollectionExtensions.cs +++ b/CSharpEssentials.Maybe/Extensions/MaybeCollectionExtensions.cs @@ -14,6 +14,18 @@ private static IEnumerable EnsureSource(IEnumerable source) return source; } + private static Func EnsureSelector(Func selector) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(selector); +#else + if (selector is null) + throw new ArgumentNullException(nameof(selector)); +#endif + + return selector; + } + public static Maybe Sequence(this IEnumerable> source) { source = EnsureSource(source); @@ -31,7 +43,7 @@ public static Maybe Sequence(this IEnumerable> s } public static Maybe Traverse(this IEnumerable source, Func> selector) => - EnsureSource(source).Select(selector).Sequence(); + EnsureSource(source).Select(EnsureSelector(selector)).Sequence(); public static (TValue[] Values, int NoneCount) Partition(this IEnumerable> source) { diff --git a/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs b/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs index 2f0d8a2..d067844 100644 --- a/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs +++ b/CSharpEssentials.Results/Extensions/ResultCollectionExtensions.cs @@ -17,6 +17,18 @@ private static IEnumerable EnsureSource(IEnumerable source) return source; } + private static Func EnsureSelector(Func selector) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(selector); +#else + if (selector is null) + throw new ArgumentNullException(nameof(selector)); +#endif + + return selector; + } + public static Result CombineAll(this IEnumerable source) { source = EnsureSource(source); @@ -84,7 +96,7 @@ public static Result Sequence(this IEnumerable } public static Result Traverse(this IEnumerable source, Func> selector) => - EnsureSource(source).Select(selector).Sequence(); + EnsureSource(source).Select(EnsureSelector(selector)).Sequence(); public static (TValue[] Successes, Error[] Errors) Partition(this IEnumerable> source) { diff --git a/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs b/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs index 593b24a..058cef3 100644 --- a/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs +++ b/CSharpEssentials.Tests/Any/AnyCollectionExtensionsTests.cs @@ -138,6 +138,15 @@ public void Traverse_T8_ShouldProjectAndPartition() result.Eighth.Should().Equal(new DateTime(2026, 5, 27, 0, 0, 0, DateTimeKind.Utc)); } + [Fact] + public void Partition_WithEmptySource_ShouldReturnEmptyArrays() + { + var result = Array.Empty>().Partition(); + + result.First.Should().BeEmpty(); + result.Second.Should().BeEmpty(); + } + [Fact] public void CollectionExtensions_WithNullSource_ShouldThrowArgumentNullException() { @@ -166,4 +175,15 @@ public void CollectionExtensions_WithNullSource_ShouldThrowArgumentNullException foreach (Action action in actions) action.Should().Throw().WithParameterName("source"); } + + [Fact] + public void Traverse_WithNullSelector_ShouldThrowArgumentNullException() + { + int[] values = [1]; + Func> selector = null!; + + Action action = () => values.Traverse(selector); + + action.Should().Throw().WithParameterName("selector"); + } } diff --git a/CSharpEssentials.Tests/Maybe/MaybeCollectionExtensionsTests.cs b/CSharpEssentials.Tests/Maybe/MaybeCollectionExtensionsTests.cs index 4ff95a7..cd741f7 100644 --- a/CSharpEssentials.Tests/Maybe/MaybeCollectionExtensionsTests.cs +++ b/CSharpEssentials.Tests/Maybe/MaybeCollectionExtensionsTests.cs @@ -48,6 +48,18 @@ public void Partition_ShouldReturnValuesAndNoneCount() noneCount.Should().Be(2); } + [Fact] + public void CollectionExtensions_WithEmptySource_ShouldReturnEmptyValues() + { + Maybe sequence = Array.Empty>().Sequence(); + (int[] values, int noneCount) = Array.Empty>().Partition(); + + sequence.HasValue.Should().BeTrue(); + sequence.Value.Should().BeEmpty(); + values.Should().BeEmpty(); + noneCount.Should().Be(0); + } + [Fact] public void CollectionExtensions_WithNullSource_ShouldThrowArgumentNullException() { @@ -64,4 +76,15 @@ public void CollectionExtensions_WithNullSource_ShouldThrowArgumentNullException foreach (Action action in actions) action.Should().Throw().WithParameterName("source"); } + + [Fact] + public void Traverse_WithNullSelector_ShouldThrowArgumentNullException() + { + int[] values = [1]; + Func> selector = null!; + + Action action = () => values.Traverse(selector); + + action.Should().Throw().WithParameterName("selector"); + } } diff --git a/CSharpEssentials.Tests/Results/ResultCollectionBatchTests.cs b/CSharpEssentials.Tests/Results/ResultCollectionBatchTests.cs index 4ce59dd..61a8804 100644 --- a/CSharpEssentials.Tests/Results/ResultCollectionBatchTests.cs +++ b/CSharpEssentials.Tests/Results/ResultCollectionBatchTests.cs @@ -99,6 +99,8 @@ public void CombineAll_WithEmptyCollections_ShouldReturnSuccess() { Array.Empty().CombineAll().IsSuccess.Should().BeTrue(); Array.Empty().CombineAll().IsSuccess.Should().BeTrue(); + Array.Empty>().CombineAll().Value.Should().BeEmpty(); + Array.Empty>().CombineAll().Value.Should().BeEmpty(); } [Fact] @@ -197,6 +199,8 @@ public void ResultCollectionExtensions_WithNullSource_ShouldThrowArgumentNullExc [ () => results.CombineAll(), () => resultBases.CombineAll(), + () => genericResults.CombineAll(), + () => genericInterfaces.CombineAll(), () => genericResults.Sequence(), () => genericInterfaces.Sequence(), () => genericResults.Partition(), @@ -211,4 +215,15 @@ public void ResultCollectionExtensions_WithNullSource_ShouldThrowArgumentNullExc foreach (Action action in actions) action.Should().Throw().WithParameterName("source"); } + + [Fact] + public void Traverse_WithNullSelector_ShouldThrowArgumentNullException() + { + int[] values = [1]; + Func> selector = null!; + + Action action = () => values.Traverse(selector); + + action.Should().Throw().WithParameterName("selector"); + } } From db4f2463144518b43aaa3d7788e6869988e5f297 Mon Sep 17 00:00:00 2001 From: Erdem Date: Wed, 27 May 2026 19:23:41 +0300 Subject: [PATCH 5/5] docs(readme): refresh test badge count --- README.MD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.MD b/README.MD index 2289def..e222781 100644 --- a/README.MD +++ b/README.MD @@ -5,7 +5,7 @@ # CSharpEssentials [![Build](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml/badge.svg)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) -[![Tests](https://img.shields.io/badge/tests-2735%20passing-brightgreen)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) +[![Tests](https://img.shields.io/badge/tests-2740%20passing-brightgreen)](https://github.com/senrecep/CSharpEssentials/actions/workflows/build.yml) [![NuGet](https://img.shields.io/nuget/v/CSharpEssentials.svg)](https://www.nuget.org/packages/CSharpEssentials) [![Downloads](https://img.shields.io/nuget/dt/CSharpEssentials.svg)](https://www.nuget.org/packages/CSharpEssentials) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/senrecep/CSharpEssentials/blob/main/LICENCE)