Skip to content

Commit fad807c

Browse files
More extension methods organised into extension blocks. Preconditions updated to support CallerArgumentException.
1 parent f0de0d5 commit fad807c

File tree

7 files changed

+853
-829
lines changed

7 files changed

+853
-829
lines changed

OnixLabs.Core.UnitTests/PreconditionTests.cs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ public void RequireIsFailureShouldThrowAnInvalidOperationExceptionWhenTheResultI
332332
Exception exception = Assert.Throws<ArgumentException>(() => RequireIsFailure(result));
333333

334334
// Then
335-
Assert.Equal("Argument must be a Failure state.", exception.Message);
335+
Assert.Equal("Argument must be a Failure state. (Parameter 'result')", exception.Message);
336336
}
337337

338338
[Fact(DisplayName = "RequireIsFailure should return a Failure when the result is a failure state")]
@@ -358,7 +358,7 @@ public void RequireIsSuccessShouldThrowAnInvalidOperationExceptionWhenTheResultI
358358
Exception exception = Assert.Throws<ArgumentException>(() => RequireIsSuccess(result));
359359

360360
// Then
361-
Assert.Equal("Argument must be a Success state.", exception.Message);
361+
Assert.Equal("Argument must be a Success state. (Parameter 'result')", exception.Message);
362362
}
363363

364364
[Fact(DisplayName = "RequireIsSuccess should return a Success when the result is a success state")]
@@ -384,7 +384,7 @@ public void RequireIsFailureTShouldThrowAnInvalidOperationExceptionWhenTheResult
384384
Exception exception = Assert.Throws<ArgumentException>(() => RequireIsFailure(result));
385385

386386
// Then
387-
Assert.Equal("Argument must be a Failure state.", exception.Message);
387+
Assert.Equal("Argument must be a Failure state. (Parameter 'result')", exception.Message);
388388
}
389389

390390
[Fact(DisplayName = "RequireIsFailure<T> should return a Failure when the result is a failure state")]
@@ -410,7 +410,7 @@ public void RequireIsSuccessTShouldThrowAnInvalidOperationExceptionWhenTheResult
410410
Exception exception = Assert.Throws<ArgumentException>(() => RequireIsSuccess(result));
411411

412412
// Then
413-
Assert.Equal("Argument must be a Success state.", exception.Message);
413+
Assert.Equal("Argument must be a Success state. (Parameter 'result')", exception.Message);
414414
}
415415

416416
[Fact(DisplayName = "RequireIsSuccess<T> should return a Success when the result is a success state")]
@@ -436,7 +436,7 @@ public void RequireIsNoneShouldThrowAnInvalidOperationExceptionWhenTheOptionalIs
436436
Exception exception = Assert.Throws<ArgumentException>(() => RequireIsNone(optional));
437437

438438
// Then
439-
Assert.Equal("Argument must be a None<T> value.", exception.Message);
439+
Assert.Equal("Argument must be a None<T> value. (Parameter 'optional')", exception.Message);
440440
}
441441

442442
[Fact(DisplayName = "RequireIsNone<T> should return a None<T> when the optional is a None<T> value")]
@@ -462,7 +462,7 @@ public void RequireIsSomeShouldThrowAnInvalidOperationExceptionWhenTheOptionalIs
462462
Exception exception = Assert.Throws<ArgumentException>(() => RequireIsSome(optional));
463463

464464
// Then
465-
Assert.Equal("Argument must be a Some<T> value.", exception.Message);
465+
Assert.Equal("Argument must be a Some<T> value. (Parameter 'optional')", exception.Message);
466466
}
467467

468468
[Fact(DisplayName = "RequireIsSome<T> should return a Some<T> when the optional is a Some<T> value")]
@@ -485,7 +485,7 @@ public void RequireWithinRangeInclusiveShouldThrowArgumentOutOfRangeExceptionWhe
485485
Exception exception = Assert.Throws<ArgumentOutOfRangeException>(() => RequireWithinRangeInclusive(1, 2, 3));
486486

487487
// Then
488-
Assert.Equal("Argument must be within range.", exception.Message);
488+
Assert.Equal("Argument must be within range. (Parameter '1')", exception.Message);
489489
}
490490

