Skip to content

Commit 6d96ac1

Browse files
authored
Merge pull request #54 from tgharold/20260328-1737
Add async version of TryValidateObjectRecursive method and tests
2 parents 4abd4db + a3c89f6 commit 6d96ac1

File tree

6 files changed

+435
-6
lines changed

6 files changed

+435
-6
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations;
3+
using System.Threading.Tasks;
4+
5+
namespace RecursiveDataAnnotationsValidation
6+
{
7+
/// <summary>Async interface for the RecursiveDataAnnotationValidator. Useful if you need
8+
/// to swap in a different approach, or to mock the methods.</summary>
9+
public interface IAsyncRecursiveDataAnnotationValidator
10+
{
11+
/// <summary>Runs async validation on an object.</summary>
12+
/// <param name="obj">The object being validated.</param>
13+
/// <param name="validationContext">Validation context.</param>
14+
/// <param name="validationResults">A collection that will be populated if validation errors occur.</param>
15+
/// <returns>Returns true if all validation passes.</returns>
16+
Task<bool> TryValidateObjectRecursiveAsync(
17+
object obj,
18+
ValidationContext validationContext,
19+
List<ValidationResult> validationResults
20+
);
21+
22+
/// <summary>Runs async validation on an object.</summary>
23+
/// <param name="obj">The object being validated.</param>
24+
/// <param name="validationResults">A collection that will be populated if validation errors occur.</param>
25+
/// <param name="validationContextItems">Validation context items.</param>
26+
/// <returns>Returns true if all validation passes.</returns>
27+
Task<bool> TryValidateObjectRecursiveAsync(
28+
object obj,
29+
List<ValidationResult> validationResults,
30+
IDictionary<object, object> validationContextItems = null
31+
);
32+
}
33+
}

src/RecursiveDataAnnotationsValidation/RecursiveDataAnnotationValidator.cs

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
using System.Collections.Generic;
33
using System.ComponentModel.DataAnnotations;
44
using System.Linq;
5+
using System.Threading.Tasks;
56
using RecursiveDataAnnotationsValidation.Attributes;
67
using RecursiveDataAnnotationsValidation.Extensions;
78

