diff --git a/src/RecursiveDataAnnotationsValidation/IAsyncRecursiveDataAnnotationValidator.cs b/src/RecursiveDataAnnotationsValidation/IAsyncRecursiveDataAnnotationValidator.cs new file mode 100644 index 0000000..4e1059c --- /dev/null +++ b/src/RecursiveDataAnnotationsValidation/IAsyncRecursiveDataAnnotationValidator.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; + +namespace RecursiveDataAnnotationsValidation +{ + /// Async interface for the RecursiveDataAnnotationValidator. Useful if you need + /// to swap in a different approach, or to mock the methods. + public interface IAsyncRecursiveDataAnnotationValidator + { + /// Runs async validation on an object. + /// The object being validated. + /// Validation context. + /// A collection that will be populated if validation errors occur. + /// Returns true if all validation passes. + Task TryValidateObjectRecursiveAsync( + object obj, + ValidationContext validationContext, + List validationResults + ); + + /// Runs async validation on an object. + /// The object being validated. + /// A collection that will be populated if validation errors occur. + /// Validation context items. + /// Returns true if all validation passes. + Task TryValidateObjectRecursiveAsync( + object obj, + List validationResults, + IDictionary validationContextItems = null + ); + } +} \ No newline at end of file diff --git a/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs b/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs index 9eab228..f90368a 100644 --- a/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs +++ b/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs @@ -2,13 +2,14 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Threading.Tasks; using RecursiveDataAnnotationsValidation.Attributes; using RecursiveDataAnnotationsValidation.Extensions; namespace RecursiveDataAnnotationsValidation { /// Recursive validator for DataAnnotation attribute-based validation. - public class RecursiveDataAnnotationValidator : IRecursiveDataAnnotationValidator + public class RecursiveDataAnnotationValidator : IRecursiveDataAnnotationValidator, IAsyncRecursiveDataAnnotationValidator { /// Runs validation on an object. /// The object being validated. @@ -34,19 +35,63 @@ List validationResults /// Validation context items. /// Returns true if all validation passes. public bool TryValidateObjectRecursive( - object obj, - List validationResults, + object obj, + List validationResults, IDictionary validationContextItems = null ) { return TryValidateObjectRecursive( - obj, - validationResults, - new HashSet(), + obj, + validationResults, + new HashSet(), validationContextItems ); } + /// Runs async validation on an object. + /// The object being validated. + /// Validation context. + /// A collection that will be populated if validation errors occur. + /// Returns true if all validation passes. + public async Task TryValidateObjectRecursiveAsync( + object obj, + ValidationContext validationContext, + List validationResults + ) + { + return await Task.Run(() => TryValidateObjectRecursive( + obj, + validationResults, + validationContext.Items + )); + } + + /// Runs async validation on an object. + /// The object being validated. + /// A collection that will be populated if validation errors occur. + /// Validation context items. + /// Returns true if all validation passes. + public async Task TryValidateObjectRecursiveAsync( + object obj, + List validationResults, + IDictionary validationContextItems = null + ) + { + return await Task.Run(() => TryValidateObjectRecursive( + obj, + validationResults, + new HashSet(), + validationContextItems + )); + } + + /// + /// Validates the specified object and adds any validation results to the provided collection. + /// + /// The object to validate. + /// A collection to receive any validation errors. + /// Optional context items for the validation context. + /// True if the object is valid; otherwise, false. private bool TryValidateObject( object obj, ICollection validationResults, diff --git a/test/RecursiveDataAnnotationsValidation.Tests/AsyncCollectionTests.cs b/test/RecursiveDataAnnotationsValidation.Tests/AsyncCollectionTests.cs new file mode 100644 index 0000000..6c5cfa5 --- /dev/null +++ b/test/RecursiveDataAnnotationsValidation.Tests/AsyncCollectionTests.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using RecursiveDataAnnotationsValidation.Tests.TestModels; +using Xunit; + +namespace RecursiveDataAnnotationsValidation.Tests +{ + public class AsyncCollectionTests + { + private readonly IAsyncRecursiveDataAnnotationValidator _sut = new RecursiveDataAnnotationValidator(); + + [Fact] + public async Task Validates_collections_recursively() + { + var model = new ItemWithListExample + { + ItemWithListName = "Parent", + Claims = new List { "Claim1", "Claim2" } + }; + + var validationResults = new List(); + var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults); + + Assert.True(result); + Assert.Empty(validationResults); + } + + [Fact] + public async Task Fails_when_collection_item_has_validation_errors() + { + var model = new ItemWithListExample + { + ItemWithListName = "Parent", + Claims = new List { null } // This should fail validation due to [Required] + }; + + var validationResults = new List(); + var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults); + + Assert.False(result); + Assert.NotEmpty(validationResults); + } + } +} \ No newline at end of file diff --git a/test/RecursiveDataAnnotationsValidation.Tests/AsyncRecursionTests.cs b/test/RecursiveDataAnnotationsValidation.Tests/AsyncRecursionTests.cs new file mode 100644 index 0000000..de6a6c7 --- /dev/null +++ b/test/RecursiveDataAnnotationsValidation.Tests/AsyncRecursionTests.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using RecursiveDataAnnotationsValidation.Tests.TestModels; +using Xunit; + +namespace RecursiveDataAnnotationsValidation.Tests +{ + public class AsyncRecursionTests + { + private readonly IAsyncRecursiveDataAnnotationValidator _sut = new RecursiveDataAnnotationValidator(); + + // This test verifies that async version handles recursive structures correctly + [Fact] + public async Task Handles_recursive_structures_without_infinite_loop() + { + var recursiveModel = new RecursionExample + { + Name = "Recursion1-pass", + BooleanA = false, + Recursion = new RecursionExample + { + Name = "Recursion1-pass.Inner1", + BooleanA = true, + Recursion = null + } + }; + recursiveModel.Recursion.Recursion = recursiveModel; + + var model = new RecursionExample + { + Name = "SUT", + BooleanA = true, + Recursion = recursiveModel + }; + + var validationResults = new List(); + var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults); + + Assert.True(result); + Assert.Empty(validationResults); + } + + [Fact] + public async Task Fails_when_recursive_property_has_validation_errors() + { + var recursiveModel = new RecursionExample + { + Name = "Recursion1-fail", + BooleanA = false, + Recursion = new RecursionExample + { + Name = "Recursion1-fail.Inner1", + BooleanA = null, // This should fail validation due to [Required] + Recursion = null + } + }; + recursiveModel.Recursion.Recursion = recursiveModel; + + var model = new RecursionExample + { + Name = "SUT", + BooleanA = true, + Recursion = recursiveModel + }; + + var validationResults = new List(); + var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults); + + Assert.False(result); + Assert.NotEmpty(validationResults); + } + } +} \ No newline at end of file diff --git a/test/RecursiveDataAnnotationsValidation.Tests/AsyncValidatorTests.cs b/test/RecursiveDataAnnotationsValidation.Tests/AsyncValidatorTests.cs new file mode 100644 index 0000000..2b2ccaf --- /dev/null +++ b/test/RecursiveDataAnnotationsValidation.Tests/AsyncValidatorTests.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using RecursiveDataAnnotationsValidation.Tests.TestModels; +using Xunit; + +namespace RecursiveDataAnnotationsValidation.Tests +{ + public class AsyncValidatorTests + { + private readonly IAsyncRecursiveDataAnnotationValidator _sut = new RecursiveDataAnnotationValidator(); + + [Fact] + public async Task Pass_all_validation() + { + var model = new SimpleExample + { + IntegerA = 100, + StringB = "test-100", + BoolC = true, + ExampleEnumD = ExampleEnum.ValueB + }; + + var validationContext = new ValidationContext(model); + var validationResults = new List(); + var result = await _sut.TryValidateObjectRecursiveAsync(model, validationContext, validationResults); + + Assert.True(result); + Assert.Empty(validationResults); + } + + [Fact] + public async Task Indicate_that_IntegerA_is_missing() + { + var model = new SimpleExample + { + IntegerA = null, + StringB = "test-101", + BoolC = false, + ExampleEnumD = ExampleEnum.ValueC + }; + + const string fieldName = nameof(SimpleExample.IntegerA); + var validationContext = new ValidationContext(model); + var validationResults = new List(); + var result = await _sut.TryValidateObjectRecursiveAsync(model, validationContext, validationResults); + + Assert.False(result); + Assert.NotEmpty(validationResults); + Assert.NotNull(validationResults + .FirstOrDefault(x => x.MemberNames.Contains(fieldName))); + } + + [Fact] + public async Task Pass_all_validation_without_context() + { + var model = new SimpleExample + { + IntegerA = 100, + StringB = "test-100", + BoolC = true, + ExampleEnumD = ExampleEnum.ValueB + }; + + var validationResults = new List(); + var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults); + + Assert.True(result); + Assert.Empty(validationResults); + } + } +} \ No newline at end of file diff --git a/test/RecursiveDataAnnotationsValidation.Tests/DeadlockPreventionTests.cs b/test/RecursiveDataAnnotationsValidation.Tests/DeadlockPreventionTests.cs new file mode 100644 index 0000000..d93f837 --- /dev/null +++ b/test/RecursiveDataAnnotationsValidation.Tests/DeadlockPreventionTests.cs @@ -0,0 +1,157 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using RecursiveDataAnnotationsValidation.Tests.TestModels; +using Xunit; + +namespace RecursiveDataAnnotationsValidation.Tests +{ + public class DeadlockPreventionTests + { + private readonly IAsyncRecursiveDataAnnotationValidator _sut = new RecursiveDataAnnotationValidator(); + + /// + /// Test to ensure that our async implementation doesn't create potential deadlocks + /// by testing proper async execution patterns. + /// + [Fact] + public async Task Async_methods_should_not_create_deadlocks() + { + // This test ensures that the async methods properly execute + // without creating synchronization context issues that could lead to deadlocks + + var model = new SimpleExample + { + IntegerA = 100, + StringB = "test-100", + BoolC = true, + ExampleEnumD = ExampleEnum.ValueB + }; + + var validationResults = new List(); + + // Test that the async method can be awaited without blocking + var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults); + + Assert.True(result); + Assert.Empty(validationResults); + } + + /// + /// Test that validates async behavior with collections doesn't cause blocking issues + /// + [Fact] + public async Task Async_collection_validation_should_not_deadlock() + { + var model = new ItemWithListExample + { + ItemWithListName = "Parent", + Claims = new List { "Claim1", "Claim2" } + }; + + var validationResults = new List(); + + // Test that collection validation works async without hanging + var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults); + + Assert.True(result); + Assert.Empty(validationResults); + } + + /// + /// Test that validates recursive structure handling async without deadlock + /// + [Fact] + public async Task Async_recursive_validation_should_not_deadlock() + { + var recursiveModel = new RecursionExample + { + Name = "Recursion1-pass", + BooleanA = false, + Recursion = new RecursionExample + { + Name = "Recursion1-pass.Inner1", + BooleanA = true, + Recursion = null + } + }; + recursiveModel.Recursion.Recursion = recursiveModel; + + var model = new RecursionExample + { + Name = "SUT", + BooleanA = true, + Recursion = recursiveModel + }; + + var validationResults = new List(); + + // Test that recursive validation works async without hanging + var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults); + + Assert.True(result); + Assert.Empty(validationResults); + } + + /// + /// Test that the async methods properly handle validation errors without deadlocking + /// + [Fact] + public async Task Async_error_handling_should_not_deadlock() + { + var model = new SimpleExample + { + IntegerA = null, // This should fail validation due to [Required] + StringB = "test-101", + BoolC = false, + ExampleEnumD = ExampleEnum.ValueC + }; + + var validationResults = new List(); + + // Test that error handling works async without hanging + var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults); + + Assert.False(result); + Assert.NotEmpty(validationResults); + } + + /// + /// Test that validates proper async execution pattern for concurrent usage + /// + [Fact] + public async Task Multiple_concurrent_async_calls_should_not_deadlock() + { + var model1 = new SimpleExample + { + IntegerA = 100, + StringB = "test-100", + BoolC = true, + ExampleEnumD = ExampleEnum.ValueB + }; + + var model2 = new SimpleExample + { + IntegerA = 200, + StringB = "test-200", + BoolC = false, + ExampleEnumD = ExampleEnum.ValueA + }; + + var validationResults1 = new List(); + var validationResults2 = new List(); + + // Test concurrent async calls + var task1 = _sut.TryValidateObjectRecursiveAsync(model1, validationResults1); + var task2 = _sut.TryValidateObjectRecursiveAsync(model2, validationResults2); + + // Both should complete without deadlocking + var results = await Task.WhenAll(task1, task2); + + Assert.True(results[0]); + Assert.True(results[1]); + Assert.Empty(validationResults1); + Assert.Empty(validationResults2); + } + } +} \ No newline at end of file