491491
[Fact(DisplayName = "RequireWithinRangeInclusive should throw an ArgumentOutOfRangeException when the value falls above the specified range")]
@@ -495,7 +495,7 @@ public void RequireWithinRangeInclusiveShouldThrowArgumentOutOfRangeExceptionWhe
495495
Exception exception = Assert.Throws<ArgumentOutOfRangeException>(() => RequireWithinRangeInclusive(4, 2, 3));
496496

497497
// Then
498-
Assert.Equal("Argument must be within range.", exception.Message);
498+
Assert.Equal("Argument must be within range. (Parameter '4')", exception.Message);
499499
}
500500

501501
[Fact(DisplayName = "RequireWithinRangeInclusive should not throw an ArgumentOutOfRangeException when the value is exactly the minimum value")]
@@ -544,7 +544,7 @@ public void RequireWithinRangeExclusiveShouldThrowArgumentOutOfRangeExceptionWhe
544544
Exception exception = Assert.Throws<ArgumentOutOfRangeException>(() => RequireWithinRangeExclusive(2, 2, 4));
545545

546546
// Then
547-
Assert.Equal("Argument must be within range.", exception.Message);
547+
Assert.Equal("Argument must be within range. (Parameter '2')", exception.Message);
548548
}
549549

550550
[Fact(DisplayName = "RequireWithinRangeExclusive should throw an ArgumentOutOfRangeException when the value falls above the specified range")]
@@ -554,7 +554,7 @@ public void RequireWithinRangeExclusiveShouldThrowArgumentOutOfRangeExceptionWhe
554554
Exception exception = Assert.Throws<ArgumentOutOfRangeException>(() => RequireWithinRangeExclusive(4, 2, 4));
555555

556556
// Then
557-
Assert.Equal("Argument must be within range.", exception.Message);
557+
Assert.Equal("Argument must be within range. (Parameter '4')", exception.Message);
558558
}
559559

560560
[Fact(DisplayName = "RequireWithinRangeExclusive should not throw an ArgumentOutOfRangeException when the value falls between the specified range")]
@@ -577,7 +577,7 @@ public void RequireNotNullShouldThrowArgumentNullExceptionWhenConditionIsNull()
577577
Exception exception = Assert.Throws<ArgumentNullException>(() => RequireNotNull<object>(null));
578578

579579
// Then
580-
Assert.Equal("Argument must not be null.", exception.Message);
580+
Assert.Equal("Argument must not be null. (Parameter 'null')", exception.Message);
581581
}
582582

583583
[Fact(DisplayName = "RequireNotNull should not throw an InvalidOperationException when the condition is not null")]
@@ -599,7 +599,7 @@ public void RequireNotNullOfValueTypeShouldThrowInvalidOperationExceptionWhenCon
599599

600600
// Then
601601
Assert.Equal(0, actual);
602-
Assert.Equal("Argument must not be null.", exception.Message);
602+
Assert.Equal("Argument must not be null. (Parameter 'expected')", exception.Message);
603603
}
604604

605605
[Fact(DisplayName = "RequireNotNull of ValueType should not throw an InvalidOperationException when the condition is not null")]
@@ -622,7 +622,7 @@ public void RequireNotNullOrEmptyShouldThrowArgumentExceptionWhenValueIsNull()
622622
Exception exception = Assert.Throws<ArgumentException>(() => RequireNotNullOrEmpty(null));
623623

624624
// Then
625-
Assert.Equal("Argument must not be null or empty.", exception.Message);
625+
Assert.Equal("Argument must not be null or empty. (Parameter 'null')", exception.Message);
626626
}
627627

628628
[Fact(DisplayName = "RequireNotNullOrEmpty should throw an ArgumentException when the value is empty")]
@@ -632,7 +632,7 @@ public void RequireNotNullOrEmptyShouldThrowArgumentExceptionWhenValueIsEmpty()
632632
Exception exception = Assert.Throws<ArgumentException>(() => RequireNotNullOrEmpty(string.Empty));
633633

634634
// Then
635-
Assert.Equal("Argument must not be null or empty.", exception.Message);
635+
Assert.Equal("Argument must not be null or empty. (Parameter 'string.Empty')", exception.Message);
636636
}
637637

638638
[Fact(DisplayName = "RequireNotNullOrEmpty should return the argument value when the value is not null and not empty")]
@@ -655,7 +655,7 @@ public void RequireNotNullOrWhiteSpaceShouldThrowArgumentExceptionWhenValueIsNul
655655
Exception exception = Assert.Throws<ArgumentException>(() => RequireNotNullOrWhiteSpace(null));
656656

