Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;

namespace RecursiveDataAnnotationsValidation
{
/// <summary>Async interface for the RecursiveDataAnnotationValidator. Useful if you need
/// to swap in a different approach, or to mock the methods.</summary>
public interface IAsyncRecursiveDataAnnotationValidator
{
/// <summary>Runs async validation on an object.</summary>
/// <param name="obj">The object being validated.</param>
/// <param name="validationContext">Validation context.</param>
/// <param name="validationResults">A collection that will be populated if validation errors occur.</param>
/// <returns>Returns true if all validation passes.</returns>
Task<bool> TryValidateObjectRecursiveAsync(
object obj,
ValidationContext validationContext,
List<ValidationResult> validationResults
);

/// <summary>Runs async validation on an object.</summary>
/// <param name="obj">The object being validated.</param>
/// <param name="validationResults">A collection that will be populated if validation errors occur.</param>
/// <param name="validationContextItems">Validation context items.</param>
/// <returns>Returns true if all validation passes.</returns>
Task<bool> TryValidateObjectRecursiveAsync(
object obj,
List<ValidationResult> validationResults,
IDictionary<object, object> validationContextItems = null
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>Recursive validator for DataAnnotation attribute-based validation.</summary>
public class RecursiveDataAnnotationValidator : IRecursiveDataAnnotationValidator
public class RecursiveDataAnnotationValidator : IRecursiveDataAnnotationValidator, IAsyncRecursiveDataAnnotationValidator
{
/// <summary>Runs validation on an object.</summary>
/// <param name="obj">The object being validated.</param>
Expand All @@ -34,19 +35,63 @@ List<ValidationResult> validationResults
/// <param name="validationContextItems">Validation context items.</param>
/// <returns>Returns true if all validation passes.</returns>
public bool TryValidateObjectRecursive(
object obj,
List<ValidationResult> validationResults,
object obj,
List<ValidationResult> validationResults,
IDictionary<object, object> validationContextItems = null
)
{
return TryValidateObjectRecursive(
obj,
validationResults,
new HashSet<object>(),
obj,
validationResults,
new HashSet<object>(),
validationContextItems
);
}

/// <summary>Runs async validation on an object.</summary>
/// <param name="obj">The object being validated.</param>
/// <param name="validationContext">Validation context.</param>
/// <param name="validationResults">A collection that will be populated if validation errors occur.</param>
/// <returns>Returns true if all validation passes.</returns>
public async Task<bool> TryValidateObjectRecursiveAsync(
object obj,
ValidationContext validationContext,
List<ValidationResult> validationResults
)
{
return await Task.Run(() => TryValidateObjectRecursive(
obj,
validationResults,
validationContext.Items
));
}

/// <summary>Runs async validation on an object.</summary>
/// <param name="obj">The object being validated.</param>
/// <param name="validationResults">A collection that will be populated if validation errors occur.</param>
/// <param name="validationContextItems">Validation context items.</param>
/// <returns>Returns true if all validation passes.</returns>
public async Task<bool> TryValidateObjectRecursiveAsync(
object obj,
List<ValidationResult> validationResults,
IDictionary<object, object> validationContextItems = null
)
{
return await Task.Run(() => TryValidateObjectRecursive(
obj,
validationResults,
new HashSet<object>(),
validationContextItems
));
}

/// <summary>
/// Validates the specified object and adds any validation results to the provided collection.
/// </summary>
/// <param name="obj">The object to validate.</param>
/// <param name="validationResults">A collection to receive any validation errors.</param>
/// <param name="validationContextItems">Optional context items for the validation context.</param>
/// <returns>True if the object is valid; otherwise, false.</returns>
private bool TryValidateObject(
object obj,
ICollection<ValidationResult> validationResults,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string> { "Claim1", "Claim2" }
};

var validationResults = new List<ValidationResult>();
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<string> { null } // This should fail validation due to [Required]
};

var validationResults = new List<ValidationResult>();
var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults);

Assert.False(result);
Assert.NotEmpty(validationResults);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ValidationResult>();
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<ValidationResult>();
var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults);

Assert.False(result);
Assert.NotEmpty(validationResults);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ValidationResult>();
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<ValidationResult>();
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<ValidationResult>();
var result = await _sut.TryValidateObjectRecursiveAsync(model, validationResults);

Assert.True(result);
Assert.Empty(validationResults);
}
}
}
Loading
Loading