89
namespace RecursiveDataAnnotationsValidation
910
{
1011
/// <summary>Recursive validator for DataAnnotation attribute-based validation.</summary>
11-
public class RecursiveDataAnnotationValidator : IRecursiveDataAnnotationValidator
12+
public class RecursiveDataAnnotationValidator : IRecursiveDataAnnotationValidator, IAsyncRecursiveDataAnnotationValidator
1213
{
1314
/// <summary>Runs validation on an object.</summary>
1415
/// <param name="obj">The object being validated.</param>
@@ -34,19 +35,63 @@ List<ValidationResult> validationResults
3435
/// <param name="validationContextItems">Validation context items.</param>
3536
/// <returns>Returns true if all validation passes.</returns>
3637
public bool TryValidateObjectRecursive(
37-
object obj,
38-
List<ValidationResult> validationResults,
38+
object obj,
39+
List<ValidationResult> validationResults,
3940
IDictionary<object, object> validationContextItems = null
4041
)
4142
{
4243
return TryValidateObjectRecursive(
43-
obj,
44-
validationResults,
45-
new HashSet<object>(),
44+
obj,
45+
validationResults,
46+
new HashSet<object>(),
4647
validationContextItems
4748
);
4849
}
4950

51+
/// <summary>Runs async validation on an object.</summary>
52+
/// <param name="obj">The object being validated.</param>
53+
/// <param name="validationContext">Validation context.</param>
54+
/// <param name="validationResults">A collection that will be populated if validation errors occur.</param>
55+
/// <returns>Returns true if all validation passes.</returns>
56+
public async Task<bool> TryValidateObjectRecursiveAsync(
57+
object obj,
58+
ValidationContext validationContext,
59+
List<ValidationResult> validationResults
60+
)
61+
{
62+
return await Task.Run(() => TryValidateObjectRecursive(
63+
obj,
64+
validationResults,
65+
validationContext.Items
66+
));
67+
}
68+
69+
/// <summary>Runs async validation on an object.</summary>
70+
/// <param name="obj">The object being validated.</param>
71+
/// <param name="validationResults">A collection that will be populated if validation errors occur.</param>
72+
/// <param name="validationContextItems">Validation context items.</param>
73+
/// <returns>Returns true if all validation passes.</returns>
74+
public async Task<bool> TryValidateObjectRecursiveAsync(
75+
object obj,
76+
List<ValidationResult> validationResults,
77+
IDictionary<object, object> validationContextItems = null
78+
)
79+
{
80+
return await Task.Run(() => TryValidateObjectRecursive(
81+
obj,
82+
validationResults,
83+
new HashSet<object>(),
84+
validationContextItems
85+
));
86+
}
87+
88+
/// <summary>
89+
/// Validates the specified object and adds any validation results to the provided collection.
90+
/// </summary>
91+
/// <param name="obj">The object to validate.</param>
92+
/// <param name="validationResults">A collection to receive any validation errors.</param>
93+
/// <param name="validationContextItems">Optional context items for the validation context.</param>
94+
/// <returns>True if the object is valid; otherwise, false.</returns>
5095
private bool TryValidateObject(
5196
object obj,
5297
ICollection<ValidationResult> validationResults,
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using RecursiveDataAnnotationsValidation.Tests.TestModels;
6+
using Xunit;
7+
8+
namespace RecursiveDataAnnotationsValidation.Tests
9+
{
10+
public class AsyncCollectionTests
11+
{
12+
private readonly IAsyncRecursiveDataAnnotationValidator _sut = new RecursiveDataAnnotationValidator();
13+
14+
[Fact]
15+
public async Task Validates_collections_recursively()
16+
{
17+
var model = new ItemWithListExample
18+
{
19+
ItemWithListName = "Parent",
20+
Claims = new List<string> { "Claim1", "Claim2" }
21+
};
22+
23+
var validationResults = new List<ValidationResult>();
24+
var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults);
25+
26+
Assert.True(result);
27+
Assert.Empty(validationResults);
28+
}
29+
30+
[Fact]
31+
public async Task Fails_when_collection_item_has_validation_errors()
32+
{
33+
var model = new ItemWithListExample
34+
{
35+
ItemWithListName = "Parent",
36+
Claims = new List<string> { null } // This should fail validation due to [Required]
37+
};
38+
39+
var validationResults = new List<ValidationResult>();
40+
var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults);
41+
42+
Assert.False(result);
43+
Assert.NotEmpty(validationResults);
44+
}
45+
}
46+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using RecursiveDataAnnotationsValidation.Tests.TestModels;
6+
using Xunit;
7+
8+
namespace RecursiveDataAnnotationsValidation.Tests
9+
{
10+
public class AsyncRecursionTests
11+
{
12+
private readonly IAsyncRecursiveDataAnnotationValidator _sut = new RecursiveDataAnnotationValidator();
13+
14+
// This test verifies that async version handles recursive structures correctly
15+
[Fact]
16+
public async Task Handles_recursive_structures_without_infinite_loop()
17+
{
18+
var recursiveModel = new RecursionExample
19+
{
20+
Name = "Recursion1-pass",
21+
BooleanA = false,
22+
Recursion = new RecursionExample
23+
{
24+
Name = "Recursion1-pass.Inner1",
25+
BooleanA = true,
26+
Recursion = null
27+
}
28+
};
29+
recursiveModel.Recursion.Recursion = recursiveModel;
30+
31+
var model = new RecursionExample
32+
{
33+
Name = "SUT",
34+
BooleanA = true,
35+
Recursion = recursiveModel
36+
};
37+
38+
var validationResults = new List<ValidationResult>();
39+
var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults);
40+
41+
Assert.True(result);
42+
Assert.Empty(validationResults);
43+
}
44+
45+
[Fact]
46+
public async Task Fails_when_recursive_property_has_validation_errors()
47+
{
48+
var recursiveModel = new RecursionExample
49+
{
50+
Name = "Recursion1-fail",
51+
BooleanA = false,
52+
Recursion = new RecursionExample
53+
{
54+
Name = "Recursion1-fail.Inner1",
55+
BooleanA = null, // This should fail validation due to [Required]
56+
Recursion = null
57+
}
58+
};
59+
recursiveModel.Recursion.Recursion = recursiveModel;
60+
61+
var model = new RecursionExample
62+
{
63+
Name = "SUT",
64+
BooleanA = true,
65+
Recursion = recursiveModel
66+
};
67+
68+
var validationResults = new List<ValidationResult>();
69+
var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults);
70+
71+
Assert.False(result);
72+
Assert.NotEmpty(validationResults);
73+
}
74+
}
75+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using RecursiveDataAnnotationsValidation.Tests.TestModels;
6+
using Xunit;
7+
8+
namespace RecursiveDataAnnotationsValidation.Tests
9+
{
10+
public class AsyncValidatorTests
11+
{
12+
private readonly IAsyncRecursiveDataAnnotationValidator _sut = new RecursiveDataAnnotationValidator();
13+
14+
[Fact]
15+
public async Task Pass_all_validation()
16+
{
17+
var model = new SimpleExample
18+
{
19+
IntegerA = 100,
20+
StringB = "test-100",
21+
BoolC = true,
22+
ExampleEnumD = ExampleEnum.ValueB
23+
};
24+
25+
var validationContext = new ValidationContext(model);
26+
var validationResults = new List<ValidationResult>();
27+
var result = await _sut.TryValidateObjectRecursiveAsync(model, validationContext, validationResults);
28+
29+
Assert.True(result);
30+
Assert.Empty(validationResults);
31+
}
32+
33+
[Fact]
34+
public async Task Indicate_that_IntegerA_is_missing()
35+
{
36+
var model = new SimpleExample
37+
{
38+
IntegerA = null,
39+
StringB = "test-101",
40+
BoolC = false,
41+
ExampleEnumD = ExampleEnum.ValueC
42+
};
43+
44+
const string fieldName = nameof(SimpleExample.IntegerA);
45+
var validationContext = new ValidationContext(model);
46+
var validationResults = new List<ValidationResult>();
47+
var result = await _sut.TryValidateObjectRecursiveAsync(model, validationContext, validationResults);
48+
49+
Assert.False(result);
50+
Assert.NotEmpty(validationResults);
51+
Assert.NotNull(validationResults
52+
.FirstOrDefault(x => x.MemberNames.Contains(fieldName)));
53+
}
54+
55+
[Fact]
56+
public async Task Pass_all_validation_without_context()
57+
{
58+
var model = new SimpleExample
59+
{
60+
IntegerA = 100,
61+
StringB = "test-100",
62+
BoolC = true,
63+
ExampleEnumD = ExampleEnum.ValueB
64+
};
65+
66+
var validationResults = new List<ValidationResult>();
67+
var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults);
68+
69+
Assert.True(result);
70+
Assert.Empty(validationResults);
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)