From af7aa8e0ede4b48553a66f0b1b5fc6c16058ed37 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 28 Nov 2025 12:34:35 +0300 Subject: [PATCH 01/28] Added an optional `Action` `onSuccessValidation` parameter to `ExpressValidatorBuilder.AddFunc` for handling successful validation of `Func` result. --- .../ExpressPropertyValidator.TOptions.cs | 37 +++++++++++++++++-- .../BuilderWithPropValidator.TOptions.cs | 11 ++++-- .../ExpressValidatorBuilder.TOptions.cs | 9 +++-- .../ExpressValidatorWithOptionsTests.cs | 28 ++++++++++++++ 4 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.TOptions.cs b/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.TOptions.cs index 8a9faa0..7f07ac9 100644 --- a/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.TOptions.cs +++ b/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.TOptions.cs @@ -17,11 +17,14 @@ internal class ExpressPropertyValidator : IExpressPropertyVal private Action> _action; - public ExpressPropertyValidator(Func propertyFunc, string propName, bool isAsync) + private readonly Action _onSuccessValidation; + + public ExpressPropertyValidator(Func propertyFunc, string propName, bool isAsync, Action onSuccessValidation = null) { _propertyFunc = propertyFunc; _propName = propName; IsAsync = isAsync; + _onSuccessValidation = onSuccessValidation; } public void SetValidation(Action> action) @@ -29,9 +32,22 @@ public void SetValidation(Action> action) _actionWithOptions = action; } - public Task<(bool IsValid, List Failures)> ValidateAsync(TObj obj, CancellationToken token = default) + public async Task<(bool IsValid, List Failures)> ValidateAsync(TObj obj, CancellationToken token = default) { - return _typeValidator.ValidateExAsync(_propertyFunc(obj), token); + if (_onSuccessValidation != null) + { + var value = _propertyFunc(obj); + var res = await _typeValidator.ValidateExAsync(value, token); + if (res.IsValid) + { + _onSuccessValidation(value); + } + return res; + } + else + { + return await _typeValidator.ValidateExAsync(_propertyFunc(obj), token); + } } public (bool IsValid, List Failures) Validate(TObj obj) @@ -40,7 +56,20 @@ public void SetValidation(Action> action) { throw new InvalidOperationException(); } - return _typeValidator.ValidateEx(_propertyFunc(obj)); + if (_onSuccessValidation != null) + { + var value = _propertyFunc(obj); + var res = _typeValidator.ValidateEx(value); + if (res.IsValid) + { + _onSuccessValidation(value); + } + return res; + } + else + { + return _typeValidator.ValidateEx(_propertyFunc(obj)); + } } public void ApplyOptions(TOptions options) diff --git a/src/ExpressValidator/ValidatorBuilders/BuilderWithPropValidator.TOptions.cs b/src/ExpressValidator/ValidatorBuilders/BuilderWithPropValidator.TOptions.cs index 5246b01..a01c66d 100644 --- a/src/ExpressValidator/ValidatorBuilders/BuilderWithPropValidator.TOptions.cs +++ b/src/ExpressValidator/ValidatorBuilders/BuilderWithPropValidator.TOptions.cs @@ -10,27 +10,30 @@ namespace ExpressValidator public class BuilderWithPropValidator : IBuilderWithPropValidator { private readonly string _propName; - private readonly Func _propertyFunc; + private readonly Func _propertyFunc; + + private readonly Action _onSuccessValidation; internal BuilderWithPropValidator(ExpressValidatorBuilder expressValidatorBuilder, MemberInfo memberInfo) : this(expressValidatorBuilder, memberInfo.GetTypedValue, memberInfo?.Name ?? string.Empty) { } - internal BuilderWithPropValidator(ExpressValidatorBuilder expressValidatorBuilder, Func propertyFunc, string propName) + internal BuilderWithPropValidator(ExpressValidatorBuilder expressValidatorBuilder, Func propertyFunc, string propName, Action onSuccessValidation = null) { ExpressValidatorBuilder = expressValidatorBuilder; _propertyFunc = propertyFunc; _propName = propName; + _onSuccessValidation = onSuccessValidation; } public ExpressValidatorBuilder WithAsyncValidation(Action> action) { - return WithValidationByRules(action, new ExpressPropertyValidator(_propertyFunc, _propName, true)); + return WithValidationByRules(action, new ExpressPropertyValidator(_propertyFunc, _propName, true, _onSuccessValidation)); } public ExpressValidatorBuilder WithValidation(Action> action) { - return WithValidationByRules(action, new ExpressPropertyValidator(_propertyFunc, _propName, false)); + return WithValidationByRules(action, new ExpressPropertyValidator(_propertyFunc, _propName, false, _onSuccessValidation)); } private ExpressValidatorBuilder WithValidationByRules(Action> action, IExpressPropertyValidator expressPropertyValidator) diff --git a/src/ExpressValidator/ValidatorBuilders/ExpressValidatorBuilder.TOptions.cs b/src/ExpressValidator/ValidatorBuilders/ExpressValidatorBuilder.TOptions.cs index c8ee95e..5e84000 100644 --- a/src/ExpressValidator/ValidatorBuilders/ExpressValidatorBuilder.TOptions.cs +++ b/src/ExpressValidator/ValidatorBuilders/ExpressValidatorBuilder.TOptions.cs @@ -41,18 +41,19 @@ public IBuilderWithPropValidator AddField(Expression(this, memInfo); - } - + } + /// /// Add Func for object to get value to validate. /// /// A type of value. /// Func for object. /// A name of the property if the validation failed. + /// Specifies a method to execute when validation succeeds. /// - public IBuilderWithPropValidator AddFunc(Func func, string propName) + public IBuilderWithPropValidator AddFunc(Func func, string propName, Action onSuccessValidation = null) { - return new BuilderWithPropValidator(this, func, propName); + return new BuilderWithPropValidator(this, func, propName, onSuccessValidation); } /// diff --git a/tests/ExpressValidator.Tests/ExpressValidatorWithOptionsTests.cs b/tests/ExpressValidator.Tests/ExpressValidatorWithOptionsTests.cs index 640a220..249278a 100644 --- a/tests/ExpressValidator.Tests/ExpressValidatorWithOptionsTests.cs +++ b/tests/ExpressValidator.Tests/ExpressValidatorWithOptionsTests.cs @@ -220,6 +220,34 @@ public void Should_AddFunc_Preserve_Property_Name(SetPropertyNameType setPropert } } + [Test] + public void Should_Invoke_SuccessValidationHandler_When_IsValid() + { + int percentSum = 0; + var options = new ObjWithTwoPublicPropsOptions() { IGreaterThanValue = 0, IGreaterThanValue2 = 100 }; + var result = new ExpressValidatorBuilder() + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum", (p) => percentSum = p) + .WithValidation((topt, o) => o.InclusiveBetween(topt.IGreaterThanValue, topt.IGreaterThanValue2)) + .Build(options) + .Validate(new ObjWithTwoPublicProps() { PercentValue1 = 20, PercentValue2 = 80 }); + Assert.That(percentSum, Is.EqualTo(100)); + Assert.That(result.IsValid, Is.True); + } + + [Test] + public void Should_Not_Invoke_SuccessValidationHandler_When_IsNotValid() + { + int percentSum = 0; + var options = new ObjWithTwoPublicPropsOptions() { IGreaterThanValue = 0, IGreaterThanValue2 = 100}; + var result = new ExpressValidatorBuilder() + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum", (p) => percentSum = p) + .WithValidation((topt, o) => o.InclusiveBetween(topt.IGreaterThanValue, topt.IGreaterThanValue2)) + .Build(options) + .Validate(new ObjWithTwoPublicProps() { PercentValue1 = 21, PercentValue2 = 83 }); + Assert.That(percentSum, Is.EqualTo(0)); + Assert.That(result.IsValid, Is.False); + } + [Test] [TestCase(true)] [TestCase(false)] From 745872209726cf383968e12ca43f20184bc490e1 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 13 Jan 2026 15:05:28 +0300 Subject: [PATCH 02/28] Remove unnecessary using. --- .../ProxyValidator.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ExpressValidator.Extensions.DependencyInjection/ProxyValidator.cs b/src/ExpressValidator.Extensions.DependencyInjection/ProxyValidator.cs index e4bb06d..22a56e2 100644 --- a/src/ExpressValidator.Extensions.DependencyInjection/ProxyValidator.cs +++ b/src/ExpressValidator.Extensions.DependencyInjection/ProxyValidator.cs @@ -1,15 +1,12 @@ using FluentValidation.Results; using Microsoft.Extensions.DependencyInjection; using System; -using System.Collections.Generic; -using System.Data; -using System.Text; using System.Threading; using System.Threading.Tasks; namespace ExpressValidator.Extensions.DependencyInjection { - internal class ProxyValidator : IExpressValidator + internal class ProxyValidator : IExpressValidator { private readonly IExpressValidator _innerValidator; public ProxyValidator(IServiceProvider serviceProvider) From ac4a88a74494289a6d96ad219ab2b283250dcafb Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 13 Jan 2026 15:34:28 +0300 Subject: [PATCH 03/28] Package 0.4.0 version and update CHANGELOG.md. --- .../CHANGELOG.md | 11 +++++++++++ ...essValidator.Extensions.DependencyInjection.csproj | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md b/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md index f051f0b..98c9a85 100644 --- a/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md +++ b/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md @@ -1,3 +1,14 @@ +## 0.4.0 + +- Introduced class-based validation configuration with dedicated configurator classes inheriting from `ValidatorConfigurator` and registered through `AddExpressValidation`. +- Update to ExpressValidator 0.12.2 and FluentValidation 12.1.0. +- Update Microsoft nuget packages. +- Edit NuGet README. +- Edit README.md. +- Add Shared.csproj to the ExpressValidator.Extensions.DependencyInjection.Sample.sln solution. +- Split sample project into multiple projects illustrating README-described features. + + ## 0.3.12 - Support .NET 8.0 and FluentValidation 12.0.0. diff --git a/src/ExpressValidator.Extensions.DependencyInjection/ExpressValidator.Extensions.DependencyInjection.csproj b/src/ExpressValidator.Extensions.DependencyInjection/ExpressValidator.Extensions.DependencyInjection.csproj index c561f19..2467860 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.12 + 0.4.0 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.12.0 + 0.4.0.0 From 46e795601f31b20a312ade6904aa35e228281388 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 19 Jan 2026 11:31:07 +0300 Subject: [PATCH 04/28] Refactor `ExpressPropertyValidator` to set `IsAsync` in the constructor. --- .../PropertyValidators/ExpressPropertyValidator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.cs b/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.cs index a0ad0f5..50f71f7 100644 --- a/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.cs +++ b/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.cs @@ -20,6 +20,7 @@ public ExpressPropertyValidator(Func propertyFunc, string propName, Typ _propertyFunc = propertyFunc; _propName = propName; _typeValidator = typeValidator; + IsAsync = _typeValidator.IsAsync == true; _onSuccessValidation = onSuccessValidation; } @@ -68,6 +69,6 @@ public void SetValidation(Action> action) } } - public bool IsAsync => _typeValidator.IsAsync == true; + public bool IsAsync { get; } } } From 40a8b7c10c3c1899fd4625b88e56f5d3b51ca2c9 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 19 Jan 2026 13:05:19 +0300 Subject: [PATCH 05/28] Add internal `PropertyValidationProcessor` class. --- .../PropertyValidationProcessor.cs | 64 ++++++++++++++ .../ExpressValidator.Tests.csproj | 1 + .../ExpressValidatorTests.ForAsync.Tests.cs | 4 +- .../PropertyValidationProcessorTests.cs | 85 +++++++++++++++++++ 4 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 src/ExpressValidator/PropertyValidators/PropertyValidationProcessor.cs create mode 100644 tests/ExpressValidator.Tests/PropertyValidationProcessorTests.cs diff --git a/src/ExpressValidator/PropertyValidators/PropertyValidationProcessor.cs b/src/ExpressValidator/PropertyValidators/PropertyValidationProcessor.cs new file mode 100644 index 0000000..50124ba --- /dev/null +++ b/src/ExpressValidator/PropertyValidators/PropertyValidationProcessor.cs @@ -0,0 +1,64 @@ +using FluentValidation.Results; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; + +namespace ExpressValidator +{ + internal class PropertyValidationProcessor + { + private readonly TypeValidatorBase _typeValidator; + private readonly Func _propertyFunc; + private readonly bool _isAsync; + private readonly Action _onSuccessValidation; + + public PropertyValidationProcessor(Func propertyFunc, TypeValidatorBase typeValidator, Action onSuccessValidation) + { + _propertyFunc = propertyFunc; + _typeValidator = typeValidator; + _isAsync = _typeValidator.IsAsync == true; + _onSuccessValidation = onSuccessValidation; + } + + public async Task<(bool IsValid, List Failures)> ValidateAsync(TObj obj, CancellationToken token = default) + { + if (_onSuccessValidation != null) + { + var value = _propertyFunc(obj); + var res = await _typeValidator.ValidateExAsync(value, token); + if (res.IsValid) + { + _onSuccessValidation(value); + } + return res; + } + else + { + return await _typeValidator.ValidateExAsync(_propertyFunc(obj), token); + } + } + + public (bool IsValid, List Failures) Validate(TObj obj) + { + if (_isAsync) + { + throw new InvalidOperationException(); + } + if (_onSuccessValidation != null) + { + var value = _propertyFunc(obj); + var res = _typeValidator.ValidateEx(value); + if (res.IsValid) + { + _onSuccessValidation(value); + } + return res; + } + else + { + return _typeValidator.ValidateEx(_propertyFunc(obj)); + } + } + } +} diff --git a/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj b/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj index 2d107d5..7ebb811 100644 --- a/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj +++ b/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj @@ -81,6 +81,7 @@ + diff --git a/tests/ExpressValidator.Tests/ExpressValidatorTests.ForAsync.Tests.cs b/tests/ExpressValidator.Tests/ExpressValidatorTests.ForAsync.Tests.cs index 69040dc..0dad07d 100644 --- a/tests/ExpressValidator.Tests/ExpressValidatorTests.ForAsync.Tests.cs +++ b/tests/ExpressValidator.Tests/ExpressValidatorTests.ForAsync.Tests.cs @@ -122,11 +122,11 @@ public async Task Should_ValidateAsync_When_Used_In_External_API(bool valid) var result = await new ExpressValidatorBuilder() .AddProperty(o => o.CustomerId) - .WithAsyncValidation(o => o.MustAsync(async (id, cancellation) => + .WithAsyncValidation(o => o.MustAsync(async (id, cancellation) => !await apiClient.IdExistsAsync(id, cancellation))) - .Build() + .Build() .ValidateAsync(customer); Assert.That(result.IsValid, Is.EqualTo(valid)); diff --git a/tests/ExpressValidator.Tests/PropertyValidationProcessorTests.cs b/tests/ExpressValidator.Tests/PropertyValidationProcessorTests.cs new file mode 100644 index 0000000..e4b68f8 --- /dev/null +++ b/tests/ExpressValidator.Tests/PropertyValidationProcessorTests.cs @@ -0,0 +1,85 @@ +using FluentValidation; +using NUnit.Framework; +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests +{ + internal class PropertyValidationProcessorTests + { + [Test] + [TestCase("t", true, false)] + [TestCase("t", true, true)] + [TestCase("tt", false, null)] + public void Should_Validate_When_IsAsync_False(string whatToTest, bool result, bool? useHandler) + { + string handlerResult = null; + void successHandler(string s) { handlerResult = s; } + + MemberInfoParser.TryParse(o => o.Value, MemberTypes.Property, out MemberInfo propertyInfo); + + var validator = new TypeValidator(); + validator.SetValidation(o => o.MaximumLength(1), propertyInfo.Name); + + var processor = new PropertyValidationProcessor(o => o.Value, validator, useHandler == true ? successHandler : null); + + var (IsValid, Failures) = processor.Validate(new ObjWithNullable() { Value = whatToTest }); + Assert.That (IsValid, Is.EqualTo(result)); + if (!result) + { + Assert.That(Failures.Count, Is.EqualTo(1)); + } + else + { + Assert.That(handlerResult, Is.EqualTo(useHandler == true ? whatToTest : null)); + } + } + + [Test] + [TestCase("t")] + [TestCase("tt")] + public void Should_Throw_On_Validate_When_IsAsync_True(string whatToTest) + { + MemberInfoParser.TryParse(o => o.Value, MemberTypes.Property, out MemberInfo propertyInfo); + + var validator = new TypeAsyncValidator(); + validator.SetValidation(o => o.MaximumLength(1), propertyInfo.Name); + + var processor = new PropertyValidationProcessor(o => o.Value, validator, null); + + Assert.Throws(() => processor.Validate(new ObjWithNullable() { Value = whatToTest })); + } + + [Test] + [TestCase("t", true, false, true)] + [TestCase("t", true, true, true)] + [TestCase("tt", false, null, true)] + [TestCase("t", true, false, false)] + [TestCase("t", true, true, false)] + [TestCase("tt", false, null, false)] + public async Task Should_ValidateAsync_Do_Not_Depend_On_TypeValidator_Sync_Type(string whatToTest, bool result, bool? useHandler, bool isAsync) + { + string handlerResult = null; + void successHandler(string s) { handlerResult = s; } + + MemberInfoParser.TryParse(o => o.Value, MemberTypes.Property, out MemberInfo propertyInfo); + + TypeValidatorBase validator = isAsync ? new TypeAsyncValidator() : new TypeValidator(); + validator.SetValidation(o => o.MaximumLength(1), propertyInfo.Name); + + var processor = new PropertyValidationProcessor(o => o.Value, validator, useHandler == true ? successHandler : null); + + var (IsValid, Failures) = await processor.ValidateAsync(new ObjWithNullable() { Value = whatToTest }); + Assert.That(IsValid, Is.EqualTo(result)); + if (!result) + { + Assert.That(Failures.Count, Is.EqualTo(1)); + } + else + { + Assert.That(handlerResult, Is.EqualTo(useHandler == true ? whatToTest : null)); + } + } + } +} From 454bb4b34323ee830a238336c8e9dc2f58489b5a Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 22 Jan 2026 15:19:41 +0300 Subject: [PATCH 06/28] Switch to using `PropertyValidationProcessor` in `ExpressPropertyValidator` and ` ExpressPropertyValidator`. --- .../ExpressPropertyValidator.TOptions.cs | 39 +++---------------- .../ExpressPropertyValidator.cs | 39 +++---------------- 2 files changed, 11 insertions(+), 67 deletions(-) diff --git a/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.TOptions.cs b/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.TOptions.cs index 7f07ac9..afb8cb4 100644 --- a/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.TOptions.cs +++ b/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.TOptions.cs @@ -19,6 +19,8 @@ internal class ExpressPropertyValidator : IExpressPropertyVal private readonly Action _onSuccessValidation; + private PropertyValidationProcessor _validationProcessor; + public ExpressPropertyValidator(Func propertyFunc, string propName, bool isAsync, Action onSuccessValidation = null) { _propertyFunc = propertyFunc; @@ -32,44 +34,14 @@ public void SetValidation(Action> action) _actionWithOptions = action; } - public async Task<(bool IsValid, List Failures)> ValidateAsync(TObj obj, CancellationToken token = default) + public Task<(bool IsValid, List Failures)> ValidateAsync(TObj obj, CancellationToken token = default) { - if (_onSuccessValidation != null) - { - var value = _propertyFunc(obj); - var res = await _typeValidator.ValidateExAsync(value, token); - if (res.IsValid) - { - _onSuccessValidation(value); - } - return res; - } - else - { - return await _typeValidator.ValidateExAsync(_propertyFunc(obj), token); - } + return _validationProcessor.ValidateAsync(obj, token); } public (bool IsValid, List Failures) Validate(TObj obj) { - if (IsAsync) - { - throw new InvalidOperationException(); - } - if (_onSuccessValidation != null) - { - var value = _propertyFunc(obj); - var res = _typeValidator.ValidateEx(value); - if (res.IsValid) - { - _onSuccessValidation(value); - } - return res; - } - else - { - return _typeValidator.ValidateEx(_propertyFunc(obj)); - } + return _validationProcessor.Validate(obj); } public void ApplyOptions(TOptions options) @@ -89,6 +61,7 @@ private void SetTypeValidator() _typeValidator = new TypeValidator(); } _typeValidator.SetValidation(_action, _propName); + _validationProcessor = new PropertyValidationProcessor(_propertyFunc, _typeValidator, _onSuccessValidation); } public bool IsAsync { get; } diff --git a/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.cs b/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.cs index 50f71f7..ae01760 100644 --- a/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.cs +++ b/src/ExpressValidator/PropertyValidators/ExpressPropertyValidator.cs @@ -12,8 +12,8 @@ internal class ExpressPropertyValidator : IExpressPropertyValidator _typeValidator; private readonly Func _propertyFunc; - private readonly Action _onSuccessValidation; + private PropertyValidationProcessor _validationProcessor; public ExpressPropertyValidator(Func propertyFunc, string propName, TypeValidatorBase typeValidator, Action onSuccessValidation = null) { @@ -27,46 +27,17 @@ public ExpressPropertyValidator(Func propertyFunc, string propName, Typ public void SetValidation(Action> action) { _typeValidator.SetValidation(action, _propName); + _validationProcessor = new PropertyValidationProcessor(_propertyFunc, _typeValidator, _onSuccessValidation); } - public async Task<(bool IsValid, List Failures)> ValidateAsync(TObj obj, CancellationToken token = default) + public Task<(bool IsValid, List Failures)> ValidateAsync(TObj obj, CancellationToken token = default) { - if (_onSuccessValidation != null) - { - var value = _propertyFunc(obj); - var res = await _typeValidator.ValidateExAsync(value, token); - if (res.IsValid) - { - _onSuccessValidation(value); - } - return res; - } - else - { - return await _typeValidator.ValidateExAsync(_propertyFunc(obj), token); - } + return _validationProcessor.ValidateAsync(obj, token); } public (bool IsValid, List Failures) Validate(TObj obj) { - if (IsAsync) - { - throw new InvalidOperationException(); - } - if (_onSuccessValidation != null) - { - var value = _propertyFunc(obj); - var res = _typeValidator.ValidateEx(value); - if (res.IsValid) - { - _onSuccessValidation(value); - } - return res; - } - else - { - return _typeValidator.ValidateEx(_propertyFunc(obj)); - } + return _validationProcessor.Validate(obj); } public bool IsAsync { get; } From ad6472035441276fe5a3f8ea2e9f39ddc541dc9b Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 29 Jan 2026 14:24:13 +0300 Subject: [PATCH 07/28] Introduce internal utility `TypeInfo` class and use it in `TypeValidatorBase`. --- .../TypeValidators/TypeValidatorBase.cs | 11 ++---- src/ExpressValidator/Utilities/TypeInfo.cs | 15 ++++++++ .../ExpressValidator.Tests/UtilitiesTests.cs | 34 +++++++++++++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 src/ExpressValidator/Utilities/TypeInfo.cs diff --git a/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs b/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs index 12d33a8..014f8f6 100644 --- a/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs +++ b/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs @@ -15,8 +15,6 @@ internal abstract class TypeValidatorBase : AbstractValidator private IValidationRule _rule; private string _propName; - private static readonly bool _canBeNull = !typeof(T).IsValueType || (Nullable.GetUnderlyingType(typeof(T)) != null); - protected override void OnRuleAdded(IValidationRule rule) { _rule = rule; @@ -30,7 +28,7 @@ protected override void OnRuleAdded(IValidationRule rule) /// protected override bool PreValidate(ValidationContext context, ValidationResult result) { - if (IsValueNull(context.InstanceToValidate)) + if (TypeInfo.IsValueNull(context.InstanceToValidate)) { result.Errors.Add(new ValidationFailure(_propName, NullFallbackMessageProvider.GetMessage(_propName, context))); return false; @@ -86,12 +84,7 @@ public void SetValidation(Action> action, string propN internal abstract bool? IsAsync { get; } - protected bool ShouldValidate(T value) => !IsValueNull(value) || HasNonEmptyValidators; - - private static bool IsValueNull(T value) - { - return _canBeNull && EqualityComparer.Default.Equals(value, default); - } + protected bool ShouldValidate(T value) => !TypeInfo.IsValueNull(value) || HasNonEmptyValidators; private bool HasNonEmptyValidators { get; set; } diff --git a/src/ExpressValidator/Utilities/TypeInfo.cs b/src/ExpressValidator/Utilities/TypeInfo.cs new file mode 100644 index 0000000..9825270 --- /dev/null +++ b/src/ExpressValidator/Utilities/TypeInfo.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace ExpressValidator +{ + internal static class TypeInfo + { + private static readonly bool canBeNull = !typeof(T).IsValueType || (Nullable.GetUnderlyingType(typeof(T)) != null); + + public static bool IsValueNull(T value) + { + return canBeNull && EqualityComparer.Default.Equals(value, default); + } + } +} \ No newline at end of file diff --git a/tests/ExpressValidator.Tests/UtilitiesTests.cs b/tests/ExpressValidator.Tests/UtilitiesTests.cs index f856d18..8ff2d1f 100644 --- a/tests/ExpressValidator.Tests/UtilitiesTests.cs +++ b/tests/ExpressValidator.Tests/UtilitiesTests.cs @@ -53,5 +53,39 @@ public void Should_GetTypedValue_Work() _ = MemberInfoParser.TryParse(s => s._sField, MemberTypes.Field, out MemberInfo memberInfoF); Assert.That(memberInfoF.GetTypedValue(objToTest), Is.EqualTo("TestField")); } + + [Test] + public void Should_ReturnFalse_ForNonNullableValueType_WhenCheckingIsValueNull() + { + Assert.That(TypeInfo.IsValueNull(0), Is.False); + } + + [Test] + public void Should_ReturnTrue_ForNullableValueType_WhenCheckingIsValueNull_AndValueIsNull() + { + int? value = null; + Assert.That(TypeInfo.IsValueNull(value), Is.True); + } + + [Test] + public void Should_ReturnFalse_ForNullableValueType_WhenCheckingIsValueNull_AndValueIsNotNull() + { + int? value = 5; + Assert.That(TypeInfo.IsValueNull(value), Is.False); + } + + [Test] + public void Should_ReturnTrue_ForReferenceType_WhenCheckingIsValueNull_AndValueIsNull() + { + string value = null; + Assert.That(TypeInfo.IsValueNull(value), Is.True); + } + + [Test] + public void Should_ReturnFalse_ForReferenceType_WhenCheckingIsValueNull_AndValueIsNotNull() + { + string value = "hello"; + Assert.That(TypeInfo.IsValueNull(value), Is.False); + } } } From 67b1d7b54bd6a25148b99d8b1b855f0b57813e98 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 30 Jan 2026 17:09:13 +0300 Subject: [PATCH 08/28] Replace `TypeInfo` with `TypeTraits`. --- .../TypeValidators/TypeValidatorBase.cs | 4 ++-- src/ExpressValidator/Utilities/TypeInfo.cs | 15 ------------- src/ExpressValidator/Utilities/TypeTraits.cs | 9 ++++++++ .../ExpressValidator.Tests/UtilitiesTests.cs | 22 +++---------------- 4 files changed, 14 insertions(+), 36 deletions(-) delete mode 100644 src/ExpressValidator/Utilities/TypeInfo.cs create mode 100644 src/ExpressValidator/Utilities/TypeTraits.cs diff --git a/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs b/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs index 014f8f6..3cd66e8 100644 --- a/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs +++ b/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs @@ -28,7 +28,7 @@ protected override void OnRuleAdded(IValidationRule rule) /// protected override bool PreValidate(ValidationContext context, ValidationResult result) { - if (TypeInfo.IsValueNull(context.InstanceToValidate)) + if(TypeTraits.CanBeNull && EqualityComparer.Default.Equals(context.InstanceToValidate, default)) { result.Errors.Add(new ValidationFailure(_propName, NullFallbackMessageProvider.GetMessage(_propName, context))); return false; @@ -84,7 +84,7 @@ public void SetValidation(Action> action, string propN internal abstract bool? IsAsync { get; } - protected bool ShouldValidate(T value) => !TypeInfo.IsValueNull(value) || HasNonEmptyValidators; + protected bool ShouldValidate(T value) => !(TypeTraits.CanBeNull && EqualityComparer.Default.Equals(value, default)) || HasNonEmptyValidators; private bool HasNonEmptyValidators { get; set; } diff --git a/src/ExpressValidator/Utilities/TypeInfo.cs b/src/ExpressValidator/Utilities/TypeInfo.cs deleted file mode 100644 index 9825270..0000000 --- a/src/ExpressValidator/Utilities/TypeInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace ExpressValidator -{ - internal static class TypeInfo - { - private static readonly bool canBeNull = !typeof(T).IsValueType || (Nullable.GetUnderlyingType(typeof(T)) != null); - - public static bool IsValueNull(T value) - { - return canBeNull && EqualityComparer.Default.Equals(value, default); - } - } -} \ No newline at end of file diff --git a/src/ExpressValidator/Utilities/TypeTraits.cs b/src/ExpressValidator/Utilities/TypeTraits.cs new file mode 100644 index 0000000..97c1450 --- /dev/null +++ b/src/ExpressValidator/Utilities/TypeTraits.cs @@ -0,0 +1,9 @@ +using System; + +namespace ExpressValidator +{ + internal static class TypeTraits + { + public static readonly bool CanBeNull = !typeof(T).IsValueType || Nullable.GetUnderlyingType(typeof(T)) != null; + } +} \ No newline at end of file diff --git a/tests/ExpressValidator.Tests/UtilitiesTests.cs b/tests/ExpressValidator.Tests/UtilitiesTests.cs index 8ff2d1f..21651f2 100644 --- a/tests/ExpressValidator.Tests/UtilitiesTests.cs +++ b/tests/ExpressValidator.Tests/UtilitiesTests.cs @@ -57,35 +57,19 @@ public void Should_GetTypedValue_Work() [Test] public void Should_ReturnFalse_ForNonNullableValueType_WhenCheckingIsValueNull() { - Assert.That(TypeInfo.IsValueNull(0), Is.False); + Assert.That(TypeTraits.CanBeNull, Is.False); } [Test] public void Should_ReturnTrue_ForNullableValueType_WhenCheckingIsValueNull_AndValueIsNull() { - int? value = null; - Assert.That(TypeInfo.IsValueNull(value), Is.True); - } - - [Test] - public void Should_ReturnFalse_ForNullableValueType_WhenCheckingIsValueNull_AndValueIsNotNull() - { - int? value = 5; - Assert.That(TypeInfo.IsValueNull(value), Is.False); + Assert.That(TypeTraits.CanBeNull, Is.True); } [Test] public void Should_ReturnTrue_ForReferenceType_WhenCheckingIsValueNull_AndValueIsNull() { - string value = null; - Assert.That(TypeInfo.IsValueNull(value), Is.True); - } - - [Test] - public void Should_ReturnFalse_ForReferenceType_WhenCheckingIsValueNull_AndValueIsNotNull() - { - string value = "hello"; - Assert.That(TypeInfo.IsValueNull(value), Is.False); + Assert.That(TypeTraits.CanBeNull, Is.True); } } } From 3f028bc643c123ca6c00d445a25f3833d0f293f9 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 4 Feb 2026 17:42:31 +0300 Subject: [PATCH 09/28] Add internal utility `TypeHelper` class with `IsNull` method and use it in `TypeValidatorBase`. --- .../TypeValidators/TypeValidatorBase.cs | 4 +-- src/ExpressValidator/Utilities/TypeHelper.cs | 14 ++++++++ .../ExpressValidator.Tests/UtilitiesTests.cs | 34 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/ExpressValidator/Utilities/TypeHelper.cs diff --git a/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs b/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs index 3cd66e8..8a3f04d 100644 --- a/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs +++ b/src/ExpressValidator/TypeValidators/TypeValidatorBase.cs @@ -28,7 +28,7 @@ protected override void OnRuleAdded(IValidationRule rule) /// protected override bool PreValidate(ValidationContext context, ValidationResult result) { - if(TypeTraits.CanBeNull && EqualityComparer.Default.Equals(context.InstanceToValidate, default)) + if(TypeHelper.IsNull(context.InstanceToValidate)) { result.Errors.Add(new ValidationFailure(_propName, NullFallbackMessageProvider.GetMessage(_propName, context))); return false; @@ -84,7 +84,7 @@ public void SetValidation(Action> action, string propN internal abstract bool? IsAsync { get; } - protected bool ShouldValidate(T value) => !(TypeTraits.CanBeNull && EqualityComparer.Default.Equals(value, default)) || HasNonEmptyValidators; + protected bool ShouldValidate(T value) => !TypeHelper.IsNull(value) || HasNonEmptyValidators; private bool HasNonEmptyValidators { get; set; } diff --git a/src/ExpressValidator/Utilities/TypeHelper.cs b/src/ExpressValidator/Utilities/TypeHelper.cs new file mode 100644 index 0000000..2ec5608 --- /dev/null +++ b/src/ExpressValidator/Utilities/TypeHelper.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace ExpressValidator +{ + internal static class TypeHelper + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNull(T value) + { + return TypeTraits.CanBeNull && EqualityComparer.Default.Equals(value, default); + } + } +} diff --git a/tests/ExpressValidator.Tests/UtilitiesTests.cs b/tests/ExpressValidator.Tests/UtilitiesTests.cs index 21651f2..20f1d78 100644 --- a/tests/ExpressValidator.Tests/UtilitiesTests.cs +++ b/tests/ExpressValidator.Tests/UtilitiesTests.cs @@ -71,5 +71,39 @@ public void Should_ReturnTrue_ForReferenceType_WhenCheckingIsValueNull_AndValueI { Assert.That(TypeTraits.CanBeNull, Is.True); } + + [Test] + public void Should_TypeHelper_ReturnFalse_ForNonNullableValueType_WhenCheckingIsValueNull() + { + Assert.That(TypeHelper.IsNull(0), Is.False); + } + + [Test] + public void Should_TypeHelper_ReturnTrue_ForNullableValueType_WhenCheckingIsValueNull_AndValueIsNull() + { + int? value = null; + Assert.That(TypeHelper.IsNull(value), Is.True); + } + + [Test] + public void Should_TypeHelper_ReturnFalse_ForNullableValueType_WhenCheckingIsValueNull_AndValueIsNotNull() + { + int? value = 5; + Assert.That(TypeHelper.IsNull(value), Is.False); + } + + [Test] + public void Should_TypeHelper_ReturnTrue_ForReferenceType_WhenCheckingIsValueNull_AndValueIsNull() + { + string value = null; + Assert.That(TypeHelper.IsNull(value), Is.True); + } + + [Test] + public void Should_TypeHelper_ReturnFalse_ForReferenceType_WhenCheckingIsValueNull_AndValueIsNotNull() + { + string value = "hello"; + Assert.That(TypeHelper.IsNull(value), Is.False); + } } } From a834eac11fc92e65e58764425cbb4caa763ee4e7 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 5 Feb 2026 13:15:49 +0300 Subject: [PATCH 10/28] Introduce abstract `ValidatorProfile` class. --- src/ExpressValidator/ValidatorProfile.cs | 20 ++++++++++ .../ExpressValidator.Tests.csproj | 1 + .../ValidatorProfileTests.cs | 37 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 src/ExpressValidator/ValidatorProfile.cs create mode 100644 tests/ExpressValidator.Tests/ValidatorProfileTests.cs diff --git a/src/ExpressValidator/ValidatorProfile.cs b/src/ExpressValidator/ValidatorProfile.cs new file mode 100644 index 0000000..1dded4d --- /dev/null +++ b/src/ExpressValidator/ValidatorProfile.cs @@ -0,0 +1,20 @@ +namespace ExpressValidator +{ + public abstract class ValidatorProfile + { + protected readonly ExpressValidatorBuilder _validatorBuilder; + + protected ValidatorProfile(OnFirstPropertyValidatorFailed onFirstPropertyValidatorFailed = OnFirstPropertyValidatorFailed.Continue) + { + _validatorBuilder = new ExpressValidatorBuilder(onFirstPropertyValidatorFailed); + } + + public IExpressValidator CreateValidator() + { + Configure(_validatorBuilder); + return _validatorBuilder.Build(); + } + + public abstract void Configure(ExpressValidatorBuilder expressValidatorBuilder); + } +} diff --git a/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj b/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj index 7ebb811..af59d53 100644 --- a/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj +++ b/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj @@ -94,6 +94,7 @@ + diff --git a/tests/ExpressValidator.Tests/ValidatorProfileTests.cs b/tests/ExpressValidator.Tests/ValidatorProfileTests.cs new file mode 100644 index 0000000..17b87ca --- /dev/null +++ b/tests/ExpressValidator.Tests/ValidatorProfileTests.cs @@ -0,0 +1,37 @@ +using NUnit.Framework; + +namespace ExpressValidator.Tests +{ + [TestFixture] + internal class ValidatorProfileTests + { + [Test] + public void Should_AllowDerivedClassesToConfigureValidator_WhenCreatingValidator() + { + // Arrange + var customProfile = new CustomValidatorProfile(); + + // Act + var validator = customProfile.CreateValidator(); + + // Assert + Assert.That(validator, Is.Not.Null); + Assert.That(customProfile.CustomConfigurationWasApplied, Is.True); + } + } + + internal class CustomValidatorProfile : ValidatorProfile + { + public bool CustomConfigurationWasApplied { get; private set; } + + public CustomValidatorProfile(OnFirstPropertyValidatorFailed option = OnFirstPropertyValidatorFailed.Continue) + : base(option) + { + } + + public override void Configure(ExpressValidatorBuilder expressValidatorBuilder) + { + CustomConfigurationWasApplied = true; + } + } +} From fb957c37bf5ad5c92c9bfd22ebf6c73fd89504e7 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 10 Feb 2026 12:54:40 +0300 Subject: [PATCH 11/28] Rename `ValidatorProfile` to `ValidationProfile`. --- .../{ValidatorProfile.cs => ValidationProfile.cs} | 4 ++-- tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj | 2 +- .../{ValidatorProfileTests.cs => ValidationProfileTests.cs} | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/ExpressValidator/{ValidatorProfile.cs => ValidationProfile.cs} (69%) rename tests/ExpressValidator.Tests/{ValidatorProfileTests.cs => ValidationProfileTests.cs} (88%) diff --git a/src/ExpressValidator/ValidatorProfile.cs b/src/ExpressValidator/ValidationProfile.cs similarity index 69% rename from src/ExpressValidator/ValidatorProfile.cs rename to src/ExpressValidator/ValidationProfile.cs index 1dded4d..a0d911b 100644 --- a/src/ExpressValidator/ValidatorProfile.cs +++ b/src/ExpressValidator/ValidationProfile.cs @@ -1,10 +1,10 @@ namespace ExpressValidator { - public abstract class ValidatorProfile + public abstract class ValidationProfile { protected readonly ExpressValidatorBuilder _validatorBuilder; - protected ValidatorProfile(OnFirstPropertyValidatorFailed onFirstPropertyValidatorFailed = OnFirstPropertyValidatorFailed.Continue) + protected ValidationProfile(OnFirstPropertyValidatorFailed onFirstPropertyValidatorFailed = OnFirstPropertyValidatorFailed.Continue) { _validatorBuilder = new ExpressValidatorBuilder(onFirstPropertyValidatorFailed); } diff --git a/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj b/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj index af59d53..c20f3be 100644 --- a/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj +++ b/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj @@ -94,7 +94,7 @@ - + diff --git a/tests/ExpressValidator.Tests/ValidatorProfileTests.cs b/tests/ExpressValidator.Tests/ValidationProfileTests.cs similarity index 88% rename from tests/ExpressValidator.Tests/ValidatorProfileTests.cs rename to tests/ExpressValidator.Tests/ValidationProfileTests.cs index 17b87ca..dcba5e4 100644 --- a/tests/ExpressValidator.Tests/ValidatorProfileTests.cs +++ b/tests/ExpressValidator.Tests/ValidationProfileTests.cs @@ -3,7 +3,7 @@ namespace ExpressValidator.Tests { [TestFixture] - internal class ValidatorProfileTests + internal class ValidationProfileTests { [Test] public void Should_AllowDerivedClassesToConfigureValidator_WhenCreatingValidator() @@ -20,7 +20,7 @@ public void Should_AllowDerivedClassesToConfigureValidator_WhenCreatingValidator } } - internal class CustomValidatorProfile : ValidatorProfile + internal class CustomValidatorProfile : ValidationProfile { public bool CustomConfigurationWasApplied { get; private set; } From 86e0a25b6b422b0e48d26f7f01e3b449a7c56cba Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 10 Feb 2026 13:53:42 +0300 Subject: [PATCH 12/28] Update to FluentValidation 12.1.1. --- src/ExpressValidator/ExpressValidator.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ExpressValidator/ExpressValidator.csproj b/src/ExpressValidator/ExpressValidator.csproj index 70c1eb9..d45c74a 100644 --- a/src/ExpressValidator/ExpressValidator.csproj +++ b/src/ExpressValidator/ExpressValidator.csproj @@ -24,7 +24,7 @@ - + From 23c07f78e496db8925230c86c49c0aa99af0e390 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 18 Feb 2026 19:03:13 +0300 Subject: [PATCH 13/28] Add `TypeName` property to `TypeTraits`. --- src/ExpressValidator/Utilities/TypeTraits.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ExpressValidator/Utilities/TypeTraits.cs b/src/ExpressValidator/Utilities/TypeTraits.cs index 97c1450..1aca5e4 100644 --- a/src/ExpressValidator/Utilities/TypeTraits.cs +++ b/src/ExpressValidator/Utilities/TypeTraits.cs @@ -5,5 +5,6 @@ namespace ExpressValidator internal static class TypeTraits { public static readonly bool CanBeNull = !typeof(T).IsValueType || Nullable.GetUnderlyingType(typeof(T)) != null; + public static readonly string TypeName = typeof(T).Name; } } \ No newline at end of file From c4fd4fc9d0f8579ef99cadf07afe17d6331071d3 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 19 Feb 2026 15:14:27 +0300 Subject: [PATCH 14/28] Add internal static `ValidationFallbackProvider` class representing validation failure for a `null` instance. --- .../ValidationFallbackProvider.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/ExpressValidator/TypeValidators/ValidationFallbackProvider.cs diff --git a/src/ExpressValidator/TypeValidators/ValidationFallbackProvider.cs b/src/ExpressValidator/TypeValidators/ValidationFallbackProvider.cs new file mode 100644 index 0000000..9562429 --- /dev/null +++ b/src/ExpressValidator/TypeValidators/ValidationFallbackProvider.cs @@ -0,0 +1,26 @@ +using FluentValidation; +using FluentValidation.Results; + +namespace ExpressValidator +{ + internal static class ValidationFallbackProvider + { + /// + /// Generates a ValidationResult representing a failure due to a null instance. + /// + public static ValidationResult GetNullFailure() + { + var typeName = TypeTraits.TypeName; + + var message = NullFallbackMessageProvider.GetMessage( + typeName, + new ValidationContext(null) + ); + + var failures = new[] { new ValidationFailure(typeName, message) }; + return new ValidationResult( + failures + ); + } + } +} From 7e8ae7d4f2f6f957dc5771ab60063b554d5ab309 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 19 Feb 2026 15:21:11 +0300 Subject: [PATCH 15/28] Use `ValidationFallbackProvider` for `null` validation in `ExpressValidator.Validate`. --- src/ExpressValidator/ExpressValidator.cs | 4 ++ .../ExpressValidator.Tests.csproj | 1 + .../ExpressValidatorNullObjectTests.cs | 57 +++++++++++++++++++ .../ExpressValidatorTests.cs | 8 +-- .../ExpressValidator.Tests/ObjectsToTests.cs | 42 ++++++++++++-- 5 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 tests/ExpressValidator.Tests/ExpressValidatorNullObjectTests.cs diff --git a/src/ExpressValidator/ExpressValidator.cs b/src/ExpressValidator/ExpressValidator.cs index 29e7897..d9b64f9 100644 --- a/src/ExpressValidator/ExpressValidator.cs +++ b/src/ExpressValidator/ExpressValidator.cs @@ -23,6 +23,10 @@ internal ExpressValidator(IEnumerable> validators, OnFirs public ValidationResult Validate(TObj obj) { + if (TypeHelper.IsNull(obj)) + { + return ValidationFallbackProvider.GetNullFailure(); + } if (_validators.Any(v => v.IsAsync)) { throw new InvalidOperationException($"Object validator has a property or field with asynchronous validation rules. Please use {nameof(ValidateAsync)} method."); diff --git a/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj b/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj index c20f3be..9123a67 100644 --- a/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj +++ b/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj @@ -79,6 +79,7 @@ + diff --git a/tests/ExpressValidator.Tests/ExpressValidatorNullObjectTests.cs b/tests/ExpressValidator.Tests/ExpressValidatorNullObjectTests.cs new file mode 100644 index 0000000..cbbc763 --- /dev/null +++ b/tests/ExpressValidator.Tests/ExpressValidatorNullObjectTests.cs @@ -0,0 +1,57 @@ +using FluentValidation; +using NUnit.Framework; +using System; + +namespace ExpressValidator.Tests +{ + internal class ExpressValidatorNullObjectTests + { + [Test] + public void Should_NotThrow_When_Class_To_Validate_Is_Null() + { + var result = new ExpressValidatorBuilder() + .AddProperty(o => o.Name) + .WithValidation(o => o.NotEmpty() + .MaximumLength(100)) + .AddProperty(o => o.Email) + .WithValidation(o => o.NotEmpty() + .EmailAddress()) + .Build() + .Validate(null); + + var em = NullFallbackMessageProvider.GetMessage( + typeof(Contact).Name, + new ValidationContext(null)); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(1)); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(em)); + + Assert.Throws(() => new ContactValidator().Validate((Contact)null)); + } + + [Test] + public void Should_NotThrow_When_Nullable_Struct_Is_Null() + { + var result = new ExpressValidatorBuilder() + .AddProperty(o => o.Value.Name) + .WithValidation(o => o.NotEmpty() + .MaximumLength(100)) + .AddProperty(o => o.Value.Email) + .WithValidation(o => o.NotEmpty() + .EmailAddress()) + .Build() + .Validate(null); + + var em = NullFallbackMessageProvider.GetMessage( + typeof(ContactStruct?).Name, + new ValidationContext(null)); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(1)); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(em)); + + Assert.Throws(() => new ContactNullableStructValidator().Validate((ContactStruct?)null)); + } + } +} diff --git a/tests/ExpressValidator.Tests/ExpressValidatorTests.cs b/tests/ExpressValidator.Tests/ExpressValidatorTests.cs index e0b4a0f..3518066 100644 --- a/tests/ExpressValidator.Tests/ExpressValidatorTests.cs +++ b/tests/ExpressValidator.Tests/ExpressValidatorTests.cs @@ -98,7 +98,7 @@ public void Should_Work_When_NotValid_ForSubObjWithSimpleConditionForComplexProp .AddProperty(o => o.S) .WithValidation(o => o.MaximumLength(1)) .AddProperty(o => o.Contact) - .WithValidation(o => o.SetValidator(new ContactValidator())) + .WithValidation(o => o.SetValidator(new SimpleContactValidator())) .Build() .Validate(new SubObjWithComplexProperty() { I = 2, S = "b", Contact = new Contact()}); ClassicAssert.AreEqual(false, result.IsValid); @@ -114,7 +114,7 @@ public void Should_Work_When_NotValid_ForSubObjWithSimpleConditionForComplexProp .AddProperty(o => o.S) .WithValidation(o => o.MaximumLength(1)) .AddProperty(o => o.Contacts) - .WithValidation(o => o.ForEach(o1 => o1.SetValidator(new ContactValidator()))) + .WithValidation(o => o.ForEach(o1 => o1.SetValidator(new SimpleContactValidator()))) .Build() .Validate(new SubObjWithComplexCollectionProperty() { I = 1, S = "b"}); ClassicAssert.AreEqual(false, result.IsValid); @@ -130,7 +130,7 @@ public void Should_Work_When_NotValid_ForSubObjWithSimpleConditionForComplexProp .AddProperty(o => o.S) .WithValidation(o => o.MaximumLength(1)) .AddProperty(o => o.Contacts) - .WithValidation(o => o.NotEmpty().ForEach(o1 => o1.SetValidator(new ContactValidator()))) + .WithValidation(o => o.NotEmpty().ForEach(o1 => o1.SetValidator(new SimpleContactValidator()))) .Build() .Validate(new SubObjWithComplexCollectionProperty() { I = 1, S = "b", Contacts = new List() { new Contact(), new Contact() } }); @@ -147,7 +147,7 @@ public void Should_Work_When_Valid_ForSubObjWithSimpleConditionForComplexPropert .AddProperty(o => o.S) .WithValidation(o => o.MaximumLength(1)) .AddProperty(o => o.Contacts) - .WithValidation(o => o.ForEach(o1 => o1.SetValidator(new ContactValidator()))) + .WithValidation(o => o.ForEach(o1 => o1.SetValidator(new SimpleContactValidator()))) .Build() .Validate(new SubObjWithComplexCollectionProperty() { diff --git a/tests/ExpressValidator.Tests/ObjectsToTests.cs b/tests/ExpressValidator.Tests/ObjectsToTests.cs index 30bd8db..1ba9ed2 100644 --- a/tests/ExpressValidator.Tests/ObjectsToTests.cs +++ b/tests/ExpressValidator.Tests/ObjectsToTests.cs @@ -36,16 +36,30 @@ public class ObjWithTwoPublicPropsOptions public int PercentSumMaxValue { get; set; } } - public class ContactValidator : AbstractValidator + public class SimpleContactValidator : AbstractValidator { - public ContactValidator() + public SimpleContactValidator() { RuleFor(x => x.Name).NotNull(); RuleFor(x => x.Email).NotNull(); } } - public class Contact + public class ContactValidator : AbstractValidator + { + public ContactValidator() + { + RuleFor(x => x.Name) + .NotEmpty() + .MaximumLength(100); + + RuleFor(x => x.Email) + .NotEmpty() + .EmailAddress(); + } + } + + public class Contact { public string Name { get; set; } public string Email { get; set; } @@ -53,7 +67,27 @@ public class Contact public string K { get; set; } } - internal class ObjWithNullable + public class ContactNullableStructValidator : AbstractValidator + { + public ContactNullableStructValidator() + { + RuleFor(x => x.Value.Name) + .NotEmpty() + .MaximumLength(100); + + RuleFor(x => x.Value.Email) + .NotEmpty() + .EmailAddress(); + } + } + + public struct ContactStruct + { + public string Name { get; set; } + public string Email { get; set; } + } + + internal class ObjWithNullable { public string Value { get; set; } = "Test"; } From 9c7f062e7ad6e65cc4224c5d102e6ef993400591 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 20 Feb 2026 13:38:26 +0300 Subject: [PATCH 16/28] Use `ValidationFallbackProvider` for `null` validation in `ExpressValidator.ValidateAsync`. --- src/ExpressValidator/ExpressValidator.cs | 4 + .../ExpressValidatorNullObjectTests.cs | 76 ++++++++++++++----- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/ExpressValidator/ExpressValidator.cs b/src/ExpressValidator/ExpressValidator.cs index d9b64f9..c733003 100644 --- a/src/ExpressValidator/ExpressValidator.cs +++ b/src/ExpressValidator/ExpressValidator.cs @@ -36,6 +36,10 @@ public ValidationResult Validate(TObj obj) public Task ValidateAsync(TObj obj, CancellationToken token = default) { + if (TypeHelper.IsNull(obj)) + { + return Task.FromResult(ValidationFallbackProvider.GetNullFailure()); + } return _validationMode == OnFirstPropertyValidatorFailed.Break ? ValidateWithBreakAsync(obj, token) : ValidateWithContinueAsync(obj, token); } diff --git a/tests/ExpressValidator.Tests/ExpressValidatorNullObjectTests.cs b/tests/ExpressValidator.Tests/ExpressValidatorNullObjectTests.cs index cbbc763..9f93177 100644 --- a/tests/ExpressValidator.Tests/ExpressValidatorNullObjectTests.cs +++ b/tests/ExpressValidator.Tests/ExpressValidatorNullObjectTests.cs @@ -1,57 +1,97 @@ using FluentValidation; +using FluentValidation.Results; using NUnit.Framework; using System; +using System.Threading.Tasks; namespace ExpressValidator.Tests { internal class ExpressValidatorNullObjectTests { + private static readonly string NullErrorMessageForClass = NullFallbackMessageProvider.GetMessage(typeof(Contact).Name, + new ValidationContext(null)); + + private static readonly string NullErrorMessageForStruct = NullFallbackMessageProvider.GetMessage(typeof(ContactStruct?).Name, + new ValidationContext(null)); + [Test] - public void Should_NotThrow_When_Class_To_Validate_Is_Null() + [TestCase(true)] + [TestCase(false)] + public async Task Should_NotThrow_When_Class_To_Validate_Is_Null(bool isAsync) { - var result = new ExpressValidatorBuilder() + var validator = new ExpressValidatorBuilder() .AddProperty(o => o.Name) .WithValidation(o => o.NotEmpty() .MaximumLength(100)) .AddProperty(o => o.Email) .WithValidation(o => o.NotEmpty() .EmailAddress()) - .Build() - .Validate(null); + .Build(); - var em = NullFallbackMessageProvider.GetMessage( - typeof(Contact).Name, - new ValidationContext(null)); + ValidationResult result = null; + if (isAsync) + { + result = await validator.ValidateAsync(null); + } + else + { +#pragma warning disable S6966 // Awaitable method should be used + result = validator.Validate(null); +#pragma warning restore S6966 // Awaitable method should be used + } Assert.That(result.IsValid, Is.False); Assert.That(result.Errors.Count, Is.EqualTo(1)); - Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(em)); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(NullErrorMessageForClass)); - Assert.Throws(() => new ContactValidator().Validate((Contact)null)); + if (isAsync) + { + Assert.ThrowsAsync(async () => await new ContactValidator().ValidateAsync((Contact)null)); + } + else + { + Assert.Throws(() => new ContactValidator().Validate((Contact)null)); + } } [Test] - public void Should_NotThrow_When_Nullable_Struct_Is_Null() + [TestCase(true)] + [TestCase(false)] + public async Task Should_NotThrow_When_Nullable_Struct_Is_Null(bool isAsync) { - var result = new ExpressValidatorBuilder() + var validator = new ExpressValidatorBuilder() .AddProperty(o => o.Value.Name) .WithValidation(o => o.NotEmpty() .MaximumLength(100)) .AddProperty(o => o.Value.Email) .WithValidation(o => o.NotEmpty() .EmailAddress()) - .Build() - .Validate(null); + .Build(); - var em = NullFallbackMessageProvider.GetMessage( - typeof(ContactStruct?).Name, - new ValidationContext(null)); + ValidationResult result = null; + if (isAsync) + { + result = await validator.ValidateAsync(null); + } + else + { +#pragma warning disable S6966 // Awaitable method should be used + result = validator.Validate(null); +#pragma warning restore S6966 // Awaitable method should be used + } Assert.That(result.IsValid, Is.False); Assert.That(result.Errors.Count, Is.EqualTo(1)); - Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(em)); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(NullErrorMessageForStruct)); - Assert.Throws(() => new ContactNullableStructValidator().Validate((ContactStruct?)null)); + if (isAsync) + { + Assert.ThrowsAsync(async () => await new ContactNullableStructValidator().ValidateAsync((ContactStruct?)null)); + } + else + { + Assert.Throws(() => new ContactNullableStructValidator().Validate((ContactStruct?)null)); + } } } } From a6a942b4f12811964ea7ff9490ff5a79b070c986 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 26 Feb 2026 10:58:27 +0300 Subject: [PATCH 17/28] Check async validation before null validation in `ExpressValidator.Validate`. --- src/ExpressValidator/ExpressValidator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ExpressValidator/ExpressValidator.cs b/src/ExpressValidator/ExpressValidator.cs index c733003..80cc1a2 100644 --- a/src/ExpressValidator/ExpressValidator.cs +++ b/src/ExpressValidator/ExpressValidator.cs @@ -23,14 +23,14 @@ internal ExpressValidator(IEnumerable> validators, OnFirs public ValidationResult Validate(TObj obj) { - if (TypeHelper.IsNull(obj)) - { - return ValidationFallbackProvider.GetNullFailure(); - } if (_validators.Any(v => v.IsAsync)) { throw new InvalidOperationException($"Object validator has a property or field with asynchronous validation rules. Please use {nameof(ValidateAsync)} method."); } + if (TypeHelper.IsNull(obj)) + { + return ValidationFallbackProvider.GetNullFailure(); + } return _validationMode == OnFirstPropertyValidatorFailed.Break ? ValidateWithBreak(obj) : ValidateWithContinue(obj); } From b98c8c075106d934b90a50492ec3216a90b0535a Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 26 Feb 2026 11:36:53 +0300 Subject: [PATCH 18/28] Improve test coverage for null handling. --- .../ExpressValidatorNullObjectTests.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/ExpressValidator.Tests/ExpressValidatorNullObjectTests.cs b/tests/ExpressValidator.Tests/ExpressValidatorNullObjectTests.cs index 9f93177..acafa65 100644 --- a/tests/ExpressValidator.Tests/ExpressValidatorNullObjectTests.cs +++ b/tests/ExpressValidator.Tests/ExpressValidatorNullObjectTests.cs @@ -93,5 +93,48 @@ public async Task Should_NotThrow_When_Nullable_Struct_Is_Null(bool isAsync) Assert.Throws(() => new ContactNullableStructValidator().Validate((ContactStruct?)null)); } } + + [Test] + public async Task Should_NotThrow_When_Class_To_Validate_Is_Null_And_WithAsyncValidation_Is_Used() + { + var validator = new ExpressValidatorBuilder() + .AddProperty(o => o.Name) + .WithValidation(o => o.NotEmpty() + .MaximumLength(100)) + .AddProperty(o => o.Email) + .WithValidation(o => o.NotEmpty() + .EmailAddress()) + .AddProperty(o => o.K) + .WithAsyncValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build(); + + var result = await validator.ValidateAsync(null); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(1)); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(NullErrorMessageForClass)); + + Assert.ThrowsAsync(async () => await new ContactValidator().ValidateAsync((Contact)null)); + } + + [Test] + public async Task Should_NotThrow_When_Struct_To_Validate_Is_Null_And_WithAsyncValidation_Is_Used() + { + var validator = new ExpressValidatorBuilder() + .AddProperty(o => o.Value.Email) + .WithValidation(o => o.NotEmpty() + .EmailAddress()) + .AddProperty(o => o.Value.Name) + .WithAsyncValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build(); + + var result = await validator.ValidateAsync(null); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(1)); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(NullErrorMessageForStruct)); + + Assert.ThrowsAsync(async () => await new ContactNullableStructValidator().ValidateAsync((ContactStruct?)null)); + } } } From cc85c0bae4b90ef812bb0dd8df7bf203eea5ff4f Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 26 Feb 2026 12:41:07 +0300 Subject: [PATCH 19/28] Simplify `ValidationProfile`. --- src/ExpressValidator/ValidationProfile.cs | 15 ++------------- .../ValidationProfileTests.cs | 8 +------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/ExpressValidator/ValidationProfile.cs b/src/ExpressValidator/ValidationProfile.cs index a0d911b..d57fbf8 100644 --- a/src/ExpressValidator/ValidationProfile.cs +++ b/src/ExpressValidator/ValidationProfile.cs @@ -1,20 +1,9 @@ namespace ExpressValidator { +#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods public abstract class ValidationProfile +#pragma warning restore S1694 // An abstract class should have both abstract and concrete methods { - protected readonly ExpressValidatorBuilder _validatorBuilder; - - protected ValidationProfile(OnFirstPropertyValidatorFailed onFirstPropertyValidatorFailed = OnFirstPropertyValidatorFailed.Continue) - { - _validatorBuilder = new ExpressValidatorBuilder(onFirstPropertyValidatorFailed); - } - - public IExpressValidator CreateValidator() - { - Configure(_validatorBuilder); - return _validatorBuilder.Build(); - } - public abstract void Configure(ExpressValidatorBuilder expressValidatorBuilder); } } diff --git a/tests/ExpressValidator.Tests/ValidationProfileTests.cs b/tests/ExpressValidator.Tests/ValidationProfileTests.cs index dcba5e4..62ea28e 100644 --- a/tests/ExpressValidator.Tests/ValidationProfileTests.cs +++ b/tests/ExpressValidator.Tests/ValidationProfileTests.cs @@ -12,10 +12,9 @@ public void Should_AllowDerivedClassesToConfigureValidator_WhenCreatingValidator var customProfile = new CustomValidatorProfile(); // Act - var validator = customProfile.CreateValidator(); + customProfile.Configure(new ExpressValidatorBuilder()); // Assert - Assert.That(validator, Is.Not.Null); Assert.That(customProfile.CustomConfigurationWasApplied, Is.True); } } @@ -24,11 +23,6 @@ internal class CustomValidatorProfile : ValidationProfile { public bool CustomConfigurationWasApplied { get; private set; } - public CustomValidatorProfile(OnFirstPropertyValidatorFailed option = OnFirstPropertyValidatorFailed.Continue) - : base(option) - { - } - public override void Configure(ExpressValidatorBuilder expressValidatorBuilder) { CustomConfigurationWasApplied = true; From f3f33d4f020097d3b2f208c59e0500bf7e3c3a13 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 27 Feb 2026 18:34:47 +0300 Subject: [PATCH 20/28] Update to System.ValueTuple 4.6.2. --- tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj | 5 +++-- tests/ExpressValidator.Tests/packages.config | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj b/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj index 9123a67..d5bfbbd 100644 --- a/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj +++ b/tests/ExpressValidator.Tests/ExpressValidator.Tests.csproj @@ -69,6 +69,7 @@ ..\..\packages\System.Threading.Tasks.Extensions.4.6.3\lib\net462\System.Threading.Tasks.Extensions.dll + @@ -115,8 +116,8 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + - + \ No newline at end of file diff --git a/tests/ExpressValidator.Tests/packages.config b/tests/ExpressValidator.Tests/packages.config index 3e49123..38137b1 100644 --- a/tests/ExpressValidator.Tests/packages.config +++ b/tests/ExpressValidator.Tests/packages.config @@ -8,5 +8,5 @@ - + \ No newline at end of file From 9ac5147d67168189e6962b62d6111afb8005baf9 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Thu, 5 Mar 2026 16:25:54 +0300 Subject: [PATCH 21/28] Add `ExpressValidator.Tests.Net8.csproj` for testing `ExpressValidator` targeting net8.0. --- ExpressValidator.sln | 6 + src/ExpressValidator/ExpressValidator.csproj | 6 + .../ExpressAsyncValidatorTests.cs | 172 ++++++ .../ExpressAsyncValidatorWithOptionsTests.cs | 153 +++++ .../ExpressValidator.Tests.Net8.csproj | 34 + .../ExpressValidatorExtensionsTests.cs | 82 +++ .../ExpressValidatorNullObjectTests.cs | 140 +++++ .../ExpressValidatorTests.ForAsync.Tests.cs | 135 ++++ .../ExpressValidatorTests.cs | 394 ++++++++++++ ...alidatorWithOptionsTests.ForAsync.Tests.cs | 59 ++ .../ExpressValidatorWithOptionsTests.cs | 277 +++++++++ .../NotNullValidationMessageProviderTests.cs | 29 + .../ObjectsToTests.cs | 126 ++++ .../PropertyValidationProcessorTests.cs | 85 +++ .../QuickValidatorTests.cs | 584 ++++++++++++++++++ ...cValidatorTests.ForNullOrEmptyValidator.cs | 100 +++ ...rTests.ForNullOrEmptyWithOtherValidator.cs | 122 ++++ .../TypeAsyncValidatorTests.cs | 38 ++ ...eValidatorTests.ForNullOrEmptyValidator.cs | 69 +++ ...rTests.ForNullOrEmptyWithOtherValidator.cs | 78 +++ .../TypeValidatorTests.cs | 45 ++ .../UtilitiesTests.cs | 109 ++++ .../ValidationProfileTests.cs | 31 + 23 files changed, 2874 insertions(+) create mode 100644 tests/ExpressValidator.Tests.Net8/ExpressAsyncValidatorTests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/ExpressAsyncValidatorWithOptionsTests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/ExpressValidator.Tests.Net8.csproj create mode 100644 tests/ExpressValidator.Tests.Net8/ExpressValidatorExtensionsTests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/ExpressValidatorNullObjectTests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/ExpressValidatorTests.ForAsync.Tests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/ExpressValidatorTests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/ExpressValidatorWithOptionsTests.ForAsync.Tests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/ExpressValidatorWithOptionsTests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/NotNullValidationMessageProviderTests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/ObjectsToTests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/PropertyValidationProcessorTests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/QuickValidatorTests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.ForNullOrEmptyValidator.cs create mode 100644 tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.ForNullOrEmptyWithOtherValidator.cs create mode 100644 tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/TypeValidatorTests.ForNullOrEmptyValidator.cs create mode 100644 tests/ExpressValidator.Tests.Net8/TypeValidatorTests.ForNullOrEmptyWithOtherValidator.cs create mode 100644 tests/ExpressValidator.Tests.Net8/TypeValidatorTests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/UtilitiesTests.cs create mode 100644 tests/ExpressValidator.Tests.Net8/ValidationProfileTests.cs diff --git a/ExpressValidator.sln b/ExpressValidator.sln index 8f16c88..fb22c39 100644 --- a/ExpressValidator.sln +++ b/ExpressValidator.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExpressValidator.Tests", "t EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmark", "bench\Benchmark\Benchmark.csproj", "{163D81E7-128E-431F-B827-F2AA2BD1D077}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExpressValidator.Tests.Net8", "tests\ExpressValidator.Tests.Net8\ExpressValidator.Tests.Net8.csproj", "{C74789B4-A028-45FC-9771-F5848491AB89}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {163D81E7-128E-431F-B827-F2AA2BD1D077}.Debug|Any CPU.Build.0 = Debug|Any CPU {163D81E7-128E-431F-B827-F2AA2BD1D077}.Release|Any CPU.ActiveCfg = Release|Any CPU {163D81E7-128E-431F-B827-F2AA2BD1D077}.Release|Any CPU.Build.0 = Release|Any CPU + {C74789B4-A028-45FC-9771-F5848491AB89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C74789B4-A028-45FC-9771-F5848491AB89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C74789B4-A028-45FC-9771-F5848491AB89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C74789B4-A028-45FC-9771-F5848491AB89}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/ExpressValidator/ExpressValidator.csproj b/src/ExpressValidator/ExpressValidator.csproj index d45c74a..3e983b5 100644 --- a/src/ExpressValidator/ExpressValidator.csproj +++ b/src/ExpressValidator/ExpressValidator.csproj @@ -51,6 +51,12 @@ + + + <_Parameter1>ExpressValidator.Tests.Net8 + + + True diff --git a/tests/ExpressValidator.Tests.Net8/ExpressAsyncValidatorTests.cs b/tests/ExpressValidator.Tests.Net8/ExpressAsyncValidatorTests.cs new file mode 100644 index 0000000..97cfef9 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/ExpressAsyncValidatorTests.cs @@ -0,0 +1,172 @@ +using FluentValidation; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using System; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests.Net8 +{ + internal class ExpressAsyncValidatorTests + { + [Test] + public async Task Should_Work_For_When_IsValid_Eq_True_And_AllValidatorsSync() + { + var result = await new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(0)) + .AddProperty(o => o.S) + .WithValidation(o => o.MaximumLength(1)) + .AddField(o => o._sField) + .WithValidation(o => o.MinimumLength(1)) + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum") + .WithValidation(o => o.InclusiveBetween(0, 100)) + .Build() + .ValidateAsync(new ObjWithTwoPublicProps() { I = 2, S = "b", _sField = "a", PercentValue1 = 99, PercentValue2 = 1}); + ClassicAssert.AreEqual(true, result.IsValid); + } + + [Test] + public async Task Should_Work_For_When_IsValid_Eq_False_And_AllValidatorsSync() + { + var result = await new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(0)) + .AddProperty(o => o.S) + .WithValidation(o => o.MaximumLength(1)) + .AddField(o => o._sField) + .WithValidation(o => o.MinimumLength(1)) + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum") + .WithValidation(o => o.InclusiveBetween(0, 100)) + .Build() + .ValidateAsync(new ObjWithTwoPublicProps() { I = -2, S = "ab", _sField = "", PercentValue1 = 100, PercentValue2 = 2 }); + ClassicAssert.AreEqual(false, result.IsValid); + ClassicAssert.AreEqual(4, result.Errors.Count); + } + + [Test] + public void Should_ValidateAsyncThrow_If_Cancellation_Occurs() + { + using var ctSource = new CancellationTokenSource(); + ctSource.Cancel(); + var builder = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(0)) + .Build(); + + Assert.ThrowsAsync(async () => await builder.ValidateAsync(new ObjWithTwoPublicProps() { I = 2, S = "b" }, ctSource.Token)); + } + + [Test] + public async Task Should_IsValid_Equals_True_WhenNoValidators() + { + var result = await new ExpressValidatorBuilder() + .Build() + .ValidateAsync(new ObjWithTwoPublicProps()); + Assert.That(result.IsValid, Is.True); + } + + [Test] + [TestCase(SetPropertyNameType.WithName, MemberTypes.Property)] + [TestCase(SetPropertyNameType.NotSetExplicitly, MemberTypes.Property)] + [TestCase(SetPropertyNameType.Override, MemberTypes.Property)] + [TestCase(SetPropertyNameType.WithName, MemberTypes.Field)] + [TestCase(SetPropertyNameType.NotSetExplicitly, MemberTypes.Field)] + [TestCase(SetPropertyNameType.Override, MemberTypes.Field)] + public async Task Should_Preserve_Property_Name(SetPropertyNameType setPropertyNameType, MemberTypes memberTypes) + { + var builder = new ExpressValidatorBuilder(); + IBuilderWithPropValidator builderWithProperty; + + if (memberTypes == MemberTypes.Property) + { + builderWithProperty = builder.AddProperty(o => o.I); + } + else + { + builderWithProperty = builder.AddField(o => o._iField); + } + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + builder = builderWithProperty.WithAsyncValidation(o => o.GreaterThan(0).MustAsync(async(_, __) => { await Task.Delay(1); return true; })); + break; + case SetPropertyNameType.Override: + builder = builderWithProperty.WithAsyncValidation(o => o.GreaterThan(0).MustAsync(async (_, __) => { await Task.Delay(1); return true; }) + .OverridePropertyName("TestPropName")); + break; + case SetPropertyNameType.WithName: + builder = builderWithProperty.WithAsyncValidation(o => o.GreaterThan(0).MustAsync(async (_, __) => { await Task.Delay(1); return true; }) + .WithName("TestName")); + break; + } + var result = await builder.Build().ValidateAsync(new ObjWithTwoPublicProps()); + + Assert.That(result.IsValid, Is.False); + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + case SetPropertyNameType.WithName: + Assert.That(result.Errors.FirstOrDefault()?.PropertyName, memberTypes == MemberTypes.Property ? Is.EqualTo("I") : Is.EqualTo("_iField")); + break; + case SetPropertyNameType.Override: + Assert.That(result.Errors.FirstOrDefault()?.PropertyName, Is.EqualTo("TestPropName")); + break; + } + + if (setPropertyNameType == SetPropertyNameType.WithName) + { + Assert.That(result.Errors.FirstOrDefault()?.ErrorMessage, Does.Contain("TestName")); + } + } + + [Test] + [TestCase(SetPropertyNameType.WithName)] + [TestCase(SetPropertyNameType.NotSetExplicitly)] + [TestCase(SetPropertyNameType.Override)] + public async Task Should_AddFunc_Preserve_Property_Name(SetPropertyNameType setPropertyNameType) + { + var builder = new ExpressValidatorBuilder(); + var builderWithProperty = builder.AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum"); + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + builder = builderWithProperty.WithAsyncValidation(o => o.InclusiveBetween(0, 100).MustAsync(async (_, __) => { await Task.Delay(1); return true; })); + break; + case SetPropertyNameType.Override: + builder = builderWithProperty.WithAsyncValidation(o => o.InclusiveBetween(0, 100).MustAsync(async (_, __) => { await Task.Delay(1); return true; }) + .OverridePropertyName("TestPropName")); + break; + case SetPropertyNameType.WithName: + builder = builderWithProperty.WithAsyncValidation(o => o.InclusiveBetween(0, 100).MustAsync(async (_, __) => { await Task.Delay(1); return true; }) + .WithName("TestName")); + break; + } + + var result = await builder.Build().ValidateAsync(new ObjWithTwoPublicProps() { PercentValue1 = 1, PercentValue2 = 100 }); + + Assert.That(result.IsValid, Is.False); + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + case SetPropertyNameType.WithName: + Assert.That(result.Errors.FirstOrDefault()?.PropertyName, Is.EqualTo("percentSum")); + break; + case SetPropertyNameType.Override: + Assert.That(result.Errors.FirstOrDefault()?.PropertyName, Is.EqualTo("TestPropName")); + break; + } + + if (setPropertyNameType == SetPropertyNameType.WithName) + { + Assert.That(result.Errors.FirstOrDefault()?.ErrorMessage, Does.Contain("TestName")); + } + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/ExpressAsyncValidatorWithOptionsTests.cs b/tests/ExpressValidator.Tests.Net8/ExpressAsyncValidatorWithOptionsTests.cs new file mode 100644 index 0000000..2d8f043 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/ExpressAsyncValidatorWithOptionsTests.cs @@ -0,0 +1,153 @@ +using FluentValidation; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests.Net8 +{ + internal class ExpressAsyncValidatorWithOptionsTests + { + private readonly ObjWithTwoPublicPropsOptions _objWithTwoPublicPropsOptions = new ObjWithTwoPublicPropsOptions() + { + IGreaterThanValue = 0, + SMaximumLengthValue = 1, + SFieldMaximumLengthValue = 1 + }; + + [Test] + public async Task Should_Work_When_IsValid() + { + var result = await new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithAsyncValidation((to, p) => p.GreaterThan(to.IGreaterThanValue).MustAsync(async (_, __) => { await Task.Delay(1); return true;})) + .AddProperty(o => o.S) + .WithAsyncValidation((to, p) => p.MaximumLength(to.SMaximumLengthValue).MustAsync(async (_, __) => { await Task.Delay(1); return true;})) + .AddField(o => o._sField) + .WithAsyncValidation((to, f) => f.MaximumLength(to.SFieldMaximumLengthValue).MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build(_objWithTwoPublicPropsOptions) + .ValidateAsync(new ObjWithTwoPublicProps() { I = 1, S = "b", _sField = "1" }); + ClassicAssert.AreEqual(true, result.IsValid); + } + + [Test] + public async Task Should_Validation_Result_Change_When_Options_Change() + { + var builder = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithAsyncValidation((to, p) => p.GreaterThan(to.IGreaterThanValue).MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .AddProperty(o => o.S) + .WithAsyncValidation((to, p) => p.MaximumLength(to.SMaximumLengthValue).MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .AddField(o => o._sField) + .WithAsyncValidation((to, f) => f.MaximumLength(to.SFieldMaximumLengthValue).MustAsync(async (_, __) => { await Task.Delay(1); return true; })); + + var result1 = await builder.Build(_objWithTwoPublicPropsOptions) + .ValidateAsync(new ObjWithTwoPublicProps() { I = 1, S = "b", _sField = "1" }); + ClassicAssert.AreEqual(true, result1.IsValid); + + var options2 = new ObjWithTwoPublicPropsOptions() { IGreaterThanValue = 2, SMaximumLengthValue = 2, SFieldMaximumLengthValue = 1 }; + var result2 = await builder.Build(options2) + .ValidateAsync(new ObjWithTwoPublicProps() { I = 1, S = "abc", _sField = "12" }); + ClassicAssert.AreEqual(false, result2.IsValid); + ClassicAssert.AreEqual(3, result2.Errors.Count); + + var options3 = new ObjWithTwoPublicPropsOptions() { IGreaterThanValue = 3, SMaximumLengthValue = 3, SFieldMaximumLengthValue = 2 }; + var result3 = await builder.Build(options3) + .ValidateAsync(new ObjWithTwoPublicProps() { I = 2, S = "abcd", _sField = "123" }); + ClassicAssert.AreEqual(false, result3.IsValid); + ClassicAssert.AreEqual(3, result3.Errors.Count); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_Work_When_TheSamePropertyValidators_In_A_Row(bool isValid) + { + var options = new ObjWithTwoPublicPropsOptions() { IGreaterThanValue = 1, IGreaterThanValue2 = 2 }; + int i = isValid ? 3 : -1; + var validator = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation((topt, p) => p.GreaterThan(topt.IGreaterThanValue)) + .AddProperty(o => o.I) + .WithValidation((topt, p) => p.GreaterThan(topt.IGreaterThanValue2)) + .Build(options); + + var result = await validator.ValidateAsync(new ObjWithTwoPublicProps() { I = i }); + if (isValid) + { + Assert.That(result.IsValid, Is.True); + } + else + { + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.IsValid, Is.False); + } + } + + [Test] + [TestCase(SetPropertyNameType.WithName, MemberTypes.Property)] + [TestCase(SetPropertyNameType.NotSetExplicitly, MemberTypes.Property)] + [TestCase(SetPropertyNameType.Override, MemberTypes.Property)] + [TestCase(SetPropertyNameType.WithName, MemberTypes.Field)] + [TestCase(SetPropertyNameType.NotSetExplicitly, MemberTypes.Field)] + [TestCase(SetPropertyNameType.Override, MemberTypes.Field)] + public async Task Should_Preserve_Property_Name(SetPropertyNameType setPropertyNameType, MemberTypes memberTypes) + { + var builder = new ExpressValidatorBuilder(); + IBuilderWithPropValidator builderWithProperty; + + if (memberTypes == MemberTypes.Property) + { + builderWithProperty = builder.AddProperty(o => o.I); + } + else + { + builderWithProperty = builder.AddField(o => o._iField); + } + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + builder = builderWithProperty.WithAsyncValidation((topt, p) => p.GreaterThan(topt.IGreaterThanValue).MustAsync(async (_, __) => { await Task.Delay(1); return true; })); + break; + case SetPropertyNameType.Override: + builder = builderWithProperty.WithAsyncValidation((topt, p) => p.GreaterThan(topt.IGreaterThanValue).MustAsync(async (_, __) => { await Task.Delay(1); return true; }) + .OverridePropertyName("TestPropName")); + break; + case SetPropertyNameType.WithName: + builder = builderWithProperty.WithAsyncValidation((topt, p) => p.GreaterThan(topt.IGreaterThanValue).MustAsync(async (_, __) => { await Task.Delay(1); return true; }) + .WithName("TestName")); + break; + } + var result = await builder.Build(_objWithTwoPublicPropsOptions).ValidateAsync(new ObjWithTwoPublicProps()); + + Assert.That(result.IsValid, Is.False); + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + case SetPropertyNameType.WithName: + Assert.That(result.Errors.FirstOrDefault()?.PropertyName, memberTypes == MemberTypes.Property ? Is.EqualTo("I") : Is.EqualTo("_iField")); + break; + case SetPropertyNameType.Override: + Assert.That(result.Errors.FirstOrDefault()?.PropertyName, Is.EqualTo("TestPropName")); + break; + } + + if (setPropertyNameType == SetPropertyNameType.WithName) + { + Assert.That(result.Errors.FirstOrDefault()?.ErrorMessage, Does.Contain("TestName")); + } + } + + [Test] + public async Task Should_IsValid_Equals_True_WhenNoValidators() + { + var result = await new ExpressValidatorBuilder() + .Build(_objWithTwoPublicPropsOptions) + .ValidateAsync(new ObjWithTwoPublicProps()); + Assert.That(result.IsValid, Is.True); + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/ExpressValidator.Tests.Net8.csproj b/tests/ExpressValidator.Tests.Net8/ExpressValidator.Tests.Net8.csproj new file mode 100644 index 0000000..0388385 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/ExpressValidator.Tests.Net8.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/tests/ExpressValidator.Tests.Net8/ExpressValidatorExtensionsTests.cs b/tests/ExpressValidator.Tests.Net8/ExpressValidatorExtensionsTests.cs new file mode 100644 index 0000000..ea54b2a --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/ExpressValidatorExtensionsTests.cs @@ -0,0 +1,82 @@ +using NUnit.Framework; +using FluentValidation; +using ExpressValidator.Extensions; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests.Net8 +{ + internal class ExpressValidatorExtensionsTests + { + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_BuildAndValidate_Work(bool isValid) + { + int i = isValid ? 1 : -1; + var objToValidate = new ObjWithTwoPublicProps() { I = i }; + + var result = new ExpressValidatorBuilder() + .AddProperty((o) => o.I) + .WithValidation((opt) => opt.GreaterThan(0)) + .BuildAndValidate(objToValidate); + + Assert.That(result.IsValid, Is.EqualTo(isValid)); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_BuildAndValidate_With_TOptions_Param_Work(bool isValid) + { + var objWithTwoPublicPropsOptions = new ObjWithTwoPublicPropsOptions() + { + IGreaterThanValue = isValid ? -1 : 1, + }; + + var objToValidate = new ObjWithTwoPublicProps() { I = 0 }; + + var result = new ExpressValidatorBuilder() + .AddProperty((o) => o.I) + .WithValidation((to, opt) => opt.GreaterThan(to.IGreaterThanValue)) + .BuildAndValidate(objToValidate, objWithTwoPublicPropsOptions); + + Assert.That(result.IsValid, Is.EqualTo(isValid)); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_BuildAndValidateAsync_Work(bool isValid) + { + int i = isValid ? 1 : -1; + var objToValidate = new ObjWithTwoPublicProps() { I = i }; + + var result = await new ExpressValidatorBuilder() + .AddProperty((o) => o.I) + .WithAsyncValidation((opt) => opt.GreaterThan(0).MustAsync(async(_, __) => { await Task.Delay(1); return true; })) + .BuildAndValidateAsync(objToValidate); + + Assert.That(result.IsValid, Is.EqualTo(isValid)); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_BuildAndValidateAsync_With_TOptions_Param_Work(bool isValid) + { + var objWithTwoPublicPropsOptions = new ObjWithTwoPublicPropsOptions() + { + IGreaterThanValue = isValid ? -1 : 1, + }; + + var objToValidate = new ObjWithTwoPublicProps() { I = 0 }; + + var result = await new ExpressValidatorBuilder() + .AddProperty((o) => o.I) + .WithAsyncValidation((to, opt) => opt.GreaterThan(to.IGreaterThanValue).MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .BuildAndValidateAsync(objToValidate, objWithTwoPublicPropsOptions); + + Assert.That(result.IsValid, Is.EqualTo(isValid)); + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/ExpressValidatorNullObjectTests.cs b/tests/ExpressValidator.Tests.Net8/ExpressValidatorNullObjectTests.cs new file mode 100644 index 0000000..5d2555e --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/ExpressValidatorNullObjectTests.cs @@ -0,0 +1,140 @@ +using FluentValidation; +using FluentValidation.Results; +using NUnit.Framework; +using System; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests.Net8 +{ + internal class ExpressValidatorNullObjectTests + { + private static readonly string NullErrorMessageForClass = NullFallbackMessageProvider.GetMessage(typeof(Contact).Name, + new ValidationContext(null!)); + + private static readonly string NullErrorMessageForStruct = NullFallbackMessageProvider.GetMessage(typeof(ContactStruct?).Name, + new ValidationContext(null!)); + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_NotThrow_When_Class_To_Validate_Is_Null(bool isAsync) + { + var validator = new ExpressValidatorBuilder() + .AddProperty(o => o.Name) + .WithValidation(o => o.NotEmpty() + .MaximumLength(100)) + .AddProperty(o => o.Email) + .WithValidation(o => o.NotEmpty() + .EmailAddress()) + .Build(); + + ValidationResult result; + if (isAsync) + { + result = await validator.ValidateAsync(null!); + } + else + { +#pragma warning disable S6966 // Awaitable method should be used + result = validator.Validate(null!); +#pragma warning restore S6966 // Awaitable method should be used + } + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(1)); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(NullErrorMessageForClass)); + + if (isAsync) + { + Assert.ThrowsAsync(async () => await new ContactValidator().ValidateAsync((Contact)null!)); + } + else + { + Assert.Throws(() => new ContactValidator().Validate((Contact)null!)); + } + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_NotThrow_When_Nullable_Struct_Is_Null(bool isAsync) + { + var validator = new ExpressValidatorBuilder() + .AddProperty(o => o!.Value.Name) + .WithValidation(o => o.NotEmpty() + .MaximumLength(100)) + .AddProperty(o => o!.Value.Email) + .WithValidation(o => o.NotEmpty() + .EmailAddress()) + .Build(); + + ValidationResult result; + if (isAsync) + { + result = await validator.ValidateAsync(null); + } + else + { +#pragma warning disable S6966 // Awaitable method should be used + result = validator.Validate(null); +#pragma warning restore S6966 // Awaitable method should be used + } + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(1)); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(NullErrorMessageForStruct)); + + if (isAsync) + { + Assert.ThrowsAsync(async () => await new ContactNullableStructValidator().ValidateAsync((ContactStruct?)null)); + } + else + { + Assert.Throws(() => new ContactNullableStructValidator().Validate((ContactStruct?)null)); + } + } + + [Test] + public async Task Should_NotThrow_When_Class_To_Validate_Is_Null_And_WithAsyncValidation_Is_Used() + { + var validator = new ExpressValidatorBuilder() + .AddProperty(o => o.Name) + .WithValidation(o => o.NotEmpty() + .MaximumLength(100)) + .AddProperty(o => o.Email) + .WithValidation(o => o.NotEmpty() + .EmailAddress()) + .AddProperty(o => o.K) + .WithAsyncValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build(); + + var result = await validator.ValidateAsync(null!); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(1)); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(NullErrorMessageForClass)); + + Assert.ThrowsAsync(async () => await new ContactValidator().ValidateAsync((Contact)null!)); + } + + [Test] + public async Task Should_NotThrow_When_Struct_To_Validate_Is_Null_And_WithAsyncValidation_Is_Used() + { + var validator = new ExpressValidatorBuilder() + .AddProperty(o => o!.Value.Email) + .WithValidation(o => o.NotEmpty() + .EmailAddress()) + .AddProperty(o => o!.Value.Name) + .WithAsyncValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build(); + + var result = await validator.ValidateAsync(null); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(1)); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(NullErrorMessageForStruct)); + + Assert.ThrowsAsync(async () => await new ContactNullableStructValidator().ValidateAsync((ContactStruct?)null)); + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/ExpressValidatorTests.ForAsync.Tests.cs b/tests/ExpressValidator.Tests.Net8/ExpressValidatorTests.ForAsync.Tests.cs new file mode 100644 index 0000000..079f841 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/ExpressValidatorTests.ForAsync.Tests.cs @@ -0,0 +1,135 @@ +using FluentValidation; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests.Net8 +{ + internal partial class ExpressValidatorTests + { + [Test] + public void Should_Validate_Throw_For_AsyncRule_ForProperty() + { + var builder = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithAsyncValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build(); + var exc = Assert.Throws(() => builder.Validate(new ObjWithTwoPublicProps() { I = 1, S = "b" })); + Assert.That(exc.Message, Does.StartWith("Object validator has a property or field with asynchronous validation rules.")); + } + + [Test] + public void Should_Using_WithValidation_With_AsyncRule_Throw_AsyncValidatorInvokedSynchronouslyException_When_Validate() + { + var builder = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build(); + Assert.Throws(() => builder.Validate(new ObjWithTwoPublicProps() { I = 1, S = "b" })); + } + + [Test] + public async Task Should_AsyncInvoke_SuccessValidationHandler_When_IsValid() + { + int percentSum = 0; + var result = await new ExpressValidatorBuilder() + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum", (p) => percentSum = p) + .WithValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return true; }).InclusiveBetween(0, 100)) + .Build() + .ValidateAsync(new ObjWithTwoPublicProps() { PercentValue1 = 20, PercentValue2 = 80 }); + Assert.That(percentSum, Is.EqualTo(100)); + Assert.That(result.IsValid, Is.True); + } + + [Test] + public async Task Should_Not_AsyncInvoke_SuccessValidationHandler_When_IsNotValid() + { + int percentSum = 0; + var result = await new ExpressValidatorBuilder() + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum", (p) => percentSum = p) + .WithValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return true; }).InclusiveBetween(0, 100)) + .Build() + .ValidateAsync(new ObjWithTwoPublicProps() { PercentValue1 = 20, PercentValue2 = 82 }); + Assert.That(percentSum, Is.EqualTo(0)); + Assert.That(result.IsValid, Is.False); + } + + [Test] + public void Should_Validate_Throw_For_AsyncRule_ForField() + { + var builder = new ExpressValidatorBuilder() + .AddField(o => o._sField) + .WithAsyncValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build(); + var exc = Assert.Throws(() => builder.Validate(new ObjWithTwoPublicProps() { I = 1, _sField = "b" })); + Assert.That(exc.Message, Does.StartWith("Object validator has a property or field with asynchronous validation rules.")); + } + + [Test] + public void Should_Validate_Throw_For_AsyncRule_ForFunc() + { + var builder = new ExpressValidatorBuilder() + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum") + .WithAsyncValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build(); + var exc = Assert.Throws(() => builder.Validate(new ObjWithTwoPublicProps() { I = 1, _sField = "b" })); + Assert.That(exc.Message, Does.StartWith("Object validator has a property or field with asynchronous validation rules.")); + } + + [Test] + public async Task Should_ValidateAsync_Work_For_AsyncRules() + { + var result = await new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithAsyncValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .AddField(o => o._sField) + .WithAsyncValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum") + .WithAsyncValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build() + .ValidateAsync(new ObjWithTwoPublicProps() { I = 1, _sField = "" }); + + ClassicAssert.AreEqual(true, result.IsValid); + } + + [Test] + public async Task Should_ValidateAsync_When_Using_Combined_Validation_Strategy() + { + var result = await new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithAsyncValidation(o => o.MustAsync(async (_, __) => { await Task.Delay(1); return false; })) + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum") + .WithValidation(o => o.InclusiveBetween(0, 100)) + .Build() + .ValidateAsync(new ObjWithTwoPublicProps() { I = 1, PercentValue1 = 200 }); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_ValidateAsync_When_Used_In_External_API(bool valid) + { + const int customerId = 1; + var apiClient = new SomeExternalWebApiClient(valid ? 2 : customerId); + var customer = new Customer() { CustomerId = customerId }; + + var result = await new ExpressValidatorBuilder() + .AddProperty(o => o.CustomerId) + .WithAsyncValidation(o => o.MustAsync(async (id, cancellation) => + + !await apiClient.IdExistsAsync(id, cancellation))) + + .Build() + .ValidateAsync(customer); + + Assert.That(result.IsValid, Is.EqualTo(valid)); + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/ExpressValidatorTests.cs b/tests/ExpressValidator.Tests.Net8/ExpressValidatorTests.cs new file mode 100644 index 0000000..5a6ffb3 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/ExpressValidatorTests.cs @@ -0,0 +1,394 @@ +using ExpressValidator.Extensions; +using FluentValidation; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace ExpressValidator.Tests.Net8 +{ + internal partial class ExpressValidatorTests + { + [Test] + public void Should_Work_When_IsValid() + { + var result = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(0)) + .AddProperty(o => o.S) + .WithValidation(o => o.MaximumLength(1)) + .AddField(o=>o._sField) + .WithValidation(o => o.MinimumLength(1)) + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum") + .WithValidation(o => o.InclusiveBetween(0, 100)) + .Build() + .Validate(new ObjWithTwoPublicProps() { I = 1, S = "b", _sField = "1", PercentValue1 = 20, PercentValue2 = 80 }); + ClassicAssert.AreEqual(true, result.IsValid); + } + + [Test] + public void Should_Invoke_SuccessValidationHandler_When_IsValid() + { + int percentSum = 0; + var result = new ExpressValidatorBuilder() + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum", (p)=> percentSum = p) + .WithValidation(o => o.InclusiveBetween(0, 100)) + .Build() + .Validate(new ObjWithTwoPublicProps() { PercentValue1 = 20, PercentValue2 = 80 }); + Assert.That(percentSum, Is.EqualTo(100)); + Assert.That(result.IsValid, Is.True); + } + + [Test] + public void Should_Not_Invoke_SuccessValidationHandler_When_IsNotValid() + { + int percentSum = 0; + var result = new ExpressValidatorBuilder() + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum", (p) => percentSum = p) + .WithValidation(o => o.InclusiveBetween(0, 100)) + .Build() + .Validate(new ObjWithTwoPublicProps() { PercentValue1 = 20, PercentValue2 = 82 }); + Assert.That(percentSum, Is.EqualTo(0)); + 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() + { + var result = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(0)) + .AddProperty(o => o.S) + .WithValidation(o => o.MaximumLength(1)) + .AddProperty(o => o.Contact) + .WithValidation(o => o.Null()) + .Build() + .Validate(new SubObjWithComplexProperty() { I = 1, S = "b"}); + ClassicAssert.AreEqual(true, result.IsValid); + } + + [Test] + public void Should_Work_When_NotValid_ForSubObjWithSimpleConditionForComplexProperty_WithFluentValidator_AndTwoPropsNotValid() + { + var result = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(0)) + .AddProperty(o => o.S) + .WithValidation(o => o.MaximumLength(1)) + .AddProperty(o => o.Contact) + .WithValidation(o => o.SetValidator(new SimpleContactValidator()!)) + .Build() + .Validate(new SubObjWithComplexProperty() { I = 2, S = "b", Contact = new Contact()}); + ClassicAssert.AreEqual(false, result.IsValid); + ClassicAssert.AreEqual(2, result.Errors.Count); + } + + [Test] + public void Should_Work_When_NotValid_ForSubObjWithSimpleConditionForComplexProperty_WithFluentCollectionValidator_PropertyIsNotSet() + { + var result = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(0)) + .AddProperty(o => o.S) + .WithValidation(o => o.MaximumLength(1)) + .AddProperty(o => o.Contacts) + .WithValidation(o => o.ForEach(o1 => o1.SetValidator(new SimpleContactValidator()!))) + .Build() + .Validate(new SubObjWithComplexCollectionProperty() { I = 1, S = "b"}); + ClassicAssert.AreEqual(false, result.IsValid); + ClassicAssert.AreEqual(1, result.Errors.Count); + } + + [Test] + public void Should_Work_When_NotValid_ForSubObjWithSimpleConditionForComplexProperty_WithFluentCollectionValidator_AndTwoObjectsWithTwoPropsNotValid() + { + var result = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(0)) + .AddProperty(o => o.S) + .WithValidation(o => o.MaximumLength(1)) + .AddProperty(o => o.Contacts) + .WithValidation(o => o.NotEmpty().ForEach(o1 => o1.SetValidator(new SimpleContactValidator()))) + .Build() + .Validate(new SubObjWithComplexCollectionProperty() { I = 1, S = "b", + Contacts = [new Contact(), new Contact()] }); + ClassicAssert.AreEqual(false, result.IsValid); + ClassicAssert.AreEqual(4, result.Errors.Count); + } + + [Test] + public void Should_Work_When_Valid_ForSubObjWithSimpleConditionForComplexProperty_WithFluentCollectionValidator() + { + var result = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(0)) + .AddProperty(o => o.S) + .WithValidation(o => o.MaximumLength(1)) + .AddProperty(o => o.Contacts) + .WithValidation(o => o.ForEach(o1 => o1.SetValidator(new SimpleContactValidator()))) + .Build() + .Validate(new SubObjWithComplexCollectionProperty() + { + I = 1, + S = "b", + Contacts = [new Contact() { Email = "", Name = "" }, new Contact() { Email = "", Name = "" }] + }); + ClassicAssert.AreEqual(true, result.IsValid); + } + + [Test] + [TestCase(OnFirstPropertyValidatorFailed.Break)] + [TestCase(OnFirstPropertyValidatorFailed.Continue)] + public void Should_Work_When_NotValid(OnFirstPropertyValidatorFailed validationMode) + { + var result = new ExpressValidatorBuilder(validationMode) + .AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(0)) + .AddProperty(o => o.S) + .WithValidation(o => o.MaximumLength(1)) + .AddField(o => o._sField) + .WithValidation(o => o.MinimumLength(1)) + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum") + .WithValidation(o => o.InclusiveBetween(0, 100)) + .Build() + .Validate(new ObjWithTwoPublicProps() { I = -1, S = "ab", _sField = "", PercentValue1 = 2, PercentValue2 = 101}); + ClassicAssert.AreEqual(false, result.IsValid); + if (validationMode == OnFirstPropertyValidatorFailed.Break) + { + ClassicAssert.AreEqual(1, result.Errors.Count); + } + else + { + ClassicAssert.AreEqual(4, result.Errors.Count); + } + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_Work_When_Nullable_And_Value_Prop_Values_Validated_WithNullValidator(bool propValueIsNull) + { + ObjWithTwoPublicProps objToTest; + + if (propValueIsNull) + { + objToTest = new ObjWithTwoPublicProps() { I = -1 }; + } + else + { + objToTest = new ObjWithTwoPublicProps() { I = -1, S = "ab" }; + } + var result = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(0)) + .AddProperty(o => o.S) + .WithValidation(o => o.Null()) + .Build() + .Validate(objToTest); + + if (propValueIsNull) + { + ClassicAssert.AreEqual(1, result.Errors.Count); + } + else + { + ClassicAssert.AreEqual(2, result.Errors.Count); + } + } + + [Test] + public void Should_Throw_When_Non_Property() + { + Assert.Throws + (() => new ExpressValidatorBuilder() + .AddProperty(o => o) + .WithValidation(o => o.NotNull()) + .Build()); + } + + [Test] + public void Should_Throw_When_Non_Field() + { + Assert.Throws + (() => new ExpressValidatorBuilder() + .AddField(o => o) + .WithValidation(o => o.NotNull()) + .Build()); + } + + [Test] + public void Should_IsValid_Equals_True_WhenNoValidators() + { + var result = new ExpressValidatorBuilder() + .Build() + .Validate(new ObjWithTwoPublicProps()); + Assert.That(result.IsValid, Is.True); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_Work_When_TheSamePropertyValidators_In_A_Row(bool isValid) + { + int i = isValid ? 3 : -1; + var validator = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(1)) + .AddProperty(o => o.I) + .WithValidation(o => o.GreaterThan(2)) + .Build(); + + var result = validator.Validate(new ObjWithTwoPublicProps() { I = i }); + if (isValid) + { + Assert.That(result.IsValid, Is.True); + } + else + { + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.IsValid, Is.False); + } + } + + [Test] + [TestCase(SetPropertyNameType.WithName, MemberTypes.Property)] + [TestCase(SetPropertyNameType.NotSetExplicitly, MemberTypes.Property)] + [TestCase(SetPropertyNameType.Override, MemberTypes.Property)] + [TestCase(SetPropertyNameType.WithName, MemberTypes.Field)] + [TestCase(SetPropertyNameType.NotSetExplicitly, MemberTypes.Field)] + [TestCase(SetPropertyNameType.Override, MemberTypes.Field)] + public void Should_Preserve_Property_Name(SetPropertyNameType setPropertyNameType, MemberTypes memberTypes) + { + var builder = new ExpressValidatorBuilder(); + IBuilderWithPropValidator builderWithProperty; + + if (memberTypes == MemberTypes.Property) + { + builderWithProperty = builder.AddProperty(o => o.I); + } + else + { + builderWithProperty = builder.AddField(o => o._iField); + } + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + builder = builderWithProperty.WithValidation(o => o.GreaterThan(0)); + break; + case SetPropertyNameType.Override: + builder = builderWithProperty.WithValidation(o => o.GreaterThan(0).OverridePropertyName("TestPropName")); + break; + case SetPropertyNameType.WithName: + builder = builderWithProperty.WithValidation(o => o.GreaterThan(0).WithName("TestName")); + break; + } + var result = builder.Build().Validate(new ObjWithTwoPublicProps() { I = -1 }); + + Assert.That(result.IsValid, Is.False); + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + case SetPropertyNameType.WithName: + Assert.That(result.Errors.FirstOrDefault()?.PropertyName, memberTypes == MemberTypes.Property ? Is.EqualTo("I") : Is.EqualTo("_iField")); + break; + case SetPropertyNameType.Override: + Assert.That(result.Errors.FirstOrDefault()?.PropertyName, Is.EqualTo("TestPropName")); + break; + } + + if (setPropertyNameType == SetPropertyNameType.WithName) + { + Assert.That(result.Errors.FirstOrDefault()?.ErrorMessage, Does.Contain("TestName")); + } + } + + [Test] + [TestCase(SetPropertyNameType.WithName)] + [TestCase(SetPropertyNameType.NotSetExplicitly)] + [TestCase(SetPropertyNameType.Override)] + public void Should_AddFunc_Preserve_Property_Name(SetPropertyNameType setPropertyNameType) + { + var builder = new ExpressValidatorBuilder(); + var builderWithProperty = builder.AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum"); + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + builder = builderWithProperty.WithValidation(o => o.InclusiveBetween(0, 100)); + break; + case SetPropertyNameType.Override: + builder = builderWithProperty.WithValidation(o => o.InclusiveBetween(0, 100) + .OverridePropertyName("TestPropName")); + break; + case SetPropertyNameType.WithName: + builder = builderWithProperty.WithValidation(o => o.InclusiveBetween(0, 100) + .WithName("TestName")); + break; + } + + var result = builder.Build().Validate(new ObjWithTwoPublicProps() { PercentValue1 = 1, PercentValue2 = 100 }); + + Assert.That(result.IsValid, Is.False); + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + case SetPropertyNameType.WithName: + Assert.That(result.Errors.FirstOrDefault()?.PropertyName, Is.EqualTo("percentSum")); + break; + case SetPropertyNameType.Override: + Assert.That(result.Errors.FirstOrDefault()?.PropertyName, Is.EqualTo("TestPropName")); + break; + } + + if (setPropertyNameType == SetPropertyNameType.WithName) + { + 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.Net8/ExpressValidatorWithOptionsTests.ForAsync.Tests.cs b/tests/ExpressValidator.Tests.Net8/ExpressValidatorWithOptionsTests.ForAsync.Tests.cs new file mode 100644 index 0000000..e4f0cbb --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/ExpressValidatorWithOptionsTests.ForAsync.Tests.cs @@ -0,0 +1,59 @@ +using FluentValidation; +using NUnit.Framework; +using System; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests.Net8 +{ + internal partial class ExpressValidatorWithOptonsTests + { + [Test] + public void Should_Validate_Throw_For_AsyncRule_ForProperty() + { + var builder = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithAsyncValidation((_, p) => p.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build(_objWithTwoPublicPropsOptions); + var exc = Assert.Throws(() => builder.Validate(new ObjWithTwoPublicProps() { I = 1, S = "b" })); + Assert.That(exc.Message, Does.StartWith("Object validator has a property or field with asynchronous validation rules.")); + } + + [Test] + public void Should_Validate_Throw_For_AsyncRule_ForField() + { + var builder = new ExpressValidatorBuilder() + .AddField(o => o._sField) + .WithAsyncValidation((_, f) => f.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build(_objWithTwoPublicPropsOptions); + var exc = Assert.Throws(() => builder.Validate(new ObjWithTwoPublicProps() { I = 1, _sField = "b" })); + Assert.That(exc.Message, Does.StartWith("Object validator has a property or field with asynchronous validation rules.")); + } + + [Test] + public void Should_Validate_Throw_For_AsyncRule_ForFunc() + { + var builder = new ExpressValidatorBuilder() + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum") + .WithAsyncValidation((_, f) => f.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build(_objWithTwoPublicPropsOptions); + var exc = Assert.Throws(() => builder.Validate(new ObjWithTwoPublicProps() { I = 1, _sField = "b" })); + Assert.That(exc.Message, Does.StartWith("Object validator has a property or field with asynchronous validation rules.")); + } + + [Test] + public async Task Should_ValidateAsync_Work_For_AsyncRules() + { + var result = await new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithAsyncValidation((_, p) => p.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .AddField(o => o._sField) + .WithAsyncValidation((_, p) => p.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum") + .WithAsyncValidation((_, p) => p.MustAsync(async (_, __) => { await Task.Delay(1); return true; })) + .Build(_objWithTwoPublicPropsOptions) + .ValidateAsync(new ObjWithTwoPublicProps() { I = 1, _sField = "" }); + + Assert.That(result.IsValid, Is.EqualTo(true)); + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/ExpressValidatorWithOptionsTests.cs b/tests/ExpressValidator.Tests.Net8/ExpressValidatorWithOptionsTests.cs new file mode 100644 index 0000000..6858bb7 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/ExpressValidatorWithOptionsTests.cs @@ -0,0 +1,277 @@ +using FluentValidation; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using System; +using System.Linq; +using System.Reflection; + +namespace ExpressValidator.Tests.Net8 +{ + internal partial class ExpressValidatorWithOptonsTests + { + private readonly ObjWithTwoPublicPropsOptions _objWithTwoPublicPropsOptions = new() + { + IGreaterThanValue = 0, + SMaximumLengthValue = 1, + SFieldMaximumLengthValue = 1, + PercentSumMinValue = 0, + PercentSumMaxValue = 100, + }; + + [Test] + public void Should_Work_When_IsValid() + { + var result = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation((to, p) => p.GreaterThan(to.IGreaterThanValue)) + .AddProperty(o => o.S) + .WithValidation((to, p)=> p.MaximumLength(to.SMaximumLengthValue)) + .AddField(o => o._sField) + .WithValidation((to, f) => f.MaximumLength(to.SFieldMaximumLengthValue)) + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum") + .WithValidation((to, f) => f.InclusiveBetween(to.PercentSumMinValue, to.PercentSumMaxValue)) + .Build(_objWithTwoPublicPropsOptions) + .Validate(new ObjWithTwoPublicProps() { I = 1, S = "b", _sField = "1", PercentValue2 = 80}); + ClassicAssert.AreEqual(true, result.IsValid); + } + + [Test] + public void Should_Validation_Result_Change_When_Options_Change() + { + var builder = new ExpressValidatorBuilder() + .AddProperty(o => o.I) + .WithValidation((to, p) => p.GreaterThan(to.IGreaterThanValue)) + .AddProperty(o => o.S) + .WithValidation((to, p) => p.MaximumLength(to.SMaximumLengthValue)) + .AddField(o => o._sField) + .WithValidation((to, f) => f.MaximumLength(to.SFieldMaximumLengthValue)) + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum") + .WithValidation((to, f) => f.InclusiveBetween(to.PercentSumMinValue, to.PercentSumMaxValue)); + + var result1 = builder.Build(_objWithTwoPublicPropsOptions) + .Validate(new ObjWithTwoPublicProps() { I = 1, S = "b", _sField = "1", PercentValue2 = 80}); + ClassicAssert.AreEqual(true, result1.IsValid); + + var options2 = new ObjWithTwoPublicPropsOptions() { IGreaterThanValue = 2, SMaximumLengthValue = 2, SFieldMaximumLengthValue = 1, PercentSumMaxValue = 80 }; + var result2 = builder.Build(options2) + .Validate(new ObjWithTwoPublicProps() { I = 1, S = "abc", _sField = "12", PercentValue2 = 90}); + ClassicAssert.AreEqual(false, result2.IsValid); + ClassicAssert.AreEqual(4, result2.Errors.Count); + + var options3 = new ObjWithTwoPublicPropsOptions() { IGreaterThanValue = 3, SMaximumLengthValue = 3, SFieldMaximumLengthValue = 1, PercentSumMaxValue = 70 }; + var result3 = builder.Build(options3) + .Validate(new ObjWithTwoPublicProps() { I = 2, S = "abcd", _sField = "123", PercentValue2 = 80 }); + ClassicAssert.AreEqual(false, result3.IsValid); + ClassicAssert.AreEqual(4, result3.Errors.Count); + } + + [Test] + [TestCase(OnFirstPropertyValidatorFailed.Break)] + [TestCase(OnFirstPropertyValidatorFailed.Continue)] + public void Should_Work_When_NotValid(OnFirstPropertyValidatorFailed validationMode) + { + var result = new ExpressValidatorBuilder(validationMode) + .AddProperty(o => o.I) + .WithValidation((to, p) => p.GreaterThan(to.IGreaterThanValue)) + .AddProperty(o => o.S) + .WithValidation((to, p) => p.MaximumLength(to.SMaximumLengthValue)) + .AddField(o => o._sField) + .WithValidation((to, f) => f.MaximumLength(to.SFieldMaximumLengthValue)) + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum") + .WithValidation((to, f) => f.InclusiveBetween(to.PercentSumMinValue, to.PercentSumMaxValue)) + .Build(_objWithTwoPublicPropsOptions) + .Validate(new ObjWithTwoPublicProps() { I = -1, S = "ab", _sField = "ab", PercentValue1 = 2, PercentValue2 = 101 }); + ClassicAssert.AreEqual(false, result.IsValid); + if (validationMode == OnFirstPropertyValidatorFailed.Break) + { + ClassicAssert.AreEqual(1, result.Errors.Count); + } + else + { + ClassicAssert.AreEqual(4, result.Errors.Count); + } + } + + [Test] + [TestCase(SetPropertyNameType.WithName, MemberTypes.Property)] + [TestCase(SetPropertyNameType.NotSetExplicitly, MemberTypes.Property)] + [TestCase(SetPropertyNameType.Override, MemberTypes.Property)] + [TestCase(SetPropertyNameType.WithName, MemberTypes.Field)] + [TestCase(SetPropertyNameType.NotSetExplicitly, MemberTypes.Field)] + [TestCase(SetPropertyNameType.Override, MemberTypes.Field)] + public void Should_Preserve_Property_Name(SetPropertyNameType setPropertyNameType, MemberTypes memberTypes) + { + var builder = new ExpressValidatorBuilder(); + IBuilderWithPropValidator builderWithProperty = null; + + if (memberTypes == MemberTypes.Property) + { + builderWithProperty = builder.AddProperty(o => o.I); + } + else + { + builderWithProperty = builder.AddField(o => o._iField); + } + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + builder = builderWithProperty.WithValidation((to, p) => p.GreaterThan(to.IGreaterThanValue)); + break; + case SetPropertyNameType.Override: + builder = builderWithProperty.WithValidation((to, p) => p.GreaterThan(to.IGreaterThanValue).OverridePropertyName("TestPropName")); + break; + case SetPropertyNameType.WithName: + builder = builderWithProperty.WithValidation((to, p) => p.GreaterThan(to.IGreaterThanValue).WithName("TestName")); + break; + } + var result = builder.Build(_objWithTwoPublicPropsOptions).Validate(new ObjWithTwoPublicProps() { I = -1 }); + + Assert.That(result.IsValid, Is.False); + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + case SetPropertyNameType.WithName: + Assert.That(result.Errors.FirstOrDefault().PropertyName, memberTypes == MemberTypes.Property ? Is.EqualTo("I") : Is.EqualTo("_iField")); + break; + case SetPropertyNameType.Override: + Assert.That(result.Errors.FirstOrDefault().PropertyName, Is.EqualTo("TestPropName")); + break; + } + + if (setPropertyNameType == SetPropertyNameType.WithName) + { + Assert.That(result.Errors.FirstOrDefault().ErrorMessage, Does.Contain("TestName")); + } + } + + [Test] + public void Should_IsValid_Equals_True_WhenNoValidators() + { + var result = new ExpressValidatorBuilder() + .Build(_objWithTwoPublicPropsOptions) + .Validate(new ObjWithTwoPublicProps()); + Assert.That(result.IsValid, Is.True); + } + + [Test] + public void Should_Throw_When_Non_Property() + { + Assert.Throws + (() => new ExpressValidatorBuilder() + .AddProperty(o => o) + .WithValidation((_, p) => p.NotNull()) + .Build(_objWithTwoPublicPropsOptions)); + } + + [Test] + public void Should_Throw_When_Non_Field() + { + Assert.Throws + (() => new ExpressValidatorBuilder() + .AddField(o => o) + .WithValidation((_, p) => p.NotNull()) + .Build(_objWithTwoPublicPropsOptions)); + } + + [Test] + [TestCase(SetPropertyNameType.WithName)] + [TestCase(SetPropertyNameType.NotSetExplicitly)] + [TestCase(SetPropertyNameType.Override)] + public void Should_AddFunc_Preserve_Property_Name(SetPropertyNameType setPropertyNameType) + { + var builder = new ExpressValidatorBuilder(); + var builderWithProperty = builder.AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum"); + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + builder = builderWithProperty.WithValidation((to, p) => p.InclusiveBetween(to.PercentSumMinValue, to.PercentSumMaxValue)); + break; + case SetPropertyNameType.Override: + builder = builderWithProperty.WithValidation((to, p) => p.InclusiveBetween(to.PercentSumMinValue, to.PercentSumMaxValue) + .OverridePropertyName("TestPropName")); + break; + case SetPropertyNameType.WithName: + builder = builderWithProperty.WithValidation((to, p) => p.InclusiveBetween(to.PercentSumMinValue, to.PercentSumMaxValue) + .WithName("TestName")); + break; + } + + var result = builder.Build(_objWithTwoPublicPropsOptions).Validate(new ObjWithTwoPublicProps() { PercentValue1 = 1, PercentValue2 = 100 }); + + Assert.That(result.IsValid, Is.False); + + switch (setPropertyNameType) + { + case SetPropertyNameType.NotSetExplicitly: + case SetPropertyNameType.WithName: + Assert.That(result.Errors.FirstOrDefault().PropertyName, Is.EqualTo("percentSum")); + break; + case SetPropertyNameType.Override: + Assert.That(result.Errors.FirstOrDefault().PropertyName, Is.EqualTo("TestPropName")); + break; + } + + if (setPropertyNameType == SetPropertyNameType.WithName) + { + Assert.That(result.Errors.FirstOrDefault().ErrorMessage, Does.Contain("TestName")); + } + } + + [Test] + public void Should_Invoke_SuccessValidationHandler_When_IsValid() + { + int percentSum = 0; + var options = new ObjWithTwoPublicPropsOptions() { IGreaterThanValue = 0, IGreaterThanValue2 = 100 }; + var result = new ExpressValidatorBuilder() + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum", (p) => percentSum = p) + .WithValidation((topt, o) => o.InclusiveBetween(topt.IGreaterThanValue, topt.IGreaterThanValue2)) + .Build(options) + .Validate(new ObjWithTwoPublicProps() { PercentValue1 = 20, PercentValue2 = 80 }); + Assert.That(percentSum, Is.EqualTo(100)); + Assert.That(result.IsValid, Is.True); + } + + [Test] + public void Should_Not_Invoke_SuccessValidationHandler_When_IsNotValid() + { + int percentSum = 0; + var options = new ObjWithTwoPublicPropsOptions() { IGreaterThanValue = 0, IGreaterThanValue2 = 100}; + var result = new ExpressValidatorBuilder() + .AddFunc(o => o.PercentValue1 + o.PercentValue2, "percentSum", (p) => percentSum = p) + .WithValidation((topt, o) => o.InclusiveBetween(topt.IGreaterThanValue, topt.IGreaterThanValue2)) + .Build(options) + .Validate(new ObjWithTwoPublicProps() { PercentValue1 = 21, PercentValue2 = 83 }); + Assert.That(percentSum, Is.EqualTo(0)); + Assert.That(result.IsValid, Is.False); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_Workaround_For_Condition_Using_Validating_Object_Work(bool isValid) + { + var customer = new Customer() { CustomerDiscount = 0, IsPreferredCustomer = !isValid }; + + var result = new ExpressValidatorBuilder() + .AddProperty(c => c.CustomerDiscount) + .WithValidation((c, p) => p.GreaterThan(0) + .When((_) => c.IsPreferredCustomer)) + .Build(customer) + .Validate(customer); + + if (isValid) + { + Assert.That(result.IsValid, Is.True); + } + else + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(1)); + Assert.That(result.Errors.FirstOrDefault().PropertyName, Is.EqualTo(nameof(Customer.CustomerDiscount))); + } + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/NotNullValidationMessageProviderTests.cs b/tests/ExpressValidator.Tests.Net8/NotNullValidationMessageProviderTests.cs new file mode 100644 index 0000000..4dac7ad --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/NotNullValidationMessageProviderTests.cs @@ -0,0 +1,29 @@ +using FluentValidation; +using NUnit.Framework; +using System; + +namespace ExpressValidator.Tests.Net8 +{ + 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() + { + const string propName = "TestPropName"; + var notNullMsgProvider = new NotNullValidationMessageProvider(propName); + 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.Net8/ObjectsToTests.cs b/tests/ExpressValidator.Tests.Net8/ObjectsToTests.cs new file mode 100644 index 0000000..21207f4 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/ObjectsToTests.cs @@ -0,0 +1,126 @@ +using FluentValidation; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests.Net8 +{ + public class SubObjWithComplexProperty : ObjWithTwoPublicProps + { + public Contact? Contact { get; set; } + } + + public class SubObjWithComplexCollectionProperty : ObjWithTwoPublicProps + { + public IEnumerable? Contacts { get; set; } + } + + public class ObjWithTwoPublicProps + { + public int I { get; set; } + public string? S { get; set; } + public string? _sField; + public int _iField; + public int PercentValue1 { get; set; } + public int PercentValue2 { get; set; } + } + + public class ObjWithTwoPublicPropsOptions + { + public int IGreaterThanValue { get; set; } + public int IGreaterThanValue2 { get; set; } + public int SMaximumLengthValue { get; set; } + public int SFieldMaximumLengthValue { get; set; } + public int PercentSumMinValue { get; set; } + public int PercentSumMaxValue { get; set; } + } + + public class SimpleContactValidator : AbstractValidator + { + public SimpleContactValidator() + { + RuleFor(x => x.Name).NotNull(); + RuleFor(x => x.Email).NotNull(); + } + } + + public class ContactValidator : AbstractValidator + { + public ContactValidator() + { + RuleFor(x => x.Name) + .NotEmpty() + .MaximumLength(100); + + RuleFor(x => x.Email) + .NotEmpty() + .EmailAddress(); + } + } + + public class Contact + { + public string? Name { get; set; } + public string? Email { get; set; } + + public string? K { get; set; } + } + + public class ContactNullableStructValidator : AbstractValidator + { + public ContactNullableStructValidator() + { + RuleFor(x => x!.Value.Name) + .NotEmpty() + .MaximumLength(100); + + RuleFor(x => x!.Value.Email) + .NotEmpty() + .EmailAddress(); + } + } + + public struct ContactStruct + { + public string Name { get; set; } + public string Email { get; set; } + } + + internal class ObjWithNullable + { + public string Value { get; set; } = "Test"; + } + + internal enum SetPropertyNameType + { + NotSetExplicitly, + Override, + WithName + } + + public class Customer + { + public int CustomerId { get; set; } + public string? Name { get; set; } + public decimal CustomerDiscount { get; set; } + public bool IsPreferredCustomer { get; set; } + } + + public class SomeExternalWebApiClient + { + private readonly int _existedId; + + public SomeExternalWebApiClient(int existedId) + { + _existedId = existedId; + } + + public async Task IdExistsAsync(int id, CancellationToken token) + { + await Task.Delay(TimeSpan.FromTicks(1), token); + return _existedId == id; + } + + } +} diff --git a/tests/ExpressValidator.Tests.Net8/PropertyValidationProcessorTests.cs b/tests/ExpressValidator.Tests.Net8/PropertyValidationProcessorTests.cs new file mode 100644 index 0000000..d178703 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/PropertyValidationProcessorTests.cs @@ -0,0 +1,85 @@ +using FluentValidation; +using NUnit.Framework; +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests.Net8 +{ + internal class PropertyValidationProcessorTests + { + [Test] + [TestCase("t", true, false)] + [TestCase("t", true, true)] + [TestCase("tt", false, null)] + public void Should_Validate_When_IsAsync_False(string whatToTest, bool result, bool? useHandler) + { + string handlerResult = null; + void successHandler(string s) { handlerResult = s; } + + MemberInfoParser.TryParse(o => o.Value, MemberTypes.Property, out MemberInfo propertyInfo); + + var validator = new TypeValidator(); + validator.SetValidation(o => o.MaximumLength(1), propertyInfo.Name); + + var processor = new PropertyValidationProcessor(o => o.Value, validator, useHandler == true ? successHandler : null); + + var (IsValid, Failures) = processor.Validate(new ObjWithNullable() { Value = whatToTest }); + Assert.That (IsValid, Is.EqualTo(result)); + if (!result) + { + Assert.That(Failures.Count, Is.EqualTo(1)); + } + else + { + Assert.That(handlerResult, Is.EqualTo(useHandler == true ? whatToTest : null)); + } + } + + [Test] + [TestCase("t")] + [TestCase("tt")] + public void Should_Throw_On_Validate_When_IsAsync_True(string whatToTest) + { + MemberInfoParser.TryParse(o => o.Value, MemberTypes.Property, out MemberInfo propertyInfo); + + var validator = new TypeAsyncValidator(); + validator.SetValidation(o => o.MaximumLength(1), propertyInfo.Name); + + var processor = new PropertyValidationProcessor(o => o.Value, validator, null); + + Assert.Throws(() => processor.Validate(new ObjWithNullable() { Value = whatToTest })); + } + + [Test] + [TestCase("t", true, false, true)] + [TestCase("t", true, true, true)] + [TestCase("tt", false, null, true)] + [TestCase("t", true, false, false)] + [TestCase("t", true, true, false)] + [TestCase("tt", false, null, false)] + public async Task Should_ValidateAsync_Do_Not_Depend_On_TypeValidator_Sync_Type(string whatToTest, bool result, bool? useHandler, bool isAsync) + { + string handlerResult = null; + void successHandler(string s) { handlerResult = s; } + + MemberInfoParser.TryParse(o => o.Value, MemberTypes.Property, out MemberInfo propertyInfo); + + TypeValidatorBase validator = isAsync ? new TypeAsyncValidator() : new TypeValidator(); + validator.SetValidation(o => o.MaximumLength(1), propertyInfo.Name); + + var processor = new PropertyValidationProcessor(o => o.Value, validator, useHandler == true ? successHandler : null); + + var (IsValid, Failures) = await processor.ValidateAsync(new ObjWithNullable() { Value = whatToTest }); + Assert.That(IsValid, Is.EqualTo(result)); + if (!result) + { + Assert.That(Failures.Count, Is.EqualTo(1)); + } + else + { + Assert.That(handlerResult, Is.EqualTo(useHandler == true ? whatToTest : null)); + } + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/QuickValidatorTests.cs b/tests/ExpressValidator.Tests.Net8/QuickValidatorTests.cs new file mode 100644 index 0000000..0091207 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/QuickValidatorTests.cs @@ -0,0 +1,584 @@ +using ExpressValidator.QuickValidation; +using FluentValidation; +using NUnit.Framework; +using System; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests.Net8 +{ + internal class QuickValidatorTests + { + [Test] + public void Should_Fail_WithExpectedPropertyName_When_ValidationFails_ForPrimitiveType_UsingOverload_WithPropertyName() + { + const int valueToTest = 5; + var result = QuickValidator.Validate(valueToTest, + (opt) => opt.GreaterThan(10) + .GreaterThan(15), + nameof(valueToTest)); + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(nameof(valueToTest))); + } + + [Test] + public async Task Should_Fail_WithExpectedPropertyName_When_AsyncValidationFails_ForPrimitiveType_UsingOverload_WithPropertyName() + { + const int valueToTest = 5; + var result = await QuickValidator.ValidateAsync(valueToTest, + (opt) => opt.GreaterThan(10) + .GreaterThan(15) + .MustAsync(async (_, __) => { await Task.Delay(1); return true; }), + nameof(valueToTest)); + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(nameof(valueToTest))); + } + + [Test] + public void Should_Fail_WithOverriddenPropertyName_When_ValidationFails_ForPrimitiveType_UsingOverload_WithPropertyName() + { + const int valueToTest = 5; + const string propName = "MyPropName"; + var result = QuickValidator.Validate(valueToTest, + (opt) => opt + .OverridePropertyName(propName) + .GreaterThan(10) + .GreaterThan(15), + nameof(valueToTest)); + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(propName)); + } + + [Test] + public async Task Should_Fail_WithOverriddenPropertyName_When_AsyncValidationFails_ForPrimitiveType_UsingOverload_WithPropertyName() + { + const int valueToTest = 5; + const string propName = "MyPropName"; + var result = await QuickValidator.ValidateAsync(valueToTest, + (opt) => opt + .OverridePropertyName(propName) + .GreaterThan(10) + .GreaterThan(15) + .MustAsync(async (_, __) => { await Task.Delay(1); return true; }), + nameof(valueToTest)); + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(propName)); + } + + [Test] + [TestCase(PropertyNameMode.Default)] + [TestCase(PropertyNameMode.TypeName)] + public void Should_Fail_WithOverriddenPropertyName_When_ValidationFails_ForPrimitiveType_UsingOverload_WithPropertyNameMode(PropertyNameMode mode) + { + const int valueToTest = 5; + const string propName = "MyPropName"; + var result = QuickValidator.Validate(valueToTest, + (opt) => opt + .OverridePropertyName(propName) + .GreaterThan(10) + .GreaterThan(15), + mode); + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(propName)); + Assert.That(result.Errors[1].PropertyName, Is.EqualTo(propName)); + } + + [Test] + [TestCase(PropertyNameMode.Default)] + [TestCase(PropertyNameMode.TypeName)] + public async Task Should_Fail_WithOverriddenPropertyName_When_AsyncValidationFails_ForPrimitiveType_UsingOverload_WithPropertyNameMode(PropertyNameMode mode) + { + const int valueToTest = 5; + const string propName = "MyPropName"; + var result = await QuickValidator.ValidateAsync(valueToTest, + (opt) => opt + .OverridePropertyName(propName) + .GreaterThan(10) + .GreaterThan(15) + .MustAsync(async (_, __) => { await Task.Delay(1); return true; }) + , + mode); + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(propName)); + Assert.That(result.Errors[1].PropertyName, Is.EqualTo(propName)); + } + + [Test] + [TestCase(PropertyNameMode.Default)] + [TestCase(PropertyNameMode.TypeName)] + public void Should_Fail_WithExpectedPropertyName_When_ValidationFails_ForPrimitiveType_UsingOverload_WithPropertyNameMode(PropertyNameMode mode) + { + const int valueToTest = 5; + var result = QuickValidator.Validate(valueToTest, + (opt) => opt.GreaterThan(10) + .GreaterThan(15), + mode); + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + if (mode == PropertyNameMode.Default) + { + Assert.That(result.Errors[0].PropertyName, Is.EqualTo("Input")); + } + else + { + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(typeof(int).Name)); + } + } + + [Test] + [TestCase(PropertyNameMode.Default)] + [TestCase(PropertyNameMode.TypeName)] + public async Task Should_Fail_WithExpectedPropertyName_When_AsyncValidationFails_ForPrimitiveType_UsingOverload_WithPropertyNameMode(PropertyNameMode mode) + { + const int valueToTest = 5; + var result = await QuickValidator.ValidateAsync(valueToTest, + (opt) => opt.GreaterThan(10) + .GreaterThan(15) + .MustAsync(async (_, __) => { await Task.Delay(1); return true; }) + , + mode); + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + if (mode == PropertyNameMode.Default) + { + Assert.That(result.Errors[0].PropertyName, Is.EqualTo("Input")); + } + else + { + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(typeof(int).Name)); + } + } + + [Test] + public void Should_Fail_WithExpectedPropertyName_When_ValidationFails_ForNonPrimitiveType_UsingOverload_WithPropertyName() + { + var objToQuick = new ObjWithTwoPublicProps() { I = -1, PercentValue1 = 101 }; + var rule = GetRule(); + + var result = QuickValidator.Validate(objToQuick, + rule, + nameof(objToQuick)); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(nameof(objToQuick) + "." + nameof(ObjWithTwoPublicProps.I))); + } + + [Test] + public async Task Should_Fail_WithExpectedPropertyName_When_AsyncValidationFails_ForNonPrimitiveType_UsingOverload_WithPropertyName() + { + var objToQuick = new ObjWithTwoPublicProps() { I = -1, PercentValue1 = 101 }; + var rule = GetAsyncRule(); + + var result = await QuickValidator.ValidateAsync(objToQuick, + rule, + nameof(objToQuick)); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(nameof(objToQuick) + "." + nameof(ObjWithTwoPublicProps.I))); + } + + [Test] + public void Should_Fail_WithOverriddenPropertyName_When_ValidationFails_ForNonPrimitiveType_UsingOverload_WithPropertyName() + { + var objToQuick = new ObjWithTwoPublicProps() { I = -1, PercentValue1 = 101 }; + var rule = GetRuleWithOverriddenPropertyName(); + + var result = QuickValidator.Validate(objToQuick, + rule, + nameof(objToQuick)); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(nameof(objToQuick) + ".MyPropNameI")); + } + + [Test] + public async Task Should_Fail_WithOverriddenPropertyName_When_AsyncValidationFails_ForNonPrimitiveType_UsingOverload_WithPropertyName() + { + var objToQuick = new ObjWithTwoPublicProps() { I = -1, PercentValue1 = 101 }; + var rule = GetAsyncRuleWithOverriddenPropertyName(); + + var result = await QuickValidator.ValidateAsync(objToQuick, + rule, + nameof(objToQuick)); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(nameof(objToQuick) + ".MyPropNameI")); + } + + [Test] + [TestCase(PropertyNameMode.Default)] + [TestCase(PropertyNameMode.TypeName)] + public void Should_Fail_WithOverriddenPropertyName_When_ValidationFails_ForNonPrimitiveType_UsingOverload_WithPropertyNameMode(PropertyNameMode mode) + { + var objToQuick = new ObjWithTwoPublicProps() { I = -1, PercentValue1 = 101 }; + var rule = GetRuleWithOverriddenPropertyName(); + + var result = QuickValidator.Validate(objToQuick, + rule, + mode); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + if (mode == PropertyNameMode.Default) + { + Assert.That(result.Errors[0].PropertyName, Is.EqualTo("Input.MyPropNameI")); + } + else + { + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(nameof(ObjWithTwoPublicProps) + ".MyPropNameI")); + } + } + + [Test] + [TestCase(PropertyNameMode.Default)] + [TestCase(PropertyNameMode.TypeName)] + public async Task Should_Fail_WithOverriddenPropertyName_When_AsyncValidationFails_ForNonPrimitiveType_UsingOverload_WithPropertyNameMode(PropertyNameMode mode) + { + var objToQuick = new ObjWithTwoPublicProps() { I = -1, PercentValue1 = 101 }; + var rule = GetAsyncRuleWithOverriddenPropertyName(); + + var result = await QuickValidator.ValidateAsync(objToQuick, + rule, + mode); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + if (mode == PropertyNameMode.Default) + { + Assert.That(result.Errors[0].PropertyName, Is.EqualTo("Input.MyPropNameI")); + } + else + { + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(nameof(ObjWithTwoPublicProps) + ".MyPropNameI")); + } + } + + [Test] + [TestCase(PropertyNameMode.Default)] + [TestCase(PropertyNameMode.TypeName)] + public void Should_Fail_WithExpectedPropertyName_When_ValidationFails_ForNonPrimitiveType_UsingOverload_WithPropertyNameMode(PropertyNameMode mode) + { + var objToQuick = new ObjWithTwoPublicProps() { I = -1, PercentValue1 = 101 }; + var rule = GetRule(); + + var result = QuickValidator.Validate(objToQuick, + rule, + mode); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + if (mode == PropertyNameMode.Default) + { + Assert.That(result.Errors[0].PropertyName, Is.EqualTo("Input." + nameof(ObjWithTwoPublicProps.I))); + } + else + { + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(nameof(ObjWithTwoPublicProps) + "." + nameof(ObjWithTwoPublicProps.I))); + } + } + + [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)] + public async Task Should_Fail_WithExpectedPropertyName_When_AsyncValidationFails_ForNonPrimitiveType_UsingOverload_WithPropertyNameMode(PropertyNameMode mode) + { + var objToQuick = new ObjWithTwoPublicProps() { I = -1, PercentValue1 = 101 }; + var rule = GetAsyncRule(); + + var result = await QuickValidator.ValidateAsync(objToQuick, + rule, + mode); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + if (mode == PropertyNameMode.Default) + { + Assert.That(result.Errors[0].PropertyName, Is.EqualTo("Input." + nameof(ObjWithTwoPublicProps.I))); + } + else + { + Assert.That(result.Errors[0].PropertyName, Is.EqualTo(nameof(ObjWithTwoPublicProps) + "." + nameof(ObjWithTwoPublicProps.I))); + } + } + + [Test] + public void Should_Pass_Validation_When_Valid() + { + const int valueToTest = 25; + var result = QuickValidator.Validate(valueToTest, + (opt) => opt.GreaterThan(10) + .InclusiveBetween(15, 25)); + Assert.That(result.IsValid, Is.True); + } + + [Test] + public async Task Should_Pass_AsyncValidation_When_Valid() + { + const int valueToTest = 25; + var result = await QuickValidator.ValidateAsync(valueToTest, + (opt) => opt.GreaterThan(10) + .InclusiveBetween(15, 25)); + Assert.That(result.IsValid, Is.True); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_Call_OnSuccess_When_Validation_Succeeds(bool isValid) + { + int valueFromHandler = 0; + int valueToTest; + if (isValid) + { + valueToTest = 25; + } + else + { + valueToTest = 5; + } + + var result = QuickValidator.Validate(valueToTest, + (opt) => opt.GreaterThan(10), + "vv", + (v) => valueFromHandler = v); + if (isValid) + { + Assert.That(result.IsValid, Is.True); + Assert.That(valueFromHandler, Is.EqualTo(25)); + } + else + { + Assert.That(result.IsValid, Is.False); + Assert.That(valueFromHandler, Is.EqualTo(0)); + } + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_Call_OnSuccess_When_ValidationAsync_Succeeds(bool isValid) + { + int valueFromHandler = 0; + int valueToTest; + if (isValid) + { + valueToTest = 25; + } + else + { + valueToTest = 5; + } + + var result = await QuickValidator.ValidateAsync(valueToTest, + (opt) => opt.GreaterThan(10), + "vv", + (v) => valueFromHandler = v); + if (isValid) + { + Assert.That(result.IsValid, Is.True); + Assert.That(valueFromHandler, Is.EqualTo(25)); + } + else + { + Assert.That(result.IsValid, Is.False); + Assert.That(valueFromHandler, Is.EqualTo(0)); + } + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_Call_OnSuccess_When_Validation_Succeeds_UsingOverload_WithPropertyNameMode(bool isValid) + { + int valueFromHandler = 0; + int valueToTest; + if (isValid) + { + valueToTest = 25; + } + else + { + valueToTest = 5; + } + + var result = QuickValidator.Validate(valueToTest, + (opt) => opt.GreaterThan(10), + PropertyNameMode.TypeName, + (v) => valueFromHandler = v); + if (isValid) + { + Assert.That(result.IsValid, Is.True); + Assert.That(valueFromHandler, Is.EqualTo(25)); + } + else + { + Assert.That(result.IsValid, Is.False); + Assert.That(valueFromHandler, Is.EqualTo(0)); + } + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_Call_OnSuccess_When_AsyncValidation_Succeeds_UsingOverload_WithPropertyNameMode(bool isValid) + { + int valueFromHandler = 0; + int valueToTest; + if (isValid) + { + valueToTest = 25; + } + else + { + valueToTest = 5; + } + + var result = await QuickValidator.ValidateAsync(valueToTest, + (opt) => opt.GreaterThan(10), + PropertyNameMode.TypeName, + (v) => valueFromHandler = v); + if (isValid) + { + Assert.That(result.IsValid, Is.True); + Assert.That(valueFromHandler, Is.EqualTo(25)); + } + else + { + Assert.That(result.IsValid, Is.False); + Assert.That(valueFromHandler, Is.EqualTo(0)); + } + } + + private static Action> GetRule() + { + return (opt) => + opt + .ChildRules((v) => v.RuleFor(o => o.I) + .GreaterThan(0)) + .ChildRules((v) => v.RuleFor(o => o.PercentValue1) + .InclusiveBetween(0, 100)); + } + + 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) => + opt + .ChildRules((v) => v.RuleFor(o => o.I) + .GreaterThan(0)) + .MustAsync(async (_, __) => { await Task.Delay(1); return true; }) + .ChildRules((v) => v.RuleFor(o => o.PercentValue1) + .InclusiveBetween(0, 100) + .MustAsync(async (_, __) => { await Task.Delay(1); return true; }) + ); + } + + private static Action> GetRuleWithOverriddenPropertyName() + { + return (opt) => + opt + .ChildRules((v) => v.RuleFor(o => o.I) + .GreaterThan(0).OverridePropertyName("MyPropNameI")) + .ChildRules((v) => v.RuleFor(o => o.PercentValue1) + .InclusiveBetween(0, 100)); + } + + private static Action> GetAsyncRuleWithOverriddenPropertyName() + { + return (opt) => + opt + .ChildRules((v) => v.RuleFor(o => o.I) + .GreaterThan(0).OverridePropertyName("MyPropNameI")) + .MustAsync(async (_, __) => { await Task.Delay(1); return true; }) + .ChildRules((v) => v.RuleFor(o => o.PercentValue1) + .InclusiveBetween(0, 100) + .MustAsync(async (_, __) => { await Task.Delay(1); return true; }) + ); + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.ForNullOrEmptyValidator.cs b/tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.ForNullOrEmptyValidator.cs new file mode 100644 index 0000000..83f1ec3 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.ForNullOrEmptyValidator.cs @@ -0,0 +1,100 @@ +using FluentValidation; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests.Net8 +{ + internal partial class TypeAsyncValidatorTests + { + [Test] + [TestCase(true, null, true)] + [TestCase(false, null, true)] + [TestCase(true, "t", false)] + [TestCase(false, "t", false)] + public async Task Should_OnlyNullValidation_Be_Corect_ForNull_And_NotNull(bool single, string valueToTest, bool isValid) + { + var validator = new TypeAsyncValidator(); + if (single) + validator.SetValidation(o => o.Null(), "someprop"); + else + validator.SetValidation(o => o.Null().Null(), "someprop"); + + var (IsValid, _) = await validator.ValidateExAsync(valueToTest); + ClassicAssert.AreEqual(isValid, IsValid); + } + + [Test] + [TestCase(true, null, true)] + [TestCase(false, null, true)] + [TestCase(true, 1, false)] + [TestCase(false, 1, false)] + public async Task Should_OnlyNullValidation_Be_Corect_ForNull_And_NotNull_NullableValue(bool single, int? valueToTest, bool isValid) + { + var validator = new TypeAsyncValidator(); + if (single) + validator.SetValidation(o => o.Null(), "someprop"); + else + validator.SetValidation(o => o.Null().Null(), "someprop"); + + var (IsValid, _) = await validator.ValidateExAsync(valueToTest); + ClassicAssert.AreEqual(isValid, IsValid); + } + + [Test] + [TestCase(true, null, true)] + [TestCase(false, null, true)] + [TestCase(true, "t", false)] + [TestCase(false, "t", false)] + public async Task Should_OnlyEmptyValidation_Be_Corect_ForNull_And_NotNull(bool single, string valueToTest, bool isValid) + { + var validator = new TypeAsyncValidator(); + if (single) + validator.SetValidation(o => o.Empty(), string.Empty); + else + validator.SetValidation(o => o.Empty().Empty(), string.Empty); + + var (IsValid, _) = await validator.ValidateExAsync(valueToTest); + ClassicAssert.AreEqual(isValid, IsValid); + } + + [Test] + [TestCase(true, null, true)] + [TestCase(false, null, true)] + [TestCase(true, 1, false)] + [TestCase(false, 1, false)] + public async Task Should_OnlyEmptyValidation_Be_Corect_ForNull_And_NotNull_For_NullableValue(bool single, int? valueToTest, bool isValid) + { + var validator = new TypeAsyncValidator(); + if (single) + validator.SetValidation(o => o.Empty(), string.Empty); + else + validator.SetValidation(o => o.Empty().Empty(), string.Empty); + + var (IsValid, _) = await validator.ValidateExAsync(valueToTest); + ClassicAssert.AreEqual(isValid, IsValid); + } + + [Test] + [TestCase(null, true)] + [TestCase("t", false)] + public async Task Should_NullAndEmptyValidation_Be_Corect_ForNull_And_NotNull(string valueToTest, bool isValid) + { + var validator = new TypeAsyncValidator(); + validator.SetValidation(o => o.Empty().Null().Null().Empty(), "someprop"); + var (IsValid, _) = await validator.ValidateExAsync(valueToTest); + ClassicAssert.AreEqual(isValid, IsValid); + } + + [Test] + [TestCase(null, true)] + [TestCase(1, false)] + public async Task Should_NullAndEmptyValidation_Be_Corect_ForNull_And_NotNull_For_NullableValue(int? valueToTest, bool isValid) + { + var validator = new TypeAsyncValidator(); + validator.SetValidation(o => o.Empty().Null().Null().Empty(), "someprop"); + var (IsValid, _) = await validator.ValidateExAsync(valueToTest); + ClassicAssert.AreEqual(isValid, IsValid); + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.ForNullOrEmptyWithOtherValidator.cs b/tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.ForNullOrEmptyWithOtherValidator.cs new file mode 100644 index 0000000..6272f0d --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.ForNullOrEmptyWithOtherValidator.cs @@ -0,0 +1,122 @@ +using FluentValidation; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests.Net8 +{ + internal partial class TypeAsyncValidatorTests + { + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_NotOnlyNullValidation_Be_NotValid_ForNullValue(bool single) + { + var validator = new TypeAsyncValidator(); + if (single) + validator.SetValidation(o => o.Null().MinimumLength(1), "someprop"); + else + validator.SetValidation(o => o.Null().Null().MinimumLength(1), "someprop"); + + var (IsValid, Failures) = await validator.ValidateExAsync(null); + ClassicAssert.AreEqual(false, IsValid); + ClassicAssert.AreEqual(1, Failures.Count); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_NotOnlyNullValidation_Be_NotValid_ForNullValue_For_NullableValue(bool single) + { + var validator = new TypeAsyncValidator(); + if (single) + validator.SetValidation(o => o.Null().GreaterThan(1), "someprop"); + else + validator.SetValidation(o => o.Null().Null().GreaterThan(1), "someprop"); + + var (IsValid, Failures) = await validator.ValidateExAsync(null); + ClassicAssert.AreEqual(false, IsValid); + ClassicAssert.AreEqual(1, Failures.Count); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_NotOnlyEmptyValidation_Be_NotValid_ForNullValue(bool single) + { + var validator = new TypeAsyncValidator(); + if (single) + validator.SetValidation(o => o.Empty().MinimumLength(1), "someprop"); + else + validator.SetValidation(o => o.Empty().Empty().MinimumLength(1), "someprop"); + + var (IsValid, Failures) = await validator.ValidateExAsync(null); + ClassicAssert.AreEqual(false, IsValid); + ClassicAssert.AreEqual(1, Failures.Count); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_NotOnlyEmptyValidation_Be_NotValid_ForNullValue_For_NullableValue(bool single) + { + var validator = new TypeAsyncValidator(); + if (single) + validator.SetValidation(o => o.Empty().GreaterThan(1), "someprop"); + else + validator.SetValidation(o => o.Empty().Empty().GreaterThan(1), "someprop"); + + var (IsValid, Failures) = await validator.ValidateExAsync(null); + ClassicAssert.AreEqual(false, IsValid); + ClassicAssert.AreEqual(1, Failures.Count); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_NotOnlyEmptyValidation_Be_NotValid_ForNullValue_For_RefProperty_IfEmptyValidators_AreLast(bool single) + { + var validator = new TypeAsyncValidator(); + if (single) + validator.SetValidation(o => o.MinimumLength(1).Empty(), "someprop"); + else + validator.SetValidation(o => o.MinimumLength(1).Empty().Empty(), "someprop"); + + var (IsValid, Failures) = await validator.ValidateExAsync(null); + ClassicAssert.AreEqual(false, IsValid); + ClassicAssert.AreEqual(1, Failures.Count); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_NotOnlyEmptyValidation_Be_NotValid_ForDefaultValue_For_ValueProperty(bool single) + { + var validator = new TypeAsyncValidator(); + if (single) + validator.SetValidation(o => o.Empty().GreaterThan(1), "someprop"); + else + validator.SetValidation(o => o.Empty().Empty().GreaterThan(1), "someprop"); + + var (IsValid, Failures) = await validator.ValidateExAsync(0); + ClassicAssert.AreEqual(false, IsValid); + ClassicAssert.AreEqual(1, Failures.Count); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Should_NotOnlyEmptyValidation_Be_NotValid_ForDefaultValue_For_NullableValueProperty(bool single) + { + var validator = new TypeAsyncValidator(); + if (single) + validator.SetValidation(o => o.Empty().GreaterThan(1), "someprop"); + else + validator.SetValidation(o => o.Empty().Empty().GreaterThan(1), "someprop"); + + var (IsValid, Failures) = await validator.ValidateExAsync(0); + ClassicAssert.AreEqual(false, IsValid); + ClassicAssert.AreEqual(single ? 2 : 3, Failures.Count); + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.cs b/tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.cs new file mode 100644 index 0000000..1b70284 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.cs @@ -0,0 +1,38 @@ +using NUnit.Framework; +using System.Threading.Tasks; +using FluentValidation; +using System.Reflection; +using NUnit.Framework.Legacy; + +namespace ExpressValidator.Tests.Net8 +{ + internal partial class TypeAsyncValidatorTests + { + [Test] + [TestCase("t", true)] + [TestCase("tt", false)] + public async Task Should_ValidateAsync_ForUsualRules_Work(string whatToTest, bool result) + { + MemberInfoParser.TryParse(o => o.Value, MemberTypes.Property, out MemberInfo propertyInfo); + + var validator = new TypeAsyncValidator(); + validator.SetValidation(o => o.MaximumLength(1), propertyInfo.Name); + var res = await validator.ValidateAsync(whatToTest); + ClassicAssert.AreEqual(result, res.IsValid); + if (!result) + { + ClassicAssert.AreEqual(1, res.Errors.Count); + } + } + + [Test] + public void Should_Validate_Throw() + { + MemberInfoParser.TryParse(o => o.Value, MemberTypes.Property, out MemberInfo propertyInfo); + + var validator = new TypeAsyncValidator(); + validator.SetValidation(o => o.MaximumLength(1), propertyInfo.Name); + Assert.Throws(() => validator.Validate("t")) ; + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/TypeValidatorTests.ForNullOrEmptyValidator.cs b/tests/ExpressValidator.Tests.Net8/TypeValidatorTests.ForNullOrEmptyValidator.cs new file mode 100644 index 0000000..82f9ec2 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/TypeValidatorTests.ForNullOrEmptyValidator.cs @@ -0,0 +1,69 @@ +using FluentValidation; +using NUnit.Framework; +using NUnit.Framework.Legacy; + +namespace ExpressValidator.Tests.Net8 +{ + internal partial class TypeValidatorTests + { + [Test] + [TestCase(true, null, true)] + [TestCase(false, null, true)] + [TestCase(true, "t", false)] + [TestCase(false, "t", false)] + public void Should_OnlyNullValidation_Be_Corect_ForNull_And_NotNull(bool single, string valueToTest, bool isValid) + { + var validator = new TypeValidator(); + if (single) + validator.SetValidation(o => o.Null(), "someprop"); + else + validator.SetValidation(o => o.Null().Null(), "someprop"); + + var (IsValid, _) = validator.ValidateEx(valueToTest); + ClassicAssert.AreEqual(isValid, IsValid); + } + + [Test] + [TestCase(true, null, true)] + [TestCase(false, null, true)] + [TestCase(true, "t", false)] + [TestCase(false, "t", false)] + public void Should_OnlyEmptyValidation_Be_Corect_ForNull_And_NotNull(bool single, string valueToTest, bool isValid) + { + var validator = new TypeValidator(); + if (single) + validator.SetValidation(o => o.Empty(), "someprop"); + else + validator.SetValidation(o => o.Empty().Empty(), "someprop"); + + var (IsValid, _) = validator.ValidateEx(valueToTest); + ClassicAssert.AreEqual(isValid, IsValid); + } + + [Test] + [TestCase(null, true)] + [TestCase("t", false)] + public void Should_NullAndEmptyValidation_Be_Corect_ForNull_And_NotNull(string valueToTest, bool isValid) + { + var validator = new TypeValidator(); + validator.SetValidation(o => o.Empty().Null().Null().Empty(), "someprop"); + var (IsValid, _) = validator.ValidateEx(valueToTest); + ClassicAssert.AreEqual(isValid, IsValid); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_NotOnlyNullValidation_Be_NotValid_ForNullValue(bool single) + { + var validator = new TypeValidator(); + if (single) + validator.SetValidation(o => o.Null().MinimumLength(1), "someprop"); + else + validator.SetValidation(o => o.Null().Null().MinimumLength(1), "someprop"); + + var (IsValid, _) = validator.ValidateEx(null!); + ClassicAssert.AreEqual(false, IsValid); + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/TypeValidatorTests.ForNullOrEmptyWithOtherValidator.cs b/tests/ExpressValidator.Tests.Net8/TypeValidatorTests.ForNullOrEmptyWithOtherValidator.cs new file mode 100644 index 0000000..095a370 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/TypeValidatorTests.ForNullOrEmptyWithOtherValidator.cs @@ -0,0 +1,78 @@ +using FluentValidation; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests.Net8 +{ + internal partial class TypeValidatorTests + { + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_NotOnlyEmptyValidation_Be_NotValid_ForNullValue_For_RefProperty(bool single) + { + var validator = new TypeValidator(); + if (single) + validator.SetValidation(o => o.Empty().MinimumLength(1), "someprop"); + else + validator.SetValidation(o => o.Empty().Empty().MinimumLength(1), "someprop"); + + var (IsValid, Failures) = validator.ValidateEx(null); + ClassicAssert.AreEqual(false, IsValid); + ClassicAssert.AreEqual(1, Failures.Count); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_NotOnlyEmptyValidation_Be_NotValid_ForNullValue_For_RefProperty_IfEmptyValidators_AreLast(bool single) + { + var validator = new TypeValidator(); + if (single) + validator.SetValidation(o => o.MinimumLength(1).Empty(), "someprop"); + else + validator.SetValidation(o => o.MinimumLength(1).Empty().Empty(), "someprop"); + + var (IsValid, Failures) = validator.ValidateEx(null); + ClassicAssert.AreEqual(false, IsValid); + ClassicAssert.AreEqual(1, Failures.Count); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_NotOnlyNullValidation_Be_NotValid_ForNullValue_For_RefProperty(bool single) + { + var validator = new TypeValidator(); + if (single) + validator.SetValidation(o => o.Null().MinimumLength(1), "someprop"); + else + validator.SetValidation(o => o.Null().Null().MinimumLength(1), "someprop"); + + var (IsValid, Failures) = validator.ValidateEx(null); + ClassicAssert.AreEqual(false, IsValid); + ClassicAssert.AreEqual(1, Failures.Count); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void Should_NotOnlyEmptyValidation_Be_NotValid_ForDefaultValue_For_ValueProperty(bool single) + { + var validator = new TypeValidator(); + if (single) + validator.SetValidation(o => o.Empty().GreaterThan(1), "someprop"); + else + validator.SetValidation(o => o.Empty().Empty().GreaterThan(1), "someprop"); + + var (IsValid, Failures) = validator.ValidateEx(0); + ClassicAssert.AreEqual(false, IsValid); + ClassicAssert.AreEqual(1, Failures.Count); + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/TypeValidatorTests.cs b/tests/ExpressValidator.Tests.Net8/TypeValidatorTests.cs new file mode 100644 index 0000000..633fd87 --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/TypeValidatorTests.cs @@ -0,0 +1,45 @@ +using FluentValidation; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using System.Reflection; +using System.Threading.Tasks; + +namespace ExpressValidator.Tests.Net8 +{ + internal partial class TypeValidatorTests + { + [Test] + [TestCase("t", true)] + [TestCase("tt", false)] + public void Should_Validate_ForUsualRules_Work(string whatToTest, bool result) + { + MemberInfoParser.TryParse(o => o.Value, MemberTypes.Property, out MemberInfo propertyInfo); + + var validator = new TypeValidator(); + validator.SetValidation(o => o.MaximumLength(1), propertyInfo.Name); + var res = validator.Validate(whatToTest); + ClassicAssert.AreEqual(result, res.IsValid); + if (!result) + { + ClassicAssert.AreEqual(1, res.Errors.Count); + } + } + + [Test] + [TestCase("t", true)] + [TestCase("tt", false)] + public async Task Should_ValidateAsync_ForUsualRules_Work(string whatToTest, bool result) + { + MemberInfoParser.TryParse(o => o.Value, MemberTypes.Property, out MemberInfo propertyInfo); + + var validator = new TypeValidator(); + validator.SetValidation(o => o.MaximumLength(1), propertyInfo.Name); + var res = await validator.ValidateAsync(whatToTest); + ClassicAssert.AreEqual(result, res.IsValid); + if (!result) + { + ClassicAssert.AreEqual(1, res.Errors.Count); + } + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/UtilitiesTests.cs b/tests/ExpressValidator.Tests.Net8/UtilitiesTests.cs new file mode 100644 index 0000000..6e55c1a --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/UtilitiesTests.cs @@ -0,0 +1,109 @@ +using NUnit.Framework; +using System.Reflection; + +namespace ExpressValidator.Tests.Net8 +{ + internal class UtilitiesTests + { + [Test] + public void Should_PropertyInfoParser_TryParse_Work_Correctly() + { + var resParseS = MemberInfoParser.TryParse(s => s.S!, MemberTypes.Property, out MemberInfo propertyInfoS); + Assert.That(resParseS, Is.True); + Assert.That(propertyInfoS.Name, Is.EqualTo("S")); + + var resParseI = MemberInfoParser.TryParse(s => s.I, MemberTypes.Property, out MemberInfo propertyInfoI); + Assert.That(resParseI, Is.True); + Assert.That(propertyInfoI.Name, Is.EqualTo("I")); + + var resParse = MemberInfoParser.TryParse(s => s, MemberTypes.Property, out MemberInfo propertyInfo); + Assert.That(resParse, Is.False); + } + + [Test] + public void Should_PropertyInfoParser_TryParse_For_MemberInfo_Work_Correctly() + { + var resParseS = MemberInfoParser.TryParse(s => s.S!, MemberTypes.Property, out MemberInfo memberInfoS); + Assert.That(resParseS, Is.True); + Assert.That(memberInfoS.Name, Is.EqualTo("S")); + Assert.That(memberInfoS.MemberType, Is.EqualTo(MemberTypes.Property)); + + var resParseI = MemberInfoParser.TryParse(s => s.I, MemberTypes.Property, out MemberInfo memberInfoI); + Assert.That(resParseI, Is.True); + Assert.That(memberInfoI.Name, Is.EqualTo("I")); + Assert.That(memberInfoS.MemberType, Is.EqualTo(MemberTypes.Property)); + + var resParseField = MemberInfoParser.TryParse(s => s._sField!, MemberTypes.Field, out MemberInfo memberInfoField); + Assert.That(resParseField, Is.True); + Assert.That(memberInfoField.Name, Is.EqualTo("_sField")); + Assert.That(memberInfoField.MemberType, Is.EqualTo(MemberTypes.Field)); + } + + [Test] + public void Should_GetTypedValue_Work() + { + var objToTest = new ObjWithTwoPublicProps() { I = 1, S = "TestProp", _sField = "TestField" }; + + _ = MemberInfoParser.TryParse(s => s.S!, MemberTypes.Property, out MemberInfo memberInfoS); + Assert.That(memberInfoS.GetTypedValue(objToTest), Is.EqualTo("TestProp")); + + _ = MemberInfoParser.TryParse(s => s.I, MemberTypes.Property, out MemberInfo memberInfoI); + Assert.That(memberInfoI.GetTypedValue(objToTest), Is.EqualTo(1)); + + _ = MemberInfoParser.TryParse(s => s._sField!, MemberTypes.Field, out MemberInfo memberInfoF); + Assert.That(memberInfoF.GetTypedValue(objToTest), Is.EqualTo("TestField")); + } + + [Test] + public void Should_ReturnFalse_ForNonNullableValueType_WhenCheckingIsValueNull() + { + Assert.That(TypeTraits.CanBeNull, Is.False); + } + + [Test] + public void Should_ReturnTrue_ForNullableValueType_WhenCheckingIsValueNull_AndValueIsNull() + { + Assert.That(TypeTraits.CanBeNull, Is.True); + } + + [Test] + public void Should_ReturnTrue_ForReferenceType_WhenCheckingIsValueNull_AndValueIsNull() + { + Assert.That(TypeTraits.CanBeNull, Is.True); + } + + [Test] + public void Should_TypeHelper_ReturnFalse_ForNonNullableValueType_WhenCheckingIsValueNull() + { + Assert.That(TypeHelper.IsNull(0), Is.False); + } + + [Test] + public void Should_TypeHelper_ReturnTrue_ForNullableValueType_WhenCheckingIsValueNull_AndValueIsNull() + { + int? value = null; + Assert.That(TypeHelper.IsNull(value), Is.True); + } + + [Test] + public void Should_TypeHelper_ReturnFalse_ForNullableValueType_WhenCheckingIsValueNull_AndValueIsNotNull() + { + int? value = 5; + Assert.That(TypeHelper.IsNull(value), Is.False); + } + + [Test] + public void Should_TypeHelper_ReturnTrue_ForReferenceType_WhenCheckingIsValueNull_AndValueIsNull() + { + string? value = null; + Assert.That(TypeHelper.IsNull(value!), Is.True); + } + + [Test] + public void Should_TypeHelper_ReturnFalse_ForReferenceType_WhenCheckingIsValueNull_AndValueIsNotNull() + { + string value = "hello"; + Assert.That(TypeHelper.IsNull(value), Is.False); + } + } +} diff --git a/tests/ExpressValidator.Tests.Net8/ValidationProfileTests.cs b/tests/ExpressValidator.Tests.Net8/ValidationProfileTests.cs new file mode 100644 index 0000000..b6f393e --- /dev/null +++ b/tests/ExpressValidator.Tests.Net8/ValidationProfileTests.cs @@ -0,0 +1,31 @@ +using NUnit.Framework; + +namespace ExpressValidator.Tests.Net8 +{ + [TestFixture] + internal class ValidationProfileTests + { + [Test] + public void Should_AllowDerivedClassesToConfigureValidator_WhenCreatingValidator() + { + // Arrange + var customProfile = new CustomValidatorProfile(); + + // Act + customProfile.Configure(new ExpressValidatorBuilder()); + + // Assert + Assert.That(customProfile.CustomConfigurationWasApplied, Is.True); + } + } + + internal class CustomValidatorProfile : ValidationProfile + { + public bool CustomConfigurationWasApplied { get; private set; } + + public override void Configure(ExpressValidatorBuilder expressValidatorBuilder) + { + CustomConfigurationWasApplied = true; + } + } +} From c380303a40ceff4edbb8444b96f9a3d10e98e329 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 6 Mar 2026 13:31:29 +0300 Subject: [PATCH 22/28] Update NuGet.md --- src/ExpressValidator/docs/NuGet.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ExpressValidator/docs/NuGet.md b/src/ExpressValidator/docs/NuGet.md index dfa8de4..a064fc3 100644 --- a/src/ExpressValidator/docs/NuGet.md +++ b/src/ExpressValidator/docs/NuGet.md @@ -8,7 +8,8 @@ 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 `QuickValidator`, with built-in tolerance for `null` values. +- Provides quick and easy validation using `QuickValidator`. +- Built-in `null` tolerance - `null` root instances fail validation instead of throwing exceptions. - Supports asynchronous validation. - Targets .NET Standard 2.0+ From f20e8bbc59c1abeec0d4824eaedd775cbd3cf1b2 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Fri, 6 Mar 2026 13:43:43 +0300 Subject: [PATCH 23/28] Fix test issues in ExpressValidator.Tests.Net8. --- .../TypeAsyncValidatorTests.ForNullOrEmptyValidator.cs | 8 +++----- .../TypeValidatorTests.ForNullOrEmptyValidator.cs | 7 +++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.ForNullOrEmptyValidator.cs b/tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.ForNullOrEmptyValidator.cs index 83f1ec3..23e845f 100644 --- a/tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.ForNullOrEmptyValidator.cs +++ b/tests/ExpressValidator.Tests.Net8/TypeAsyncValidatorTests.ForNullOrEmptyValidator.cs @@ -1,7 +1,5 @@ using FluentValidation; -using NUnit.Framework; using NUnit.Framework.Legacy; -using System.Threading.Tasks; namespace ExpressValidator.Tests.Net8 { @@ -12,7 +10,7 @@ internal partial class TypeAsyncValidatorTests [TestCase(false, null, true)] [TestCase(true, "t", false)] [TestCase(false, "t", false)] - public async Task Should_OnlyNullValidation_Be_Corect_ForNull_And_NotNull(bool single, string valueToTest, bool isValid) + public async Task Should_OnlyNullValidation_Be_Corect_ForNull_And_NotNull(bool single, string? valueToTest, bool isValid) { var validator = new TypeAsyncValidator(); if (single) @@ -46,7 +44,7 @@ public async Task Should_OnlyNullValidation_Be_Corect_ForNull_And_NotNull_Nullab [TestCase(false, null, true)] [TestCase(true, "t", false)] [TestCase(false, "t", false)] - public async Task Should_OnlyEmptyValidation_Be_Corect_ForNull_And_NotNull(bool single, string valueToTest, bool isValid) + public async Task Should_OnlyEmptyValidation_Be_Corect_ForNull_And_NotNull(bool single, string? valueToTest, bool isValid) { var validator = new TypeAsyncValidator(); if (single) @@ -78,7 +76,7 @@ public async Task Should_OnlyEmptyValidation_Be_Corect_ForNull_And_NotNull_For_N [Test] [TestCase(null, true)] [TestCase("t", false)] - public async Task Should_NullAndEmptyValidation_Be_Corect_ForNull_And_NotNull(string valueToTest, bool isValid) + public async Task Should_NullAndEmptyValidation_Be_Corect_ForNull_And_NotNull(string? valueToTest, bool isValid) { var validator = new TypeAsyncValidator(); validator.SetValidation(o => o.Empty().Null().Null().Empty(), "someprop"); diff --git a/tests/ExpressValidator.Tests.Net8/TypeValidatorTests.ForNullOrEmptyValidator.cs b/tests/ExpressValidator.Tests.Net8/TypeValidatorTests.ForNullOrEmptyValidator.cs index 82f9ec2..94322d0 100644 --- a/tests/ExpressValidator.Tests.Net8/TypeValidatorTests.ForNullOrEmptyValidator.cs +++ b/tests/ExpressValidator.Tests.Net8/TypeValidatorTests.ForNullOrEmptyValidator.cs @@ -1,5 +1,4 @@ using FluentValidation; -using NUnit.Framework; using NUnit.Framework.Legacy; namespace ExpressValidator.Tests.Net8 @@ -11,7 +10,7 @@ internal partial class TypeValidatorTests [TestCase(false, null, true)] [TestCase(true, "t", false)] [TestCase(false, "t", false)] - public void Should_OnlyNullValidation_Be_Corect_ForNull_And_NotNull(bool single, string valueToTest, bool isValid) + public void Should_OnlyNullValidation_Be_Corect_ForNull_And_NotNull(bool single, string? valueToTest, bool isValid) { var validator = new TypeValidator(); if (single) @@ -28,7 +27,7 @@ public void Should_OnlyNullValidation_Be_Corect_ForNull_And_NotNull(bool single, [TestCase(false, null, true)] [TestCase(true, "t", false)] [TestCase(false, "t", false)] - public void Should_OnlyEmptyValidation_Be_Corect_ForNull_And_NotNull(bool single, string valueToTest, bool isValid) + public void Should_OnlyEmptyValidation_Be_Corect_ForNull_And_NotNull(bool single, string? valueToTest, bool isValid) { var validator = new TypeValidator(); if (single) @@ -43,7 +42,7 @@ public void Should_OnlyEmptyValidation_Be_Corect_ForNull_And_NotNull(bool single [Test] [TestCase(null, true)] [TestCase("t", false)] - public void Should_NullAndEmptyValidation_Be_Corect_ForNull_And_NotNull(string valueToTest, bool isValid) + public void Should_NullAndEmptyValidation_Be_Corect_ForNull_And_NotNull(string? valueToTest, bool isValid) { var validator = new TypeValidator(); validator.SetValidation(o => o.Empty().Null().Null().Empty(), "someprop"); From 8797ddd95406c7b4d1759f4c33c9cee8a9672a41 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Sun, 8 Mar 2026 11:26:24 +0300 Subject: [PATCH 24/28] Update NuGet.md and README.md. --- README.md | 3 ++- src/ExpressValidator/docs/NuGet.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e2f329..9bd4617 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ 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 `QuickValidator`, with built-in tolerance for `null` values. +- Built-in `null` tolerance - `null` root instances fail validation instead of throwing exceptions. +- Quick and easy validation with `QuickValidator`, with robust support for `null` values. - Supports asynchronous validation. - Targets .NET Standard 2.0+ diff --git a/src/ExpressValidator/docs/NuGet.md b/src/ExpressValidator/docs/NuGet.md index a064fc3..2dffa56 100644 --- a/src/ExpressValidator/docs/NuGet.md +++ b/src/ExpressValidator/docs/NuGet.md @@ -8,8 +8,8 @@ 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 `QuickValidator`. - Built-in `null` tolerance - `null` root instances fail validation instead of throwing exceptions. +- Quick and easy validation with `QuickValidator`, with robust support for `null` values. - Supports asynchronous validation. - Targets .NET Standard 2.0+ From 50fa99cc3e15a0ea08bd3e0cb3cdb6b768cf86b4 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Mon, 9 Mar 2026 23:00:03 +0300 Subject: [PATCH 25/28] Refactor `ExpressValidator`. --- src/ExpressValidator/ExpressValidator.TOptions.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ExpressValidator/ExpressValidator.TOptions.cs b/src/ExpressValidator/ExpressValidator.TOptions.cs index 93fc22d..86ebcd1 100644 --- a/src/ExpressValidator/ExpressValidator.TOptions.cs +++ b/src/ExpressValidator/ExpressValidator.TOptions.cs @@ -14,18 +14,19 @@ namespace ExpressValidator /// public class ExpressValidator : IExpressValidator { - private readonly IEnumerable> _validators; private readonly OnFirstPropertyValidatorFailed _validationMode; + private readonly IEnumerable> _validators; internal ExpressValidator(TOptions options, IEnumerable> validators, OnFirstPropertyValidatorFailed validationMode) { - _validators = validators; _validationMode = validationMode; - foreach (var validator in _validators) + foreach (var validator in validators) { validator.ApplyOptions(options); } + + _validators = validators; } public ValidationResult Validate(TObj obj) From 091cbcbbc1dcc70e8e7ecae450bdd1892bf64e83 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 10 Mar 2026 15:37:47 +0300 Subject: [PATCH 26/28] Refactor `ExpressValidatorBuilder.Build` to remove dependency on `ExpressValidator`. --- .../ExpressValidatorBuilder.TOptions.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ExpressValidator/ValidatorBuilders/ExpressValidatorBuilder.TOptions.cs b/src/ExpressValidator/ValidatorBuilders/ExpressValidatorBuilder.TOptions.cs index 5e84000..0e296d2 100644 --- a/src/ExpressValidator/ValidatorBuilders/ExpressValidatorBuilder.TOptions.cs +++ b/src/ExpressValidator/ValidatorBuilders/ExpressValidatorBuilder.TOptions.cs @@ -61,8 +61,13 @@ public IBuilderWithPropValidator AddFunc(Func fun /// /// public IExpressValidator Build(TOptions options) - { - return new ExpressValidator(options, _objectValidators, _validationMode); + { + foreach (var validator in _objectValidators) + { + validator.ApplyOptions(options); + } + + return new ExpressValidator(_objectValidators, _validationMode); } internal void AddValidator(IObjectValidator objectValidator) From 2d2c22fa97f98326c3961ff41113adb8f09b5774 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Tue, 10 Mar 2026 15:42:08 +0300 Subject: [PATCH 27/28] Deprecate the `ExpressValidator` class. --- src/ExpressValidator/ExpressValidator.TOptions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ExpressValidator/ExpressValidator.TOptions.cs b/src/ExpressValidator/ExpressValidator.TOptions.cs index 86ebcd1..e94984f 100644 --- a/src/ExpressValidator/ExpressValidator.TOptions.cs +++ b/src/ExpressValidator/ExpressValidator.TOptions.cs @@ -12,6 +12,9 @@ namespace ExpressValidator /// /// /// +#pragma warning disable S1133 // Deprecated code should be removed + [Obsolete("The ExpressValidator class is obsolete and will be removed in a future version.")] +#pragma warning restore S1133 // Deprecated code should be removed public class ExpressValidator : IExpressValidator { private readonly OnFirstPropertyValidatorFailed _validationMode; From 64b6bf3c62f07f7e9cee98310c8ca8772f451488 Mon Sep 17 00:00:00 2001 From: kolan72 Date: Wed, 11 Mar 2026 18:05:07 +0300 Subject: [PATCH 28/28] Package 0.15.0 version and update CHANGELOG.md. --- CHANGELOG.md | 20 ++++++++++++++++++++ src/ExpressValidator/ExpressValidator.csproj | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 740112a..dce46f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## 0.15.0 + +- Introduced validation failure for `null` root instances instead of throwing FluentValidation exceptions. +- Introduce abstract `ValidationProfile` class. +- Added an optional `Action` `onSuccessValidation` parameter to `ExpressValidatorBuilder.AddFunc` for handling successful validation of `Func` result. +- Refactor `ExpressValidatorBuilder.Build` to remove dependency on `ExpressValidator`. +- Deprecate the `ExpressValidator` class. +- Add internal `PropertyValidationProcessor` class. +- Switch to using `PropertyValidationProcessor` in `ExpressPropertyValidator` and `ExpressPropertyValidator`. +- Refactor `ExpressPropertyValidator` to set `IsAsync` in the constructor. +- Add internal utility `TypeHelper` class with `IsNull` method and use it in `TypeValidatorBase`. +- Introduce internal utility `TypeTraits` class. +- Update to FluentValidation 12.1.1. +- Add internal static `ValidationFallbackProvider` class representing validation failure for a `null` instance. +- Add ExpressValidator.Tests.Net8.csproj for testing ExpressValidator when TargetFramework is net8.0. +- Update tests to System.ValueTuple 4.6.2. +- Improve test coverage for null handling. +- Update NuGet.md and README.md. + + ## 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. diff --git a/src/ExpressValidator/ExpressValidator.csproj b/src/ExpressValidator/ExpressValidator.csproj index 316654f..3f8ecc6 100644 --- a/src/ExpressValidator/ExpressValidator.csproj +++ b/src/ExpressValidator/ExpressValidator.csproj @@ -3,7 +3,7 @@ netstandard2.0;net8.0 true - 0.12.2 + 0.15.0 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.2.0 + 0.15.0.0 0.0.0.0