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
5 changes: 4 additions & 1 deletion .github/workflows/_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.100",
"version": "10.0.100",
"rollForward": "latestFeature"
}
}
}
2 changes: 1 addition & 1 deletion samples/Samples.Console/Samples.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

Expand Down
2 changes: 1 addition & 1 deletion samples/Samples.Web/Samples.Web.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/MiniValidation/MiniValidation.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<Description>A minimalist validation library built atop the existing validation features in .NET's `System.ComponentModel.DataAnnotations` namespace.</Description>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<PackageTags>ComponentModel DataAnnotations validation</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
<LangVersion>10.0</LangVersion>
Expand Down
38 changes: 24 additions & 14 deletions src/MiniValidation/MiniValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ private static async Task<bool> TryValidateImpl(
}
}

if (isValid && typeof(IValidatableObject).IsAssignableFrom(targetType))
if (typeof(IValidatableObject).IsAssignableFrom(targetType))
{
var validatable = (IValidatableObject)target;

Expand All @@ -514,12 +514,11 @@ private static async Task<bool> 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;

Expand All @@ -533,8 +532,7 @@ private static async Task<bool> 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;
}
}

Expand Down Expand Up @@ -598,12 +596,7 @@ private static async Task<bool> TryValidateEnumerable(
throw;
}

isValid = await validateTask.ConfigureAwait(false);

if (!isValid)
{
break;
}
isValid = await validateTask.ConfigureAwait(false) && isValid;
index++;
}
}
Expand Down Expand Up @@ -633,10 +626,13 @@ private static IDictionary<string, string[]> MapToFinalErrorsResult(Dictionary<s
return result;
}

private static void ProcessValidationResults(IEnumerable<ValidationResult> validationResults, Dictionary<string, List<string>> errors, string? prefix)
private static bool ProcessValidationResults(IEnumerable<ValidationResult> validationResults, Dictionary<string, List<string>> errors, string? prefix)
{
var isValid = true;

foreach (var result in validationResults)
{
isValid = false;
var hasMemberNames = false;
foreach (var memberName in result.MemberNames)
{
Expand All @@ -652,14 +648,28 @@ private static void ProcessValidationResults(IEnumerable<ValidationResult> valid
if (!hasMemberNames)
{
// Class level error message
var key = "";
var key = GetClassLevelKey(prefix);
if (!errors.ContainsKey(key))
{
errors.Add(key, new());
}
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<ValidationResult> validationResults, Dictionary<string, List<string>> errors, string? prefix)
Expand Down
20 changes: 18 additions & 2 deletions src/MiniValidation/TypeDetailsCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,35 @@ internal class TypeDetailsCache
private static readonly PropertyDetails[] _emptyPropertyDetails = Array.Empty<PropertyDetails>();
private readonly ConcurrentDictionary<Type, (PropertyDetails[] Properties, bool RequiresAsync)> _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)
{
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<TargetFrameworks Condition=" $([MSBuild]::IsOsPlatform('Windows')) ">net471;net6.0;net7.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>10</LangVersion>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<TargetFrameworks Condition=" $([MSBuild]::IsOsPlatform('Windows')) ">net471;net6.0;net7.0;net8.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
35 changes: 26 additions & 9 deletions tests/MiniValidation.UnitTests/Recursion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestType>
{
Expand All @@ -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" });
Expand All @@ -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<TestClassLevelValidatableOnlyType>
{
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]
Expand Down Expand Up @@ -492,4 +509,4 @@ public async Task DoesntThrow_When_Validates_Without_Recurse_And_Object_Has_Not_

Assert.True(isValid);
}
}
}
16 changes: 16 additions & 0 deletions tests/MiniValidation.UnitTests/TestTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,22 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
}
}

class TestTypeWithClassLevelValidation : IValidatableObject
{
[Required]
public string? RequiredName { get; set; } = "Default";

public bool IsValid { get; set; } = true;

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!IsValid)
{
yield return new ValidationResult($"{validationContext.DisplayName} is invalid.");
}
}
}

class TestClassWithEnumerable<TEnumerable>
{
public IEnumerable<TEnumerable>? Enumerable { get; set; }
Expand Down
24 changes: 21 additions & 3 deletions tests/MiniValidation.UnitTests/TryValidate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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]
Expand Down
Loading