diff --git a/CHANGELOG.md b/CHANGELOG.md index 5563acf..6dc581c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.10.0 + +- Introduced the `QuickValidator.ValidateAsync(T, Action>, string, Action, CancellationToken)` extension method. +- Introduced the `QuickValidator.ValidateAsync(T, Action>, PropertyNameMode, Action, CancellationToken)` extension method. +- Edit 'Quick Validation' README Chapter. +- Edit 'Quick Validation' NuGet README Chapter. + + ## 0.9.0 - Added quick validation support via `QuickValidator` and its `Validate` overloads. diff --git a/README.md b/README.md index 98e6dab..a034748 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ExpressValidator is a library that provides the ability to validate objects usin - Supports adding a property or field for validation. - Verifies that a property expression is a property and a field expression is a field, and throws `ArgumentException` if it is not. - Supports adding a `Func` that provides a value for validation. -- Provides quick validation (refers to ease of use). +- Provides quick and easy validation using the `QuickValidator`. - Supports asynchronous validation. - Targets .NET Standard 2.0+ @@ -159,6 +159,7 @@ var result = QuickValidator.Validate( .ChildRules((v) => v.RuleFor(o => o.PercentValue1).InclusiveBetween(0, 100)), nameof(obj)); ``` +The `QuickValidator` also provides a `ValidateAsync` method for asynchronous validation. ## 🧩 Nuances Of Using The Library diff --git a/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md b/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md index 951392a..e047ff9 100644 --- a/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md +++ b/src/ExpressValidator.Extensions.DependencyInjection/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.3.7 + +- Update ExpressValidator nuget package. +- Split the DI extensions into a dedicated solution. +- Update Microsoft nuget packages. +- Update Microsoft NuGet packages for ExpressValidator.Extensions.DependencyInjection.Tests. + + ## 0.3.5 - Reduced unnecessary updates to validator parameters by listening to `IOptionsMonitor.Change` with named validation options. diff --git a/src/ExpressValidator.Extensions.DependencyInjection/ExpressValidator.Extensions.DependencyInjection.csproj b/src/ExpressValidator.Extensions.DependencyInjection/ExpressValidator.Extensions.DependencyInjection.csproj index 8d9de04..91a8f85 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 true - 0.3.5 + 0.3.7 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.5.0 + 0.3.7.0 diff --git a/src/ExpressValidator/ExpressValidator.csproj b/src/ExpressValidator/ExpressValidator.csproj index 2e9f057..1fba684 100644 --- a/src/ExpressValidator/ExpressValidator.csproj +++ b/src/ExpressValidator/ExpressValidator.csproj @@ -3,7 +3,7 @@ netstandard2.0 true - 0.9.0 + 0.10.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.9.0.0 + 0.10.0.0 0.0.0.0 diff --git a/src/ExpressValidator/QuickValidation/QuickValidator.cs b/src/ExpressValidator/QuickValidation/QuickValidator.cs index 6426b7d..2783826 100644 --- a/src/ExpressValidator/QuickValidation/QuickValidator.cs +++ b/src/ExpressValidator/QuickValidation/QuickValidator.cs @@ -2,6 +2,8 @@ using FluentValidation; using FluentValidation.Results; using System; +using System.Threading; +using System.Threading.Tasks; namespace ExpressValidator.QuickValidation { @@ -52,6 +54,48 @@ private static ValidationResult ValidateInner(T obj, Action + /// Asynchronously validates the given object instance using . + /// + /// The type of the object to validate. + /// The object to validate. + /// Action to add validators. + /// The name of the property if the validation fails. + /// If , "Input" will be used. + /// Specifies a method to execute when validation succeeds. + /// >A cancellation token to cancel validation. + /// + public static Task ValidateAsync(T obj, Action> action, string propName, Action onSuccessValidation = null, CancellationToken token = default) + { + return ValidateInnerAsync(obj, action, propName ?? FALLBACK_PROP_NAME, onSuccessValidation, token); + } + + /// + /// Asynchronously validates the given object instance using . + /// + /// The type of the object to validate. + /// The object to validate. + /// Action to add validators. + /// . + /// If , "Input" will be used. + /// Specifies a method to execute when validation succeeds. + /// >A cancellation token to cancel validation. + /// + public static Task ValidateAsync(T obj, Action> action, PropertyNameMode mode = PropertyNameMode.Default, Action onSuccessValidation = null, CancellationToken token = default) + { + return ValidateInnerAsync(obj, action, GetPropName(mode), onSuccessValidation, token); + } + + private static Task ValidateInnerAsync(T obj, Action> action, string propName, Action onSuccessValidation = null, CancellationToken token = default) + { + var eb = new ExpressValidatorBuilder(); + return eb.AddFunc((_) => obj, + propName, + onSuccessValidation) + .WithAsyncValidation(action) + .BuildAndValidateAsync(Unit.Default, token); + } + private static string GetPropName(PropertyNameMode mode) => mode == PropertyNameMode.Default ? FALLBACK_PROP_NAME : typeof(T).Name; } } diff --git a/src/ExpressValidator/docs/NuGet.md b/src/ExpressValidator/docs/NuGet.md index 2d7fa16..ad050bb 100644 --- a/src/ExpressValidator/docs/NuGet.md +++ b/src/ExpressValidator/docs/NuGet.md @@ -8,7 +8,7 @@ ExpressValidator is a library that provides the ability to validate objects usin - Supports adding a property or field for validation. - Verifies that a property expression is a property and a field expression is a field, and throws `ArgumentException` if it is not. - Supports adding a `Func` that provides a value for validation. -- Provides quick validation (refers to ease of use). +- Provides quick and easy validation using the `QuickValidator`. - Supports asynchronous validation. - Targets .NET Standard 2.0+ @@ -144,6 +144,7 @@ var result = QuickValidator.Validate( .ChildRules((v) => v.RuleFor(o => o.PercentValue1).InclusiveBetween(0, 100)), nameof(obj)); ``` +The `QuickValidator` also provides a `ValidateAsync` method for asynchronous validation. ## Nuances Of Using The Library diff --git a/tests/ExpressValidator.Tests/QuickValidatorTests.cs b/tests/ExpressValidator.Tests/QuickValidatorTests.cs index ee97206..f4cba16 100644 --- a/tests/ExpressValidator.Tests/QuickValidatorTests.cs +++ b/tests/ExpressValidator.Tests/QuickValidatorTests.cs @@ -2,6 +2,7 @@ using FluentValidation; using NUnit.Framework; using System; +using System.Threading.Tasks; namespace ExpressValidator.Tests { @@ -20,6 +21,20 @@ public void Should_Fail_WithExpectedPropertyName_When_ValidationFails_ForPrimiti 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() { @@ -36,6 +51,23 @@ public void Should_Fail_WithOverriddenPropertyName_When_ValidationFails_ForPrimi 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)] @@ -55,6 +87,27 @@ public void Should_Fail_WithOverriddenPropertyName_When_ValidationFails_ForPrimi 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)] @@ -77,6 +130,30 @@ public void Should_Fail_WithExpectedPropertyName_When_ValidationFails_ForPrimiti } } + [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() { @@ -92,6 +169,21 @@ public void Should_Fail_WithExpectedPropertyName_When_ValidationFails_ForNonPrim 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() { @@ -107,6 +199,21 @@ public void Should_Fail_WithOverriddenPropertyName_When_ValidationFails_ForNonPr 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)] @@ -131,6 +238,30 @@ public void Should_Fail_WithOverriddenPropertyName_When_ValidationFails_ForNonPr } } + [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)] @@ -155,6 +286,30 @@ public void Should_Fail_WithExpectedPropertyName_When_ValidationFails_ForNonPrim } } + [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() { @@ -165,6 +320,16 @@ public void Should_Pass_Validation_When_Valid() 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)] @@ -197,6 +362,38 @@ public void Should_Call_OnSuccess_When_Validation_Succeeds(bool isValid) } } + [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)] @@ -229,6 +426,38 @@ public void Should_Call_OnSuccess_When_Validation_Succeeds_UsingOverload_WithPro } } + [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) => @@ -239,6 +468,19 @@ 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) => @@ -248,5 +490,18 @@ private static Action 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; }) + ); + } } }