From a8007a72c06304aeb292f23ad5d74fdf497cfeff Mon Sep 17 00:00:00 2001 From: Thomas Harold Date: Sat, 28 Mar 2026 19:02:17 -0400 Subject: [PATCH 1/4] Add async version of TryValidateObjectRecursive method and tests - Added IAsyncRecursiveDataAnnotationValidator interface - Updated RecursiveDataAnnotationValidator to implement the async interface - Added two new async methods: * TryValidateObjectRecursiveAsync(ValidationContext) * TryValidateObjectRecursiveAsync(IDictionary) - Created comprehensive tests for async functionality including: * Basic async validation * Async collection validation * Async recursive structure handling This maintains all existing functionality while adding async support for use in modern .NET applications. Claude Code w/ qwen3-coder-30b-a3b-instruct-mlx (4bit) --- .../IAsyncRecursiveDataAnnotationValidator.cs | 33 ++++++++ .../RecursiveDataAnnotationValidator.cs | 50 +++++++++++-- .../AsyncCollectionTests.cs | 46 ++++++++++++ .../AsyncRecursionTests.cs | 75 +++++++++++++++++++ .../AsyncValidatorTests.cs | 73 ++++++++++++++++++ 5 files changed, 271 insertions(+), 6 deletions(-) create mode 100644 src/RecursiveDataAnnotationsValidation/IAsyncRecursiveDataAnnotationValidator.cs create mode 100644 test/RecursiveDataAnnotationsValidation.Tests/AsyncCollectionTests.cs create mode 100644 test/RecursiveDataAnnotationsValidation.Tests/AsyncRecursionTests.cs create mode 100644 test/RecursiveDataAnnotationsValidation.Tests/AsyncValidatorTests.cs 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..f932b28 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,56 @@ 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 Task TryValidateObjectRecursiveAsync( + object obj, + ValidationContext validationContext, + List validationResults + ) + { + return Task.FromResult(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 Task TryValidateObjectRecursiveAsync( + object obj, + List validationResults, + IDictionary validationContextItems = null + ) + { + return Task.FromResult(TryValidateObjectRecursive( + obj, + validationResults, + new HashSet(), + validationContextItems + )); + } + 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 From cd8abf7f560f84c5997bda615495eb99c3e6a42e Mon Sep 17 00:00:00 2001 From: Thomas Harold Date: Sat, 28 Mar 2026 19:30:28 -0400 Subject: [PATCH 2/4] Use Task.Run for async validation methods to improve thread utilization - Changed from Task.FromResult to Task.Run in async implementations - This ensures the validation work runs on a thread pool thread rather than blocking the calling thread - While System.ComponentModel.DataAnnotations is still synchronous, this approach better aligns with proper async patterns in .NET - Provides better performance characteristics in high-concurrency scenarios (like web applications) - Maintains exact same functional behavior as before The underlying validation still uses the synchronous Validator.TryValidateObject methods, but the async API now properly releases calling threads when awaited. --- .../RecursiveDataAnnotationValidator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs b/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs index f932b28..421f514 100644 --- a/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs +++ b/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs @@ -53,13 +53,13 @@ public bool TryValidateObjectRecursive( /// Validation context. /// A collection that will be populated if validation errors occur. /// Returns true if all validation passes. - public Task TryValidateObjectRecursiveAsync( + public async Task TryValidateObjectRecursiveAsync( object obj, ValidationContext validationContext, List validationResults ) { - return Task.FromResult(TryValidateObjectRecursive( + return await Task.Run(() => TryValidateObjectRecursive( obj, validationResults, validationContext.Items @@ -71,13 +71,13 @@ List validationResults /// A collection that will be populated if validation errors occur. /// Validation context items. /// Returns true if all validation passes. - public Task TryValidateObjectRecursiveAsync( + public async Task TryValidateObjectRecursiveAsync( object obj, List validationResults, IDictionary validationContextItems = null ) { - return Task.FromResult(TryValidateObjectRecursive( + return await Task.Run(() => TryValidateObjectRecursive( obj, validationResults, new HashSet(), From f6ef4878063b545238165e13aec67dca079028c2 Mon Sep 17 00:00:00 2001 From: Thomas Harold Date: Sat, 28 Mar 2026 19:33:36 -0400 Subject: [PATCH 3/4] Let GitHub Copilot suggest the XML comment --- .../RecursiveDataAnnotationValidator.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs b/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs index 421f514..f90368a 100644 --- a/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs +++ b/src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs @@ -85,6 +85,13 @@ public async Task TryValidateObjectRecursiveAsync( )); } + /// + /// 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, From a3c89f67f01846e8d80b8475baa18c3b7d9ca40b Mon Sep 17 00:00:00 2001 From: Thomas Harold Date: Sat, 28 Mar 2026 20:22:23 -0400 Subject: [PATCH 4/4] Add deadlock prevention tests for async validation methods - Added comprehensive deadlock prevention tests to verify proper async behavior - Tests ensure that async implementations don't create synchronization context deadlocks - Includes tests for: * Basic async execution patterns * Collection validation in async context * Recursive structure handling in async * Error handling with async methods * Concurrent async calls These tests leverage xUnit's built-in synchronization context to expose potential deadlocks by ensuring tests complete properly without hanging. --- .../DeadlockPreventionTests.cs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 test/RecursiveDataAnnotationsValidation.Tests/DeadlockPreventionTests.cs 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