request/src/request.validation/RuleBuilderExtensions.cs
Louis Seubert fd6ed64755
fix(validation): guard null comparison bounds in comparison builders
Passing a null bound to GreaterThan/LessThan/Between etc. called
value.CompareTo(null), throwing NullReferenceException for non-null
reference-type values. Short-circuit when the bound is null so the rule is
satisfied instead of crashing.
2026-07-12 21:53:26 +02:00

336 lines
15 KiB
C#

// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace Geekeey.Request.Validation;
/// <summary>
/// Provides built-in validators for common validation scenarios.
/// </summary>
public static class RuleBuilderExtensions
{
private static bool IsNull<TProperty>([NotNullWhen(false)] TProperty? value)
{
object? boxed = value;
return boxed is null;
}
/// <summary>
/// Adds a rule to ensure that the property value is not null for reference types.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <typeparam name="TProperty">The type of the property being validated. Must be a nullable reference type.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <returns>The updated rule builder with the not-null condition added.</returns>
public static IPropertyRuleBuilder<T, TProperty> NotNull<T, TProperty>(
this IPropertyRuleBuilder<T, TProperty> rule)
where TProperty : class?
{
ArgumentNullException.ThrowIfNull(rule);
return rule.Must(static value => value is not null, "Value is required.");
}
/// <summary>
/// Adds a rule to ensure that the property value is not null for nullable value types.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <typeparam name="TProperty">The underlying non-nullable value type of the property being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <returns>The updated rule builder with the not-null condition added.</returns>
public static IPropertyRuleBuilder<T, TProperty?> NotNull<T, TProperty>(
this IPropertyRuleBuilder<T, TProperty?> rule)
where TProperty : struct
{
ArgumentNullException.ThrowIfNull(rule);
return rule.Must(static value => value.HasValue, "Value is required.");
}
/// <summary>
/// Adds a rule to ensure that the string property value is not null, empty, or whitespace.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <returns>The updated rule builder with the not-empty condition added.</returns>
public static IPropertyRuleBuilder<T, string?> NotEmpty<T>(this IPropertyRuleBuilder<T, string?> rule)
{
ArgumentNullException.ThrowIfNull(rule);
return rule.Must(static value => !string.IsNullOrWhiteSpace(value), "Value is required.");
}
/// <summary>
/// Adds a rule to ensure that the collection property value is not null and contains at least one element.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <typeparam name="TElement">The type of the elements in the collection being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <returns>The updated rule builder with the not-empty condition added.</returns>
public static IPropertyRuleBuilder<T, IEnumerable<TElement>?> NotEmpty<T, TElement>(
this IPropertyRuleBuilder<T, IEnumerable<TElement>?> rule)
{
ArgumentNullException.ThrowIfNull(rule);
return rule.Must(static value => value is not null && value.Any(), "Value is required.");
}
/// <summary>
/// Removes the current rule property path from emitted validation problems.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <typeparam name="TProperty">The type of the property being validated.</typeparam>
/// <param name="rule">The rule builder to which the property path transform is applied.</param>
/// <returns>The updated rule builder with the property path removed.</returns>
public static IPropertyRuleBuilder<T, TProperty> WithoutPropertyPath<T, TProperty>(
this IPropertyRuleBuilder<T, TProperty> rule)
{
ArgumentNullException.ThrowIfNull(rule);
return rule.WithPropertyPath(static _ => string.Empty);
}
/// <summary>
/// Adds a rule to ensure that the string property value meets the specified minimum length.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <param name="minLength">The minimum allowed number of characters.</param>
/// <returns>The updated rule builder with the minimum length condition added.</returns>
public static IPropertyRuleBuilder<T, string?> MinLength<T>(
this IPropertyRuleBuilder<T, string?> rule,
int minLength)
{
ArgumentNullException.ThrowIfNull(rule);
ArgumentOutOfRangeException.ThrowIfNegative(minLength);
return rule.Must(value => IsNull(value) || value.Length >= minLength,
$"Value must be at least {minLength} characters long.");
}
/// <summary>
/// Adds a rule to ensure that the string property value does not exceed the specified maximum length.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <param name="maxLength">The maximum allowed number of characters.</param>
/// <returns>The updated rule builder with the maximum length condition added.</returns>
public static IPropertyRuleBuilder<T, string?> MaxLength<T>(
this IPropertyRuleBuilder<T, string?> rule,
int maxLength)
{
ArgumentNullException.ThrowIfNull(rule);
ArgumentOutOfRangeException.ThrowIfNegative(maxLength);
return rule.Must(value => IsNull(value) || value.Length <= maxLength,
$"Value must be at most {maxLength} characters long.");
}
/// <summary>
/// Adds a rule to ensure that the string property value falls within the specified inclusive length range.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <param name="minLength">The minimum allowed number of characters.</param>
/// <param name="maxLength">The maximum allowed number of characters.</param>
/// <returns>The updated rule builder with the length range condition added.</returns>
public static IPropertyRuleBuilder<T, string?> Length<T>(
this IPropertyRuleBuilder<T, string?> rule,
int minLength,
int maxLength)
{
ArgumentNullException.ThrowIfNull(rule);
ArgumentOutOfRangeException.ThrowIfNegative(minLength);
ArgumentOutOfRangeException.ThrowIfNegative(maxLength);
if (maxLength < minLength)
{
throw new ArgumentOutOfRangeException(nameof(maxLength),
"Maximum length must be greater than or equal to minimum length.");
}
return rule.Must(value => IsNull(value) || (value.Length >= minLength && value.Length <= maxLength),
$"Value must be between {minLength} and {maxLength} characters long.");
}
/// <summary>
/// Adds a rule to ensure that the property value is greater than the specified comparison value.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <typeparam name="TProperty">The type of the property being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <param name="comparisonValue">The value that the property must be greater than.</param>
/// <returns>The updated rule builder with the greater-than condition added.</returns>
public static IPropertyRuleBuilder<T, TProperty> GreaterThan<T, TProperty>(
this IPropertyRuleBuilder<T, TProperty> rule,
TProperty comparisonValue)
where TProperty : IComparable<TProperty>?
{
ArgumentNullException.ThrowIfNull(rule);
return rule.Must(value => IsNull(value) || IsNull(comparisonValue) || value.CompareTo(comparisonValue) > 0,
$"Value must be greater than {comparisonValue}.");
}
/// <summary>
/// Adds a rule to ensure that the property value is greater than or equal to the specified comparison value.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <typeparam name="TProperty">The type of the property being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <param name="comparisonValue">The value that the property must be greater than or equal to.</param>
/// <returns>The updated rule builder with the greater-than-or-equal condition added.</returns>
public static IPropertyRuleBuilder<T, TProperty> GreaterThanOrEqualTo<T, TProperty>(
this IPropertyRuleBuilder<T, TProperty> rule,
TProperty comparisonValue)
where TProperty : IComparable<TProperty>?
{
ArgumentNullException.ThrowIfNull(rule);
return rule.Must(value => IsNull(value) || IsNull(comparisonValue) || value.CompareTo(comparisonValue) >= 0,
$"Value must be greater than or equal to {comparisonValue}.");
}
/// <summary>
/// Adds a rule to ensure that the property value is less than the specified comparison value.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <typeparam name="TProperty">The type of the property being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <param name="comparisonValue">The value that the property must be less than.</param>
/// <returns>The updated rule builder with the less-than condition added.</returns>
public static IPropertyRuleBuilder<T, TProperty> LessThan<T, TProperty>(
this IPropertyRuleBuilder<T, TProperty> rule,
TProperty comparisonValue)
where TProperty : IComparable<TProperty>?
{
ArgumentNullException.ThrowIfNull(rule);
return rule.Must(value => IsNull(value) || IsNull(comparisonValue) || value.CompareTo(comparisonValue) < 0,
$"Value must be less than {comparisonValue}.");
}
/// <summary>
/// Adds a rule to ensure that the property value is less than or equal to the specified comparison value.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <typeparam name="TProperty">The type of the property being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <param name="comparisonValue">The value that the property must be less than or equal to.</param>
/// <returns>The updated rule builder with the less-than-or-equal condition added.</returns>
public static IPropertyRuleBuilder<T, TProperty> LessThanOrEqualTo<T, TProperty>(
this IPropertyRuleBuilder<T, TProperty> rule,
TProperty comparisonValue)
where TProperty : IComparable<TProperty>?
{
ArgumentNullException.ThrowIfNull(rule);
return rule.Must(value => IsNull(value) || IsNull(comparisonValue) || value.CompareTo(comparisonValue) <= 0,
$"Value must be less than or equal to {comparisonValue}.");
}
/// <summary>
/// Adds a rule to ensure that the property value falls within the specified inclusive range.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <typeparam name="TProperty">The type of the property being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <param name="minValue">The minimum allowed value.</param>
/// <param name="maxValue">The maximum allowed value.</param>
/// <returns>The updated rule builder with the range condition added.</returns>
public static IPropertyRuleBuilder<T, TProperty> Between<T, TProperty>(
this IPropertyRuleBuilder<T, TProperty> rule,
TProperty minValue,
TProperty maxValue)
where TProperty : IComparable<TProperty>?
{
ArgumentNullException.ThrowIfNull(rule);
if (minValue is not null && maxValue is not null && minValue.CompareTo(maxValue) > 0)
{
throw new ArgumentOutOfRangeException(nameof(maxValue),
"Maximum value must be greater than or equal to minimum value.");
}
return rule.Must(value => IsNull(value)
|| ((minValue is null || value.CompareTo(minValue) >= 0)
&& (maxValue is null || value.CompareTo(maxValue) <= 0)),
$"Value must be between {minValue} and {maxValue}.");
}
/// <summary>
/// Adds a rule to ensure that the property value is equal to the specified comparison value.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <typeparam name="TProperty">The type of the property being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <param name="comparisonValue">The required value for the property.</param>
/// <returns>The updated rule builder with the equality condition added.</returns>
public static IPropertyRuleBuilder<T, TProperty> Equal<T, TProperty>(
this IPropertyRuleBuilder<T, TProperty> rule,
TProperty comparisonValue)
where TProperty : IEquatable<TProperty>?
{
ArgumentNullException.ThrowIfNull(rule);
return rule.Must(value => EqualityComparer<TProperty>.Default.Equals(value, comparisonValue),
$"Value must be equal to {comparisonValue}.");
}
/// <summary>
/// Adds a rule to ensure that the property value is not equal to the specified disallowed value.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <typeparam name="TProperty">The type of the property being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <param name="disallowedValue">The value that the property must not equal.</param>
/// <returns>The updated rule builder with the inequality condition added.</returns>
public static IPropertyRuleBuilder<T, TProperty> NotEqual<T, TProperty>(
this IPropertyRuleBuilder<T, TProperty> rule,
TProperty disallowedValue)
where TProperty : IEquatable<TProperty>?
{
ArgumentNullException.ThrowIfNull(rule);
return rule.Must(value => !EqualityComparer<TProperty>.Default.Equals(value, disallowedValue),
$"Value must not be equal to {disallowedValue}.");
}
/// <summary>
/// Adds a rule to ensure that the string property value matches the specified regular expression pattern.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <param name="pattern">The regular expression pattern that the property value must match.</param>
/// <returns>The updated rule builder with the pattern-matching condition added.</returns>
public static IPropertyRuleBuilder<T, string?> Matches<T>(
this IPropertyRuleBuilder<T, string?> rule,
string pattern)
{
ArgumentNullException.ThrowIfNull(rule);
ArgumentException.ThrowIfNullOrEmpty(pattern);
return rule.Must(value => IsNull(value) || Regex.IsMatch(value, pattern),
"Value is not in the correct format.");
}
/// <summary>
/// Adds a rule to ensure that the string property value matches the specified regular expression.
/// </summary>
/// <typeparam name="T">The type of the object being validated.</typeparam>
/// <param name="rule">The rule builder to which the condition is applied.</param>
/// <param name="regex">The regular expression that the property value must match.</param>
/// <returns>The updated rule builder with the pattern-matching condition added.</returns>
public static IPropertyRuleBuilder<T, string?> Matches<T>(
this IPropertyRuleBuilder<T, string?> rule,
Regex regex)
{
ArgumentNullException.ThrowIfNull(rule);
ArgumentNullException.ThrowIfNull(regex);
return rule.Must(value => IsNull(value) || regex.IsMatch(value),
"Value is not in the correct format.");
}
}