diff --git a/CHANGELOG.md b/CHANGELOG.md index c2ee3ed..740112a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## 0.12.2 + +- Move the instance field `TypeValidatorBase._shouldBeComparedToNull` to a static readonly field (renamed to `_canBeNull`) to cache the reflection result per `TypeValidatorBase` type and eliminate redundant per-instance evaluations. +- Remove the eagerly allocated `NotNullValidationMessageProvider` during `ExpressValidatorBuilder` configuration, and instantiate `NotNullValidationMessageProvider` only when the value is null. +Deprecate `NotNullValidationMessageProvider.GetMessage(ValidationContext)`. +- Update to FluentValidation 12.1.0. +- Rename private `TypeValidatorBase.HasOnlyNullOrEmptyValidators` to `HasNonEmptyValidators` (inverting the boolean logic) and remove the negation of this property in the `ShouldValidate` method. +- DRY refactor of null validation in `TypeValidatorBase`. +- Add tests for null-tolerance validation in `QuickValidator`. +- Add test for validating a primitive type using `ExpressValidatorBuilder`. +- Add a unit test that verifies `ExpressValidator` does not throw when members are null and no null-related validators are used. +- Edit README.md and NuGet.md. + + ## 0.12.0 - Support .NET 8.0 and FluentValidation 12.0.0. diff --git a/README.md b/README.md index a0648cf..9e2f329 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,15 @@ ExpressValidator is a library that provides the ability to validate objects usin - Supports adding a property or field for validation. - Verifies that a property expression is a property and a field expression is a field, and throws `ArgumentException` if it is not. - Supports adding a `Func` that provides a value for validation. -- Provides quick and easy validation using the `QuickValidator`. +- Provides quick and easy validation using `QuickValidator`, with built-in tolerance for `null` values. - Supports asynchronous validation. - Targets .NET Standard 2.0+ ## 📜 Documentation -For details, please check the [API documentation](https://www.tmfexplorer.com/ExpressValidator/api/ExpressValidator.html). +> See the [API documentation](https://www.tmfexplorer.com/ExpressValidator/api/ExpressValidator.html) for reference. +> +> Learn more on [DeepWiki](https://deepwiki.com/kolan72/ExpressValidator/2-core-library-%28expressvalidator%29). ## 🚀 Quick Start @@ -181,7 +183,8 @@ var result = QuickValidator.Validate( .ChildRules((v) => v.RuleFor(o => o.PercentValue1).InclusiveBetween(0, 100)), nameof(obj)); ``` -The `QuickValidator` also provides a `ValidateAsync` method for asynchronous validation. +The `QuickValidator` also provides a `ValidateAsync` method for asynchronous validation. +It is also tolerant of `null` values, i.e., it avoids exceptions when the input is null. ## 🧩 Nuances Of Using The Library diff --git a/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md b/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md index 9f9bb74..f051f0b 100644 --- a/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md +++ b/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.3.12 + +- Support .NET 8.0 and FluentValidation 12.0.0. +- Update Microsoft nuget packages. +- Update ExpressValidator NuGet package to v0.12.0. +- Update NUnit NuGet package to v4.4.0. +- Retarget ExpressValidator.Extensions.DependencyInjection.Sample to .NET 8.0. + + ## 0.3.9 - Update ExpressValidator nuget package. diff --git a/src/ExpressValidator.Extensions.DependencyInjection/ExpressValidator.Extensions.DependencyInjection.csproj b/src/ExpressValidator.Extensions.DependencyInjection/ExpressValidator.Extensions.DependencyInjection.csproj index 4faeedf..9129223 100644 --- a/src/ExpressValidator.Extensions.DependencyInjection/ExpressValidator.Extensions.DependencyInjection.csproj +++ b/src/ExpressValidator.Extensions.DependencyInjection/ExpressValidator.Extensions.DependencyInjection.csproj @@ -3,7 +3,7 @@ netstandard2.0;net8.0 true - 0.3.9 + 0.3.12 true Andrey Kolesnichenko MIT @@ -15,7 +15,7 @@ FluentValidation Validation DependencyInjection The ExpressValidator.Extensions.DependencyInjection package extends ExpressValidator to provide integration with Microsoft Dependency Injection. Copyright 2024 Andrey Kolesnichenko - 0.3.9.0 + 0.3.12.0 diff --git a/src/ExpressValidator.Extensions.DependencyInjection/README.md b/src/ExpressValidator.Extensions.DependencyInjection/README.md index 2db26f9..cb9343d 100644 --- a/src/ExpressValidator.Extensions.DependencyInjection/README.md +++ b/src/ExpressValidator.Extensions.DependencyInjection/README.md @@ -6,6 +6,10 @@ - Additionally, the `IExpressValidatorBuilder` interface can be configured and registered to update the validator parameters when the `ValidationParametersOptions` change. - Ability to dynamically update the validator parameters from options bound to the configuration section without restarting the application by configuring the `IExpressValidatorWithReload` interface. +## 📜 Documentation + +Explore the API documentation and in-depth details on [DeepWiki](https://deepwiki.com/kolan72/ExpressValidator/3-dependency-injection-extension). + ## 🚀 Usage ```csharp diff --git a/src/ExpressValidator/ExpressValidator.csproj b/src/ExpressValidator/ExpressValidator.csproj index 9cfb235..1aa3f89 100644 --- a/src/ExpressValidator/ExpressValidator.csproj +++ b/src/ExpressValidator/ExpressValidator.csproj @@ -3,7 +3,7 @@ netstandard2.0;net8.0 true - 0.12.0 + 0.12.2 true Andrey Kolesnichenko ExpressValidator is a library that provides the ability to validate objects using the FluentValidation library, but without object inheritance from `AbstractValidator`. @@ -15,7 +15,7 @@ ExpressValidator.png NuGet.md - 0.12.0.0 + 0.12.2.0 0.0.0.0 @@ -24,7 +24,7 @@ - + diff --git a/src/ExpressValidator/TypeValidators/NotNullValidationMessageProvider.cs b/src/ExpressValidator/TypeValidators/NotNullValidationMessageProvider.cs index 816746e..e69d166 100644 --- a/src/ExpressValidator/TypeValidators/NotNullValidationMessageProvider.cs +++ b/src/ExpressValidator/TypeValidators/NotNullValidationMessageProvider.cs @@ -1,5 +1,6 @@ using FluentValidation; using FluentValidation.Validators; +using System; namespace ExpressValidator { @@ -11,9 +12,23 @@ public NotNullValidationMessageProvider(string propName) _propName = propName; } + public string GetMessage() => GetDefaultMessageTemplate(null); + +#pragma warning disable S1133 // Deprecated code should be removed + [Obsolete("This method is obsolete")] +#pragma warning restore S1133 // Deprecated code should be removed public string GetMessage(ValidationContext context) { return context.MessageFormatter.AppendPropertyName(_propName).BuildMessage(GetDefaultMessageTemplate(null)); } } + + internal static class NullFallbackMessageProvider + { + public static string GetMessage(string propName, ValidationContext context) + { + var validator = new NotNullValidationMessageProvider(propName); + return context.MessageFormatter.AppendPropertyName(propName).BuildMessage(validator.GetMessage()); + } + } } diff --git a/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs b/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs index 29ccab4..12d33a8 100644 --- a/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs +++ b/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs @@ -11,17 +11,11 @@ namespace ExpressValidator internal abstract class TypeValidatorBase : AbstractValidator { protected IRuleBuilderOptions _ruleBuilderInitial; - private NotNullValidationMessageProvider _nullMessageProvider; private IValidationRule _rule; private string _propName; - private readonly bool _shouldBeComparedToNull; - - protected TypeValidatorBase() - { - _shouldBeComparedToNull = !typeof(T).IsValueType || (Nullable.GetUnderlyingType(typeof(T)) != null); - } + private static readonly bool _canBeNull = !typeof(T).IsValueType || (Nullable.GetUnderlyingType(typeof(T)) != null); protected override void OnRuleAdded(IValidationRule rule) { @@ -36,9 +30,9 @@ protected override void OnRuleAdded(IValidationRule rule) /// protected override bool PreValidate(ValidationContext context, ValidationResult result) { - if (_shouldBeComparedToNull && EqualityComparer.Default.Equals(context.InstanceToValidate, default)) + if (IsValueNull(context.InstanceToValidate)) { - result.Errors.Add(new ValidationFailure(_propName, _nullMessageProvider.GetMessage(context))); + result.Errors.Add(new ValidationFailure(_propName, NullFallbackMessageProvider.GetMessage(_propName, context))); return false; } return true; @@ -55,9 +49,7 @@ public void SetValidation(Action> action, string propN _ruleBuilderInitial = _ruleBuilderInitial.OverridePropertyName(_propName); } - _nullMessageProvider = new NotNullValidationMessageProvider(_propName); - - HasOnlyNullOrEmptyValidators = AllValidatorsAreNullOrEmpty(); + HasNonEmptyValidators = !AllValidatorsAreNullOrEmpty(); } public async Task<(bool IsValid, List Failures)> ValidateExAsync(T value, CancellationToken token = default) @@ -94,9 +86,14 @@ public void SetValidation(Action> action, string propN internal abstract bool? IsAsync { get; } - protected bool ShouldValidate(T value) =>!_shouldBeComparedToNull || !EqualityComparer.Default.Equals(value, default) || !HasOnlyNullOrEmptyValidators; + protected bool ShouldValidate(T value) => !IsValueNull(value) || HasNonEmptyValidators; + + private static bool IsValueNull(T value) + { + return _canBeNull && EqualityComparer.Default.Equals(value, default); + } - private bool HasOnlyNullOrEmptyValidators { get; set; } + private bool HasNonEmptyValidators { get; set; } private bool AllValidatorsAreNullOrEmpty() { diff --git a/src/ExpressValidator/docs/NuGet.md b/src/ExpressValidator/docs/NuGet.md index ad050bb..dfa8de4 100644 --- a/src/ExpressValidator/docs/NuGet.md +++ b/src/ExpressValidator/docs/NuGet.md @@ -8,7 +8,7 @@ ExpressValidator is a library that provides the ability to validate objects usin - Supports adding a property or field for validation. - Verifies that a property expression is a property and a field expression is a field, and throws `ArgumentException` if it is not. - Supports adding a `Func` that provides a value for validation. -- Provides quick and easy validation using the `QuickValidator`. +- Provides quick and easy validation using `QuickValidator`, with built-in tolerance for `null` values. - Supports asynchronous validation. - Targets .NET Standard 2.0+ @@ -144,7 +144,8 @@ var result = QuickValidator.Validate( .ChildRules((v) => v.RuleFor(o => o.PercentValue1).InclusiveBetween(0, 100)), nameof(obj)); ``` -The `QuickValidator` also provides a `ValidateAsync` method for asynchronous validation. +The `QuickValidator` also provides a `ValidateAsync` method for asynchronous validation. +It is also tolerant of `null` values, i.e., it avoids exceptions when the input is null. ## Nuances Of Using The Library diff --git a/tests/ExpressValidator.Tests/ExpressValidatorTests.cs b/tests/ExpressValidator.Tests/ExpressValidatorTests.cs index 3976ca3..e0b4a0f 100644 --- a/tests/ExpressValidator.Tests/ExpressValidatorTests.cs +++ b/tests/ExpressValidator.Tests/ExpressValidatorTests.cs @@ -1,4 +1,5 @@ -using FluentValidation; +using ExpressValidator.Extensions; +using FluentValidation; using NUnit.Framework; using NUnit.Framework.Legacy; using System; @@ -53,6 +54,26 @@ public void Should_Not_Invoke_SuccessValidationHandler_When_IsNotValid() Assert.That(result.IsValid, Is.False); } + [Test] + public void Should_NotThrow_When_MembersAreNull() + { + var result = new ExpressValidatorBuilder() + .AddProperty(o => o.S) + .WithValidation(o => o.MaximumLength(1)) + .AddField(o => o._sField) + .WithValidation(o => o.MinimumLength(1)) + .Build() + .Validate(new ObjWithTwoPublicProps()); + + var em1 = NullFallbackMessageProvider.GetMessage("S", new ValidationContext(null)); + var em2 = NullFallbackMessageProvider.GetMessage("_sField", new ValidationContext(null)); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(em1)); + Assert.That(result.Errors[1].ErrorMessage, Is.EqualTo(em2)); + } + [Test] public void Should_Work_When_IsValid_ForSubObjWithSimpleConditionForComplexProperty() { @@ -349,5 +370,25 @@ public void Should_AddFunc_Preserve_Property_Name(SetPropertyNameType setPropert Assert.That(result.Errors.FirstOrDefault().ErrorMessage, Does.Contain("TestName")); } } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_Validate_Primitive(bool valid) + { + int value; + + if(valid) + value = 3; + else + value = 1; + + var result = new ExpressValidatorBuilder() + .AddFunc(i => i, "value") + .WithValidation(o => o.GreaterThan(2)) + .BuildAndValidate(value); + + Assert.That(result.IsValid, Is.EqualTo(valid)); + } } } diff --git a/tests/ExpressValidator.Tests/NotNullValidationMessageProviderTests.cs b/tests/ExpressValidator.Tests/NotNullValidationMessageProviderTests.cs index 419f0e3..98580ce 100644 --- a/tests/ExpressValidator.Tests/NotNullValidationMessageProviderTests.cs +++ b/tests/ExpressValidator.Tests/NotNullValidationMessageProviderTests.cs @@ -1,10 +1,14 @@ using FluentValidation; using NUnit.Framework; +using System; namespace ExpressValidator.Tests { public class NotNullValidationMessageProviderTests { +#pragma warning disable S1133 // Deprecated code should be removed + [Obsolete("This test is obsolete")] +#pragma warning restore S1133 // Deprecated code should be removed [Test] public void Should_GetMessage_Returns_CorrectMessage_For_Null_Instance() { @@ -13,5 +17,13 @@ public void Should_GetMessage_Returns_CorrectMessage_For_Null_Instance() var res = notNullMsgProvider.GetMessage(new ValidationContext(null)); Assert.That(res.Contains(propName), Is.True); } + + [Test] + public void Should_NullFallbackMessageProvider_Returns_CorrectMessage_For_Null_Instance() + { + const string propName = "TestPropName"; + var res = NullFallbackMessageProvider.GetMessage(propName, new ValidationContext(null)); + Assert.That(res.Contains(propName), Is.True); + } } } diff --git a/tests/ExpressValidator.Tests/QuickValidatorTests.cs b/tests/ExpressValidator.Tests/QuickValidatorTests.cs index f4cba16..bd205a4 100644 --- a/tests/ExpressValidator.Tests/QuickValidatorTests.cs +++ b/tests/ExpressValidator.Tests/QuickValidatorTests.cs @@ -286,6 +286,65 @@ public void Should_Fail_WithExpectedPropertyName_When_ValidationFails_ForNonPrim } } + [Test] + public void Should_Fail_When_NonPrimitive_Value_Is_Null_With_NotNull_Rule() + { + var rule = GetRule(); + + var result = QuickValidator.Validate(null, rule); + Assert.That(result.IsValid, Is.False); + } + + [Test] + public void Should_Fail_When_NonPrimitive_Value_Is_Null_With_Mixed_Rules() + { + var rule = GetMixedWithNullRules(); + + var result = QuickValidator.Validate(null, rule); + Assert.That(result.IsValid, Is.False); + } + + [Test] + public void Should_Valid_When_NonPrimitive_Value_Is_Null_With_Null_Rules() + { + var rule = GetNullRules(); + + var result = QuickValidator.Validate(null, rule); + Assert.That(result.IsValid, Is.True); + } + + [Test] + public void Should_Fail_When_Nullable_Struct_Is_Null_With_NotNull_Rule() + { + var result = QuickValidator.Validate(null, + (opt) => opt.GreaterThan(10) + .GreaterThan(15)); + Assert.That(result.IsValid, Is.False); + } + + [Test] + public void Should_Fail_When_Nullable_Struct_Is_Null_With_Mixed_Rule() + { + var result = QuickValidator.Validate(null, + (opt) => + opt + .Null() + .GreaterThan(10) + .GreaterThan(15)); + Assert.That(result.IsValid, Is.False); + } + + [Test] + public void Should_Valid_When_Nullable_Struct_Is_Null_With_Null_Rules() + { + var result = QuickValidator.Validate(null, + (opt) => + opt + .Null() + .Empty()); + Assert.That(result.IsValid, Is.True); + } + [Test] [TestCase(PropertyNameMode.Default)] [TestCase(PropertyNameMode.TypeName)] @@ -468,6 +527,24 @@ private static Action> GetMixedWithNullRules() + { + return (opt) => + opt.Null() + .Empty() + .ChildRules((v) => v.RuleFor(o => o.I) + .GreaterThan(0)) + .ChildRules((v) => v.RuleFor(o => o.PercentValue1) + .InclusiveBetween(0, 100)); + } + + private static Action> GetNullRules() + { + return (opt) => + opt.Null() + .Empty(); + } + private static Action> GetAsyncRule() { return (opt) =>