657657
// Then
658-
Assert.Equal("Argument must not be null or whitespace.", exception.Message);
658+
Assert.Equal("Argument must not be null or whitespace. (Parameter 'null')", exception.Message);
659659
}
660660

661661
[Fact(DisplayName = "RequireNotNullOrWhiteSpace should throw an ArgumentException when the value is whitespace")]
@@ -665,7 +665,7 @@ public void RequireNotNullOrWhiteSpaceShouldThrowArgumentExceptionWhenValueIsWhi
665665
Exception exception = Assert.Throws<ArgumentException>(() => RequireNotNullOrWhiteSpace(" "));
666666

667667
// Then
668-
Assert.Equal("Argument must not be null or whitespace.", exception.Message);
668+
Assert.Equal("Argument must not be null or whitespace. (Parameter '\" \"')", exception.Message);
669669
}
670670

671671
[Fact(DisplayName = "RequireNotNullOrWhiteSpace should return the argument value when the value is not null and not whitespace")]
@@ -688,7 +688,7 @@ public void RequireIsDefinedShouldThrowArgumentOutOfRangeExceptionWhenSpecifiedE
688688
Exception exception = Assert.Throws<ArgumentOutOfRangeException>(() => RequireIsDefined((Shape)2));
689689

690690
// Then
691-
Assert.Equal("Invalid Shape enum value: 2. Valid values include: Square, Circle.", exception.Message);
691+
Assert.Equal("Invalid Shape enum value: 2. Valid values include: Square, Circle. (Parameter '(Shape)2')", exception.Message);
692692
}
693693

694694
[Fact(DisplayName = "RequireIsDefined should not throw an ArgumentOutOfRangeException when the specified enum value is defined")]

OnixLabs.Core/Collections/Generic/Extensions.IEqualityComparer.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,23 @@
1818
namespace OnixLabs.Core.Collections.Generic;
1919

2020
/// <summary>
21-
/// Provides LINQ-like extension methods for <see cref="IEqualityComparer{T}"/>.
21+
/// Provides extension methods for <see cref="IEqualityComparer{T}"/> instances.
2222
/// </summary>
2323
// ReSharper disable InconsistentNaming
2424
[EditorBrowsable(EditorBrowsableState.Never)]
2525
public static class IEqualityComparerExtensions
2626
{
2727
/// <summary>
28-
/// Gets the current <see cref="IEqualityComparer{T}"/>, or the default comparer if the current comparer is <see langword="null"/>.
28+
/// Provides extension methods for <see cref="IEqualityComparer{T}"/> instances.
2929
/// </summary>
30-
/// <param name="comparer">The current <see cref="IEqualityComparer{T}"/>.</param>
31-
/// <typeparam name="T">The underlying type of the current <see cref="IEqualityComparer{T}"/>.</typeparam>
32-
/// <returns>Returns the current <see cref="IEqualityComparer{T}"/>, or the default comparer if the current comparer is <see langword="null"/>.</returns>
33-
public static IEqualityComparer<T> GetOrDefault<T>(this IEqualityComparer<T>? comparer) => comparer ?? EqualityComparer<T>.Default;
30+
/// <param name="receiver">The current <see cref="IEqualityComparer{T}"/> instance.</param>
31+
/// <typeparam name="T">The underlying type of the current <see cref="IEqualityComparer{T}"/> instance.</typeparam>
32+
extension<T>(IEqualityComparer<T>? receiver)
33+
{
34+
/// <summary>
35+
/// Gets the current <see cref="IEqualityComparer{T}"/>, or the default comparer if the current comparer is <see langword="null"/>.
36+
/// </summary>
37+
/// <returns>Returns the current <see cref="IEqualityComparer{T}"/>, or the default comparer if the current comparer is <see langword="null"/>.</returns>
38+
public IEqualityComparer<T> GetOrDefault() => receiver ?? EqualityComparer<T>.Default;
39+
}
3440
}

OnixLabs.Core/Linq/Extensions.Expression.cs

Lines changed: 62 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -20,80 +20,82 @@
2020
namespace OnixLabs.Core.Linq;
2121

