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.
This commit is contained in:
Louis Seubert 2026-07-12 18:42:07 +02:00
commit 68194d3cb3
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
4 changed files with 92 additions and 5 deletions

View file

@ -314,6 +314,71 @@ internal sealed class RuleBuilderExtensionsTests
.Throws<ArgumentOutOfRangeException>();
}
[Test]
public async Task I_can_validate_greater_than_with_null_bound_without_throwing()
{
var validator = new PropertyValidator<ComparableModel, ComparableValue?>(model
=> model.Value, rule => rule.GreaterThan(null));
var valid = validator.Validate(new ComparableModel { Value = new ComparableValue { Number = 1 } });
var ignoredNull = validator.Validate(new ComparableModel());
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(ignoredNull.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_greater_than_or_equal_to_with_null_bound_without_throwing()
{
var validator = new PropertyValidator<ComparableModel, ComparableValue?>(model
=> model.Value, rule => rule.GreaterThanOrEqualTo(null));
var valid = validator.Validate(new ComparableModel { Value = new ComparableValue { Number = 1 } });
var ignoredNull = validator.Validate(new ComparableModel());
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(ignoredNull.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_less_than_with_null_bound_without_throwing()
{
var validator = new PropertyValidator<ComparableModel, ComparableValue?>(model
=> model.Value, rule => rule.LessThan(null));
var valid = validator.Validate(new ComparableModel { Value = new ComparableValue { Number = 1 } });
var ignoredNull = validator.Validate(new ComparableModel());
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(ignoredNull.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_less_than_or_equal_to_with_null_bound_without_throwing()
{
var validator = new PropertyValidator<ComparableModel, ComparableValue?>(model
=> model.Value, rule => rule.LessThanOrEqualTo(null));
var valid = validator.Validate(new ComparableModel { Value = new ComparableValue { Number = 1 } });
var ignoredNull = validator.Validate(new ComparableModel());
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(ignoredNull.IsValid).IsTrue();
}
[Test]
public async Task I_can_validate_between_with_null_bounds_without_throwing()
{
var validator = new PropertyValidator<ComparableModel, ComparableValue?>(model
=> model.Value, rule => rule.Between(null, null));
var valid = validator.Validate(new ComparableModel { Value = new ComparableValue { Number = 1 } });
var ignoredNull = validator.Validate(new ComparableModel());
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(ignoredNull.IsValid).IsTrue();
}
[Test]
public async Task I_can_see_it_throw_for_invalid_between_configuration()
{