From d5769d6d0a998672f8ebdcb6ac7c8adf59a2259d Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Sun, 20 Jul 2025 21:05:23 +0200 Subject: [PATCH 1/2] chore: add plan for issue-118 --- ...must-have-length-in-for-immutable-array.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Code/Plans/issue-118-must-have-length-in-for-immutable-array.md diff --git a/Code/Plans/issue-118-must-have-length-in-for-immutable-array.md b/Code/Plans/issue-118-must-have-length-in-for-immutable-array.md new file mode 100644 index 0000000..cc15734 --- /dev/null +++ b/Code/Plans/issue-118-must-have-length-in-for-immutable-array.md @@ -0,0 +1,23 @@ +# Issue 118 - MustHaveLengthIn for ImmutableArray + +## Context + +The .NET library Light.GuardClauses already has several assertions for collections. They often rely on `IEnumerable` or `IEnumerable`. However, these assertions would result in `ImmutableArray` being boxed - we want to avoid that by providing dedicated assertions for this type which avoids boxing. For this issue, we implement the `MustHaveLengthIn` assertion for `ImmutableArray`. + +## Tasks for this issue + +- [ ] The production code should be placed in the Light.GuardClauses project. The file `Check.MustHaveLengthIn.cs` already exists in the root folder of the project - extend this existing file. +- [ ] In this file, create several extension method overloads called `MustHaveLengthIn` for `ImmutableArray`. It should be placed in the class `Check` which is marked as `partial`. +- [ ] Each assertion in Light.GuardClauses has two overloads - the first one takes the optional `parameterName` and `message` arguments and throw the default exception. The actual exception is thrown in the `Throw` class - use the existing `Throw.ValueNotInRange` method which is located in `ExceptionFactory/Throw.Range.cs`. +- [ ] The other overload takes a delegate which allows the caller to provide their own custom exceptions. Use the existing `Throw.CustomException` method and pass the delegate, the erroneous `ImmutableArray` instance and the range. +- [ ] The assertion takes a `Range` parameter to specify the valid length range. +- [ ] Use the `Length` property of `ImmutableArray` instead of `Count` for performance and correctness. +- [ ] Create unit tests for both overloads. The corresponding tests should be placed in Light.GuardClauses.Tests project. There is already a file 'StringAssertions/MustHaveLengthInTests.cs' but you need to create a new file 'CollectionAssertions/MustHaveLengthInTests.cs' for collection-related length tests. Please follow conventions of the existing tests (e.g. use FluentAssertions' `Should()` for assertions). + +## Notes + +- There are already plenty of other assertions and tests in this library. All overloads are placed in the same file in the production code project. The test projects has top-level folders for different groups of assertions, like `CollectionAssertions`, `StringAssertions`, `DateTimeAssertions` and so on. Please take a look at them to follow a similar structure and code style. +- This assertion specifically targets `ImmutableArray` to avoid boxing that would occur with generic `IEnumerable` extensions. +- Use the `Length` property instead of `Count` as this is the appropriate property for `ImmutableArray`. +- The assertion should verify that the `ImmutableArray` length falls within the specified `Range`. +- If you have any questions or suggestions, please ask me about them. From ba8d727f7c692c26ae39953e4960ccf3094eb1a3 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Sun, 20 Jul 2025 22:11:53 +0200 Subject: [PATCH 2/2] feat: add MustHaveLengthIn for ImmutableArray Signed-off-by: Kenny Pflug --- .../MustHaveLengthInTests.cs | 71 +++++++++++++++++++ .../Check.MustHaveLengthIn.cs | 48 +++++++++++++ .../Throw.ImmutableArrayLengthNotInRange.cs | 28 ++++++++ 3 files changed, 147 insertions(+) create mode 100644 Code/Light.GuardClauses.Tests/CollectionAssertions/MustHaveLengthInTests.cs create mode 100644 Code/Light.GuardClauses/ExceptionFactory/Throw.ImmutableArrayLengthNotInRange.cs diff --git a/Code/Light.GuardClauses.Tests/CollectionAssertions/MustHaveLengthInTests.cs b/Code/Light.GuardClauses.Tests/CollectionAssertions/MustHaveLengthInTests.cs new file mode 100644 index 0000000..2e2a984 --- /dev/null +++ b/Code/Light.GuardClauses.Tests/CollectionAssertions/MustHaveLengthInTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Immutable; +using FluentAssertions; +using Xunit; + +namespace Light.GuardClauses.Tests.CollectionAssertions; + +public static class MustHaveLengthInTests +{ + [Theory] + [MemberData(nameof(LengthInRangeData))] + public static void LengthInRange(ImmutableArray array, Range range) => + array.MustHaveLengthIn(range).Should().Equal(array); + + public static readonly TheoryData, Range> LengthInRangeData = + new () + { + { [1, 2, 3], Range.FromInclusive(0).ToExclusive(10) }, + { [1, 2, 3, 4, 5], Range.FromInclusive(3).ToInclusive(5) }, + { ImmutableArray.Empty, Range.FromInclusive(0).ToExclusive(100) }, + { [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], Range.FromExclusive(5).ToInclusive(10) }, + }; + + [Theory] + [MemberData(nameof(LengthNotInRangeData))] + public static void LengthNotInRange(ImmutableArray array, Range range) + { + var act = () => array.MustHaveLengthIn(range, nameof(array)); + + act.Should().Throw() + .And.Message.Should().Contain($"must have its length in between {range.CreateRangeDescriptionText("and")}") + .And.Contain($"but it actually has length {array.Length}"); + } + + public static readonly TheoryData, Range> LengthNotInRangeData = + new () + { + { [1, 2, 3], Range.FromInclusive(10).ToInclusive(20) }, + { [1, 2, 3, 4], Range.FromExclusive(4).ToExclusive(10) }, + { ImmutableArray.Empty, Range.FromInclusive(1).ToExclusive(50) }, + { [1, 2], Range.FromInclusive(100).ToExclusive(256) }, + }; + + [Fact] + public static void CustomException() => + Test.CustomException( + ImmutableArray.Create(1, 2, 3), + Range.FromInclusive(5).ToInclusive(10), + (array, r, exceptionFactory) => array.MustHaveLengthIn(r, exceptionFactory) + ); + + [Fact] + public static void CustomMessage() => + Test.CustomMessage( + message => ImmutableArray.Create(1, 2, 3).MustHaveLengthIn( + Range.FromInclusive(42).ToInclusive(50), + message: message + ) + ); + + [Fact] + public static void CallerArgumentExpression() + { + var testArray = ImmutableArray.Create(1, 2, 3, 4, 5); + + var act = () => testArray.MustHaveLengthIn(Range.FromInclusive(10).ToExclusive(20)); + + act.Should().Throw() + .WithParameterName(nameof(testArray)); + } +} diff --git a/Code/Light.GuardClauses/Check.MustHaveLengthIn.cs b/Code/Light.GuardClauses/Check.MustHaveLengthIn.cs index c190c98..5974e71 100644 --- a/Code/Light.GuardClauses/Check.MustHaveLengthIn.cs +++ b/Code/Light.GuardClauses/Check.MustHaveLengthIn.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Immutable; using System.Runtime.CompilerServices; using JetBrains.Annotations; using Light.GuardClauses.ExceptionFactory; @@ -57,4 +58,51 @@ public static string MustHaveLengthIn( return parameter; } + + /// + /// Ensures that the 's length is within the specified range, or otherwise throws an . + /// + /// The to be checked. + /// The range where the 's length must be in-between. + /// The name of the parameter (optional). + /// The message that will be passed to the resulting exception (optional). + /// Thrown when the length of is not within the specified . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ImmutableArray MustHaveLengthIn( + this ImmutableArray parameter, + Range range, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) + { + if (!range.IsValueWithinRange(parameter.Length)) + { + Throw.ImmutableArrayLengthNotInRange(parameter, range, parameterName, message); + } + + return parameter; + } + + /// + /// Ensures that the 's length is within the specified range, or otherwise throws your custom exception. + /// + /// The to be checked. + /// The range where the 's length must be in-between. + /// The delegate that creates your custom exception. and are passed to this delegate. + /// Your custom exception thrown when the length of is not within the specified range. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ContractAnnotation("exceptionFactory:null => halt")] + public static ImmutableArray MustHaveLengthIn( + this ImmutableArray parameter, + Range range, + Func, Range, Exception> exceptionFactory + ) + { + if (!range.IsValueWithinRange(parameter.Length)) + { + Throw.CustomException(exceptionFactory, parameter, range); + } + + return parameter; + } } diff --git a/Code/Light.GuardClauses/ExceptionFactory/Throw.ImmutableArrayLengthNotInRange.cs b/Code/Light.GuardClauses/ExceptionFactory/Throw.ImmutableArrayLengthNotInRange.cs new file mode 100644 index 0000000..2bcf749 --- /dev/null +++ b/Code/Light.GuardClauses/ExceptionFactory/Throw.ImmutableArrayLengthNotInRange.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace Light.GuardClauses.ExceptionFactory; + +public static partial class Throw +{ + /// + /// Throws the default indicating that an 's length is not within the + /// given range, using the optional parameter name and message. + /// + [ContractAnnotation("=> halt")] + [DoesNotReturn] + public static void ImmutableArrayLengthNotInRange( + ImmutableArray parameter, + Range range, + [CallerArgumentExpression("parameter")] string? parameterName = null, + string? message = null + ) => + throw new ArgumentOutOfRangeException( + parameterName, + message ?? + $"{parameterName ?? "The immutable array"} must have its length in between {range.CreateRangeDescriptionText("and")}, but it actually has length {parameter.Length}." + ); +}