Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 264 additions & 0 deletions Light.GuardClauses.SingleFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4214,6 +4214,182 @@ public static TItem MustBeOneOf<TItem, TCollection>(this TItem parameter, [NotNu
return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws an <see cref = "ArgumentOutOfRangeException"/>.
/// </summary>
/// <param name = "parameter">The value to be checked.</param>
/// <param name = "parameterName">The name of the parameter (optional).</param>
/// <param name = "message">The message that will be passed to the resulting exception (optional).</param>
/// <exception cref = "ArgumentOutOfRangeException">
/// Thrown when <paramref name = "parameter"/> is zero or negative.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static sbyte MustBePositive(this sbyte parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null)
{
if (!(parameter > (sbyte)0))
{
Throw.MustBePositive(parameter, parameterName, message);
}

return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws your custom exception.
/// </summary>
/// <param name = "parameter">The value to be checked.</param>
/// <param name = "exceptionFactory">
/// The delegate that creates your custom exception. <paramref name = "parameter"/> is passed to this delegate.
/// </param>
/// <exception cref = "Exception">
/// Your custom exception thrown when <paramref name = "parameter"/> is zero or negative.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ContractAnnotation("exceptionFactory:null => halt")]
public static sbyte MustBePositive(this sbyte parameter, Func<sbyte, Exception> exceptionFactory)
{
if (!(parameter > (sbyte)0))
{
Throw.CustomException(exceptionFactory, parameter);
}

return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws an <see cref = "ArgumentOutOfRangeException"/>.
/// </summary>
/// <param name = "parameter">The value to be checked.</param>
/// <param name = "parameterName">The name of the parameter (optional).</param>
/// <param name = "message">The message that will be passed to the resulting exception (optional).</param>
/// <exception cref = "ArgumentOutOfRangeException">
/// Thrown when <paramref name = "parameter"/> is zero.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte MustBePositive(this byte parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null)
{
if (!(parameter > (byte)0))
{
Throw.MustBePositive(parameter, parameterName, message);
}

return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws your custom exception.
/// </summary>
/// <param name = "parameter">The value to be checked.</param>
/// <param name = "exceptionFactory">
/// The delegate that creates your custom exception. <paramref name = "parameter"/> is passed to this delegate.
/// </param>
/// <exception cref = "Exception">
/// Your custom exception thrown when <paramref name = "parameter"/> is zero.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ContractAnnotation("exceptionFactory:null => halt")]
public static byte MustBePositive(this byte parameter, Func<byte, Exception> exceptionFactory)
{
if (!(parameter > (byte)0))
{
Throw.CustomException(exceptionFactory, parameter);
}

return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws an <see cref = "ArgumentOutOfRangeException"/>.
/// </summary>
/// <param name = "parameter">The value to be checked.</param>
/// <param name = "parameterName">The name of the parameter (optional).</param>
/// <param name = "message">The message that will be passed to the resulting exception (optional).</param>
/// <exception cref = "ArgumentOutOfRangeException">
/// Thrown when <paramref name = "parameter"/> is zero or negative.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static short MustBePositive(this short parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null)
{
if (!(parameter > (short)0))
{
Throw.MustBePositive(parameter, parameterName, message);
}

return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws your custom exception.
/// </summary>
/// <param name = "parameter">The value to be checked.</param>
/// <param name = "exceptionFactory">
/// The delegate that creates your custom exception. <paramref name = "parameter"/> is passed to this delegate.
/// </param>
/// <exception cref = "Exception">
/// Your custom exception thrown when <paramref name = "parameter"/> is zero or negative.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ContractAnnotation("exceptionFactory:null => halt")]
public static short MustBePositive(this short parameter, Func<short, Exception> exceptionFactory)
{
if (!(parameter > (short)0))
{
Throw.CustomException(exceptionFactory, parameter);
}

return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws an <see cref = "ArgumentOutOfRangeException"/>.
/// </summary>
/// <param name = "parameter">The value to be checked.</param>
/// <param name = "parameterName">The name of the parameter (optional).</param>
/// <param name = "message">The message that will be passed to the resulting exception (optional).</param>
/// <exception cref = "ArgumentOutOfRangeException">
/// Thrown when <paramref name = "parameter"/> is zero.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort MustBePositive(this ushort parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null)
{
if (!(parameter > (ushort)0))
{
Throw.MustBePositive(parameter, parameterName, message);
}

return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws your custom exception.
/// </summary>
/// <param name = "parameter">The value to be checked.</param>
/// <param name = "exceptionFactory">
/// The delegate that creates your custom exception. <paramref name = "parameter"/> is passed to this delegate.
/// </param>
/// <exception cref = "Exception">
/// Your custom exception thrown when <paramref name = "parameter"/> is zero.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ContractAnnotation("exceptionFactory:null => halt")]
public static ushort MustBePositive(this ushort parameter, Func<ushort, Exception> exceptionFactory)
{
if (!(parameter > (ushort)0))
{
Throw.CustomException(exceptionFactory, parameter);
}

return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws an <see cref = "ArgumentOutOfRangeException"/>.
Expand Down Expand Up @@ -4258,6 +4434,50 @@ public static int MustBePositive(this int parameter, Func<int, Exception> except
return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws an <see cref = "ArgumentOutOfRangeException"/>.
/// </summary>
/// <param name = "parameter">The value to be checked.</param>
/// <param name = "parameterName">The name of the parameter (optional).</param>
/// <param name = "message">The message that will be passed to the resulting exception (optional).</param>
/// <exception cref = "ArgumentOutOfRangeException">
/// Thrown when <paramref name = "parameter"/> is zero.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint MustBePositive(this uint parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null)
{
if (!(parameter > 0U))
{
Throw.MustBePositive(parameter, parameterName, message);
}

return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws your custom exception.
/// </summary>
/// <param name = "parameter">The value to be checked.</param>
/// <param name = "exceptionFactory">
/// The delegate that creates your custom exception. <paramref name = "parameter"/> is passed to this delegate.
/// </param>
/// <exception cref = "Exception">
/// Your custom exception thrown when <paramref name = "parameter"/> is zero.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ContractAnnotation("exceptionFactory:null => halt")]
public static uint MustBePositive(this uint parameter, Func<uint, Exception> exceptionFactory)
{
if (!(parameter > 0U))
{
Throw.CustomException(exceptionFactory, parameter);
}

return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws an <see cref = "ArgumentOutOfRangeException"/>.
Expand Down Expand Up @@ -4302,6 +4522,50 @@ public static long MustBePositive(this long parameter, Func<long, Exception> exc
return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws an <see cref = "ArgumentOutOfRangeException"/>.
/// </summary>
/// <param name = "parameter">The value to be checked.</param>
/// <param name = "parameterName">The name of the parameter (optional).</param>
/// <param name = "message">The message that will be passed to the resulting exception (optional).</param>
/// <exception cref = "ArgumentOutOfRangeException">
/// Thrown when <paramref name = "parameter"/> is zero.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong MustBePositive(this ulong parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null)
{
if (!(parameter > 0UL))
{
Throw.MustBePositive(parameter, parameterName, message);
}

return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws your custom exception.
/// </summary>
/// <param name = "parameter">The value to be checked.</param>
/// <param name = "exceptionFactory">
/// The delegate that creates your custom exception. <paramref name = "parameter"/> is passed to this delegate.
/// </param>
/// <exception cref = "Exception">
/// Your custom exception thrown when <paramref name = "parameter"/> is zero.
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ContractAnnotation("exceptionFactory:null => halt")]
public static ulong MustBePositive(this ulong parameter, Func<ulong, Exception> exceptionFactory)
{
if (!(parameter > 0UL))
{
Throw.CustomException(exceptionFactory, parameter);
}

return parameter;
}

/// <summary>
/// Ensures that the specified <paramref name = "parameter"/> is positive (greater than zero), or otherwise
/// throws an <see cref = "ArgumentOutOfRangeException"/>.
Expand Down
38 changes: 38 additions & 0 deletions ai-plans/0168-must-be-positive-older-frameworks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# MustBePositive Parity for Older Target Frameworks

## Rationale

Parent issue #162 (point 5) identifies a target-framework gap in `MustBePositive`: the .NET 10 generic `INumber<T>` overload supports integral types such as `byte` and `ushort`, but the .NET Standard targets and their single-file source export expose only the existing concrete overloads. Consequently, BrilliantMessaging must express seven positive `byte` or `ushort` guards as the less-specific `MustBeGreaterThan(0)`.

Add concrete overloads for the complete missing signed and unsigned integral set so `MustBePositive` has a symmetric API on every supported target framework without changing the modern generic overloads.

## Acceptance Criteria

- [x] `MustBePositive` has concrete overloads for `sbyte`, `byte`, `short`, `ushort`, `uint`, and `ulong` on .NET Standard 2.0, .NET Standard 2.1, and .NET 10; every overload returns the original value when it is greater than zero and throws `ArgumentOutOfRangeException` for zero and, for signed types, negative values.
- [x] Each new integral type has a custom-exception-factory overload accepting `Func<T, Exception>`; it passes the original value to the factory, invokes the factory only when validation fails, and a null factory on a failing value throws `ArgumentNullException` via the existing `Throw.CustomException` convention.
- [x] Default failures preserve the existing `MustBePositive` exception contract, including caller-argument-expression parameter names, optional custom messages, actual values, and the standard generated message.
- [x] Automated tests cover positive boundary values, zero for every new type, negative boundary values for the signed types, return values, parameter-name and custom-message propagation, custom factory values and exceptions, factories not invoked on success, and null-factory behavior.
- [x] The existing `MustBePositive` source-export whitelist entry includes all new concrete overloads in portable and modern exports, preserves the generic `INumber<T>` overloads only in the modern export, and continues to trim every custom-exception-factory overload when that option is disabled.
- [x] The committed .NET Standard 2.0 single-file distribution is regenerated with the new overloads and validates for both supported source-export targets.
- [x] The comparable/range documentation no longer describes the new concrete integral types as modern-target-only, and the package release notes mention the expanded `MustBePositive` support.
- [x] The complete solution restores and builds without warnings in Release configuration, and all automated tests pass on the pinned SDK.

## Technical Details

Add the twelve overloads to the existing `Check.MustBePositive.cs` outside the `NET8_0_OR_GREATER` region, following the exact structure, annotations, XML documentation, and comparison semantics of the `int` and `long` overload pairs. For each `T` in `sbyte`, `byte`, `short`, `ushort`, `uint`, and `ulong`, the public shape is:

```csharp
public static T MustBePositive(
this T parameter,
[CallerArgumentExpression("parameter")] string? parameterName = null,
string? message = null
);

public static T MustBePositive(this T parameter, Func<T, Exception> exceptionFactory);
```

Compare each value directly with its type-appropriate zero. Do not widen values to `int` or `long`: the returned value, exception `ActualValue`, and custom-factory argument must retain the receiver's exact type. The existing generic `Throw.MustBePositive<T>` and `Throw.CustomException<T>` helpers already support all six types, so no new exception or throw-helper API is needed.

Keep the `INumber<T>` overloads under `NET8_0_OR_GREATER`. On modern targets, normal calls with the six integral types resolve to the new concrete overloads while explicitly generic calls remain available; both paths must have the same greater-than-zero semantics. This issue does not expand the other sign-guard families or add native-integer overloads.

Extend `MustBePositiveTests` and `NumericCustomFactorySuccessTests` with the concrete integral matrix. Strengthen `SourceFileMergerWhitelistTests.SignGuardWhitelistsUseTargetSpecificSurface` to assert representative signed and unsigned overloads, exact factory signatures, portable omission of `INumber<T>`, modern retention of `INumber<T>`, and factory trimming. Regenerate `tools/source-export/Light.GuardClauses.SourceCodeTransformation/Light.GuardClauses.SingleFile.cs` using the committed source-export settings.
Loading
Loading