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