diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml
index 087b222..754f544 100644
--- a/.github/workflows/_build.yml
+++ b/.github/workflows/_build.yml
@@ -19,7 +19,10 @@ jobs:
- name: Install .NET SDK
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 6.0.x
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
+ 10.0.x
global-json-file: "./global.json"
- name: Restore dependencies
diff --git a/global.json b/global.json
index 501e79a..512142d 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "8.0.100",
+ "version": "10.0.100",
"rollForward": "latestFeature"
}
-}
\ No newline at end of file
+}
diff --git a/samples/Samples.Console/Samples.Console.csproj b/samples/Samples.Console/Samples.Console.csproj
index 92b9057..7bfb498 100644
--- a/samples/Samples.Console/Samples.Console.csproj
+++ b/samples/Samples.Console/Samples.Console.csproj
@@ -2,7 +2,7 @@
Exe
- net6.0
+ net8.0
enable
diff --git a/samples/Samples.Web/Samples.Web.csproj b/samples/Samples.Web/Samples.Web.csproj
index 7190ba0..f3b5e57 100644
--- a/samples/Samples.Web/Samples.Web.csproj
+++ b/samples/Samples.Web/Samples.Web.csproj
@@ -1,7 +1,7 @@
- net7.0
+ net8.0
enable
enable
diff --git a/src/MiniValidation/MiniValidation.csproj b/src/MiniValidation/MiniValidation.csproj
index 7f2afc4..44f3c99 100644
--- a/src/MiniValidation/MiniValidation.csproj
+++ b/src/MiniValidation/MiniValidation.csproj
@@ -2,7 +2,7 @@
A minimalist validation library built atop the existing validation features in .NET's `System.ComponentModel.DataAnnotations` namespace.
- netstandard2.0;net6.0
+ netstandard2.0;net8.0
ComponentModel DataAnnotations validation
README.md
10.0
diff --git a/src/MiniValidation/MiniValidator.cs b/src/MiniValidation/MiniValidator.cs
index 91d00c4..9a0c531 100644
--- a/src/MiniValidation/MiniValidator.cs
+++ b/src/MiniValidation/MiniValidator.cs
@@ -503,7 +503,7 @@ private static async Task TryValidateImpl(
}
}
- if (isValid && typeof(IValidatableObject).IsAssignableFrom(targetType))
+ if (typeof(IValidatableObject).IsAssignableFrom(targetType))
{
var validatable = (IValidatableObject)target;
@@ -514,12 +514,11 @@ private static async Task TryValidateImpl(
var validatableResults = validatable.Validate(validationContext);
if (validatableResults is not null)
{
- ProcessValidationResults(validatableResults, workingErrors, prefix);
- isValid = workingErrors.Count == 0 && isValid;
+ isValid = ProcessValidationResults(validatableResults, workingErrors, prefix) && isValid;
}
}
- if (isValid && typeof(IAsyncValidatableObject).IsAssignableFrom(targetType))
+ if ((isValid || allowAsync) && typeof(IAsyncValidatableObject).IsAssignableFrom(targetType))
{
var validatable = (IAsyncValidatableObject)target;
@@ -533,8 +532,7 @@ private static async Task TryValidateImpl(
var validatableResults = await validateTask.ConfigureAwait(false);
if (validatableResults is not null)
{
- ProcessValidationResults(validatableResults, workingErrors, prefix);
- isValid = workingErrors.Count == 0 && isValid;
+ isValid = ProcessValidationResults(validatableResults, workingErrors, prefix) && isValid;
}
}
@@ -598,12 +596,7 @@ private static async Task TryValidateEnumerable(
throw;
}
- isValid = await validateTask.ConfigureAwait(false);
-
- if (!isValid)
- {
- break;
- }
+ isValid = await validateTask.ConfigureAwait(false) && isValid;
index++;
}
}
@@ -633,10 +626,13 @@ private static IDictionary MapToFinalErrorsResult(Dictionary validationResults, Dictionary> errors, string? prefix)
+ private static bool ProcessValidationResults(IEnumerable validationResults, Dictionary> errors, string? prefix)
{
+ var isValid = true;
+
foreach (var result in validationResults)
{
+ isValid = false;
var hasMemberNames = false;
foreach (var memberName in result.MemberNames)
{
@@ -652,7 +648,7 @@ private static void ProcessValidationResults(IEnumerable valid
if (!hasMemberNames)
{
// Class level error message
- var key = "";
+ var key = GetClassLevelKey(prefix);
if (!errors.ContainsKey(key))
{
errors.Add(key, new());
@@ -660,6 +656,20 @@ private static void ProcessValidationResults(IEnumerable valid
errors[key].Add(result.ErrorMessage ?? "");
}
}
+
+ return isValid;
+
+ static string GetClassLevelKey(string? prefix)
+ {
+ if (string.IsNullOrEmpty(prefix))
+ {
+ return "";
+ }
+
+ return prefix!.EndsWith(".", StringComparison.Ordinal)
+ ? prefix.Substring(0, prefix.Length - 1)
+ : prefix;
+ }
}
private static void ProcessValidationResults(string propertyName, ICollection validationResults, Dictionary> errors, string? prefix)
diff --git a/src/MiniValidation/TypeDetailsCache.cs b/src/MiniValidation/TypeDetailsCache.cs
index aee8fb2..0d743de 100644
--- a/src/MiniValidation/TypeDetailsCache.cs
+++ b/src/MiniValidation/TypeDetailsCache.cs
@@ -14,6 +14,21 @@ internal class TypeDetailsCache
private static readonly PropertyDetails[] _emptyPropertyDetails = Array.Empty();
private readonly ConcurrentDictionary _cache = new();
+ public TypeDetailsCache()
+ {
+ TypeDescriptor.Refreshed += args =>
+ {
+ if (args.TypeChanged is { } type)
+ {
+ _cache.TryRemove(type, out _);
+ }
+ else
+ {
+ _cache.Clear();
+ }
+ };
+ }
+
public (PropertyDetails[] Properties, bool RequiresAsync) Get(Type? type)
{
if (type is null)
@@ -21,12 +36,13 @@ internal class TypeDetailsCache
return (_emptyPropertyDetails, false);
}
- if (!_cache.ContainsKey(type))
+ (PropertyDetails[] Properties, bool RequiresAsync) details;
+ while (!_cache.TryGetValue(type, out details))
{
Visit(type);
}
- return _cache[type];
+ return details;
}
private void Visit(Type type)
diff --git a/tests/MiniValidation.Benchmarks/MiniValidation.Benchmarks.csproj b/tests/MiniValidation.Benchmarks/MiniValidation.Benchmarks.csproj
index e142f3c..87f0f67 100644
--- a/tests/MiniValidation.Benchmarks/MiniValidation.Benchmarks.csproj
+++ b/tests/MiniValidation.Benchmarks/MiniValidation.Benchmarks.csproj
@@ -2,8 +2,7 @@
Exe
- net6.0;net7.0
- net471;net6.0;net7.0
+ net8.0;net9.0;net10.0
enable
enable
10
diff --git a/tests/MiniValidation.UnitTests/MiniValidation.UnitTests.csproj b/tests/MiniValidation.UnitTests/MiniValidation.UnitTests.csproj
index 0e7f43a..a640ad8 100644
--- a/tests/MiniValidation.UnitTests/MiniValidation.UnitTests.csproj
+++ b/tests/MiniValidation.UnitTests/MiniValidation.UnitTests.csproj
@@ -1,8 +1,7 @@
- net6.0;net7.0;net8.0
- net471;net6.0;net7.0;net8.0
+ net8.0;net9.0;net10.0
10.0
enable
enable
diff --git a/tests/MiniValidation.UnitTests/Recursion.cs b/tests/MiniValidation.UnitTests/Recursion.cs
index c43d394..eac9ff9 100644
--- a/tests/MiniValidation.UnitTests/Recursion.cs
+++ b/tests/MiniValidation.UnitTests/Recursion.cs
@@ -160,7 +160,7 @@ public void Error_Message_Keys_For_Descendant_Enumerable_Are_Formatted_Correctly
}
[Fact]
- public void First_Error_In_Root_Enumerable_Returns_Immediately()
+ public void All_Errors_In_Root_Enumerable_Are_Reported()
{
var thingToValidate = new List
{
@@ -171,13 +171,13 @@ public void First_Error_In_Root_Enumerable_Returns_Immediately()
var result = MiniValidator.TryValidate(thingToValidate, recurse: true, out var errors);
Assert.False(result);
- Assert.Single(errors);
- var entry = Assert.Single(errors);
- Assert.Equal($"[0].{nameof(TestType.RequiredName)}", entry.Key);
+ Assert.Equal(2, errors.Count);
+ Assert.Contains($"[0].{nameof(TestType.RequiredName)}", errors.Keys);
+ Assert.Contains($"[1].{nameof(TestType.RequiredName)}", errors.Keys);
}
[Fact]
- public void First_Error_In_Descendant_Enumerable_Returns_Immediately()
+ public void All_Errors_In_Descendant_Enumerable_Are_Reported()
{
var thingToValidate = new TestType();
thingToValidate.Children.Add(new() { MinLengthFive = "123" });
@@ -186,9 +186,26 @@ public void First_Error_In_Descendant_Enumerable_Returns_Immediately()
var result = MiniValidator.TryValidate(thingToValidate, recurse: true, out var errors);
Assert.False(result);
- Assert.Single(errors);
- var entry = Assert.Single(errors);
- Assert.Equal($"{nameof(TestType.Children)}[0].{nameof(TestChildType.MinLengthFive)}", entry.Key);
+ Assert.Equal(2, errors.Count);
+ Assert.Contains($"{nameof(TestType.Children)}[0].{nameof(TestChildType.MinLengthFive)}", errors.Keys);
+ Assert.Contains($"{nameof(TestType.Children)}[1].{nameof(TestChildType.RequiredCategory)}", errors.Keys);
+ }
+
+ [Fact]
+ public void Class_Level_Errors_In_Root_Enumerable_Are_Keyed_By_Item()
+ {
+ var thingToValidate = new List
+ {
+ new() { TwentyOrMore = 12 },
+ new() { TwentyOrMore = 12 },
+ };
+
+ var result = MiniValidator.TryValidate(thingToValidate, recurse: true, out var errors);
+
+ Assert.False(result);
+ Assert.Equal(2, errors.Count);
+ Assert.Contains("[0]", errors.Keys);
+ Assert.Contains("[1]", errors.Keys);
}
[Fact]
@@ -492,4 +509,4 @@ public async Task DoesntThrow_When_Validates_Without_Recurse_And_Object_Has_Not_
Assert.True(isValid);
}
-}
\ No newline at end of file
+}
diff --git a/tests/MiniValidation.UnitTests/TestTypes.cs b/tests/MiniValidation.UnitTests/TestTypes.cs
index cd54e4e..2d8e907 100644
--- a/tests/MiniValidation.UnitTests/TestTypes.cs
+++ b/tests/MiniValidation.UnitTests/TestTypes.cs
@@ -82,6 +82,22 @@ public IEnumerable Validate(ValidationContext validationContex
}
}
+class TestTypeWithClassLevelValidation : IValidatableObject
+{
+ [Required]
+ public string? RequiredName { get; set; } = "Default";
+
+ public bool IsValid { get; set; } = true;
+
+ public IEnumerable Validate(ValidationContext validationContext)
+ {
+ if (!IsValid)
+ {
+ yield return new ValidationResult($"{validationContext.DisplayName} is invalid.");
+ }
+ }
+}
+
class TestClassWithEnumerable
{
public IEnumerable? Enumerable { get; set; }
diff --git a/tests/MiniValidation.UnitTests/TryValidate.cs b/tests/MiniValidation.UnitTests/TryValidate.cs
index 0d59e1e..0e445aa 100644
--- a/tests/MiniValidation.UnitTests/TryValidate.cs
+++ b/tests/MiniValidation.UnitTests/TryValidate.cs
@@ -264,7 +264,7 @@ public void Invalid_When_ValidatableObject_Has_Invalid_Attributes()
}
[Fact]
- public void ValidatableObject_Is_Not_Validated_When_Has_Invalid_Attributes()
+ public void ValidatableObject_Validate_Member_Errors_Are_Aggregated_With_Invalid_Attributes()
{
var thingToValidate = new TestValidatableType
{
@@ -275,8 +275,26 @@ public void ValidatableObject_Is_Not_Validated_When_Has_Invalid_Attributes()
var result = MiniValidator.TryValidate(thingToValidate, out var errors);
Assert.False(result);
- Assert.Single(errors);
- Assert.Equal(nameof(TestValidatableType.TenOrMore), errors.Keys.First());
+ Assert.Equal(2, errors.Count);
+ Assert.Contains(nameof(TestValidatableType.TenOrMore), errors.Keys);
+ Assert.Contains(nameof(TestValidatableType.TwentyOrMore), errors.Keys);
+ }
+
+ [Fact]
+ public void ValidatableObject_Validate_Class_Level_Errors_Are_Aggregated_With_Invalid_Attributes()
+ {
+ var thingToValidate = new TestTypeWithClassLevelValidation
+ {
+ RequiredName = null,
+ IsValid = false
+ };
+
+ var result = MiniValidator.TryValidate(thingToValidate, out var errors);
+
+ Assert.False(result);
+ Assert.Equal(2, errors.Count);
+ Assert.Contains(nameof(TestTypeWithClassLevelValidation.RequiredName), errors.Keys);
+ Assert.Contains("", errors.Keys);
}
[Fact]