2222
/// <summary>
23-
/// Provides extension methods for <see cref="Expression{TDelegate}"/>.
23+
/// Provides extension methods for <see cref="Expression{TDelegate}"/> instances.
2424
/// </summary>
2525
[EditorBrowsable(EditorBrowsableState.Never)]
2626
public static class ExpressionExtensions
2727
{
2828
private const string ParameterName = "$param";
2929

3030
/// <summary>
31-
/// Combines two <see cref="Expression{TDelegate}"/> instances into a single expression using the logical AND operator.
31+
/// Provides extension methods for <see cref="Expression{TDelegate}"/> instances.
3232
/// </summary>
33-
/// <param name="left">The left-hand expression to combine.</param>
34-
/// <param name="right">The right-hand expression to combine.</param>
35-
/// <typeparam name="T">The underlying type of the expression.</typeparam>
36-
/// <returns>
37-
/// Returns a new <see cref="Expression{TDelegate}"/> that combines two <see cref="Expression{TDelegate}"/> instances
38-
/// into a single expression using the logical AND operator.
39-
/// </returns>
40-
/// <remarks>
41-
/// Calling this method introduces a new expression parameter (named <c>$param</c>) to ensure both expressions share the same parameter.
42-
/// Internal or nested lambda parameters that do not match the replaced parameter remain untouched.
43-
/// </remarks>
44-
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
33+
/// <param name="receiver">The current (left-hand) <see cref="Expression{TDelegate}"/> instance.</param>
34+
/// <typeparam name="T">The underlying type of the current (left-hand) <see cref="Expression{TDelegate}"/> instance.</typeparam>
35+
extension<T>(Expression<Func<T, bool>> receiver)
4536
{
46-
ParameterExpression parameter = Expression.Parameter(typeof(T), ParameterName);
47-
Expression leftBody = new ReplaceParameterVisitor(left.Parameters.First(), parameter).Visit(left.Body);
48-
Expression rightBody = new ReplaceParameterVisitor(right.Parameters.First(), parameter).Visit(right.Body);
49-
BinaryExpression binaryExpression = Expression.AndAlso(leftBody, rightBody);
37+
/// <summary>
38+
/// Combines two <see cref="Expression{TDelegate}"/> instances into a single expression using the logical AND operator.
39+
/// </summary>
40+
/// <param name="other">The other (right-hand) expression to combine.</param>
41+
/// <returns>
42+
/// Returns a new <see cref="Expression{TDelegate}"/> that combines two <see cref="Expression{TDelegate}"/> instances
43+
/// into a single expression using the logical AND operator.
44+
/// </returns>
45+
/// <remarks>
46+
/// Calling this method introduces a new expression parameter (named <c>$param</c>) to ensure both expressions share the same parameter.
47+
/// Internal or nested lambda parameters that do not match the replaced parameter remain untouched.
48+
/// </remarks>
49+
public Expression<Func<T, bool>> And(Expression<Func<T, bool>> other)
50+
{
51+
ParameterExpression parameter = Expression.Parameter(typeof(T), ParameterName);
52+
Expression leftBody = new ReplaceParameterVisitor(receiver.Parameters.First(), parameter).Visit(receiver.Body);
53+
Expression rightBody = new ReplaceParameterVisitor(other.Parameters.First(), parameter).Visit(other.Body);
54+
BinaryExpression binaryExpression = Expression.AndAlso(leftBody, rightBody);
5055

51-
return Expression.Lambda<Func<T, bool>>(binaryExpression, parameter);
52-
}
56+
return Expression.Lambda<Func<T, bool>>(binaryExpression, parameter);
57+
}
5358

54-
/// <summary>
55-
/// Combines two <see cref="Expression{TDelegate}"/> instances into a single expression using the logical OR operator.
56-
/// </summary>
57-
/// <param name="left">The left-hand expression to combine.</param>
58-
/// <param name="right">The right-hand expression to combine.</param>
59-
/// <typeparam name="T">The underlying type of the expression.</typeparam>
60-
/// <returns>
61-
/// Returns a new <see cref="Expression{TDelegate}"/> that combines two <see cref="Expression{TDelegate}"/> instances
62-
/// into a single expression using the logical OR operator.
63-
/// </returns>
64-
/// <remarks>
65-
/// Calling this method introduces a new expression parameter (named <c>$param</c>) to ensure both expressions share the same parameter.
66-
/// Internal or nested lambda parameters that do not match the replaced parameter remain untouched.
67-
/// </remarks>
68-
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
69-
{
70-
ParameterExpression parameter = Expression.Parameter(typeof(T), ParameterName);
71-
Expression leftBody = new ReplaceParameterVisitor(left.Parameters.First(), parameter).Visit(left.Body);
72-
Expression rightBody = new ReplaceParameterVisitor(right.Parameters.First(), parameter).Visit(right.Body);
73-
BinaryExpression binaryExpression = Expression.OrElse(leftBody, rightBody);
59+
/// <summary>
60+
/// Combines two <see cref="Expression{TDelegate}"/> instances into a single expression using the logical OR operator.
61+
/// </summary>
62+
/// <param name="other">The other (right-hand) expression to combine.</param>
63+
/// <returns>
64+
/// Returns a new <see cref="Expression{TDelegate}"/> that combines two <see cref="Expression{TDelegate}"/> instances
65+
/// into a single expression using the logical OR operator.
66+
/// </returns>
67+
/// <remarks>
68+
/// Calling this method introduces a new expression parameter (named <c>$param</c>) to ensure both expressions share the same parameter.
69+
/// Internal or nested lambda parameters that do not match the replaced parameter remain untouched.
70+
/// </remarks>
71+
public Expression<Func<T, bool>> Or(Expression<Func<T, bool>> other)
72+
{
73+
ParameterExpression parameter = Expression.Parameter(typeof(T), ParameterName);
74+
Expression leftBody = new ReplaceParameterVisitor(receiver.Parameters.First(), parameter).Visit(receiver.Body);
75+
Expression rightBody = new ReplaceParameterVisitor(other.Parameters.First(), parameter).Visit(other.Body);
76+
BinaryExpression binaryExpression = Expression.OrElse(leftBody, rightBody);
7477

75-
return Expression.Lambda<Func<T, bool>>(binaryExpression, parameter);
76-
}
78+
return Expression.Lambda<Func<T, bool>>(binaryExpression, parameter);
79+
}
7780

78-
/// <summary>
79-
/// Negates the current <see cref="Expression{TDelegate}"/> instance using the logical NOT operator.
80-
/// </summary>
81-
/// <param name="expression">The expression to negate.</param>
82-
/// <typeparam name="T">The underlying type of the expression.</typeparam>
83-
/// <returns>
84-
/// Returns a new <see cref="Expression{TDelegate}"/> that negates the current <see cref="Expression{TDelegate}"/> instance using the logical NOT operator.
85-
/// </returns>
86-
/// <remarks>
87-
/// Calling this method introduces a new expression parameter (named <c>$param</c>) to ensure a uniform parameter expression.
88-
/// Internal or nested lambda parameters that do not match the replaced parameter remain untouched.
89-
/// </remarks>
90-
public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expression)
91-
{
92-
ParameterExpression parameter = Expression.Parameter(typeof(T), ParameterName);
93-
Expression expressionBody = new ReplaceParameterVisitor(expression.Parameters.First(), parameter).Visit(expression.Body);
81+
/// <summary>
82+
/// Negates the current <see cref="Expression{TDelegate}"/> instance using the logical NOT operator.
83+
/// </summary>
84+
/// <returns>
85+
/// Returns a new <see cref="Expression{TDelegate}"/> that negates the current <see cref="Expression{TDelegate}"/> instance using the logical NOT operator.
86+
/// </returns>
87+
/// <remarks>
88+
/// Calling this method introduces a new expression parameter (named <c>$param</c>) to ensure a uniform parameter expression.
89+
/// Internal or nested lambda parameters that do not match the replaced parameter remain untouched.
90+
/// </remarks>
91+
public Expression<Func<T, bool>> Not()
92+
{
93+
ParameterExpression parameter = Expression.Parameter(typeof(T), ParameterName);
94+
Expression expressionBody = new ReplaceParameterVisitor(receiver.Parameters.First(), parameter).Visit(receiver.Body);
9495

95-
UnaryExpression unaryExpression = Expression.Not(expressionBody);
96-
return Expression.Lambda<Func<T, bool>>(unaryExpression, parameter);
96+
UnaryExpression unaryExpression = Expression.Not(expressionBody);
97+
return Expression.Lambda<Func<T, bool>>(unaryExpression, parameter);
98+
}
9799
}
98100

99101
/// <summary>

0 commit comments

Comments
 (0)