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
8 changes: 7 additions & 1 deletion src/MiniValidation/MiniValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,12 @@ private static async Task<bool> TryValidateImpl(
throw new ArgumentNullException(nameof(target));
}

var targetType = target.GetType();
if (TypeDetailsCache.IsNonValidatableType(targetType))
{
return true;
}

// Once we get to this point we have to box the target in order to track whether we've validated it or not
if (validatedObjects.ContainsKey(target))
{
Expand All @@ -384,7 +390,6 @@ private static async Task<bool> TryValidateImpl(
// Add current target to tracking dictionary in null (validating) state
validatedObjects.Add(target, null);

var targetType = target.GetType();
var (typeProperties, _) = _typeDetailsCache.Get(targetType);

var isValid = true;
Expand Down Expand Up @@ -418,6 +423,7 @@ private static async Task<bool> TryValidateImpl(
}

if (recurse && propertyValue is not null &&
!TypeDetailsCache.IsNonValidatableType(propertyValueType!) &&
(property.Recurse
|| typeof(IValidatableObject).IsAssignableFrom(propertyValueType)
|| typeof(IAsyncValidatableObject).IsAssignableFrom(propertyValueType)
Expand Down
30 changes: 27 additions & 3 deletions src/MiniValidation/TypeDetailsCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ private void Visit(Type type, HashSet<Type> visited, ref bool requiresAsync)
return;
}

if (DoNotRecurseIntoPropertiesOf(type))
if (DoNotRecurseIntoPropertiesOf(type) || IsNonValidatableType(type))
{
_cache[type] = (_emptyPropertyDetails, false);
return;
Expand Down Expand Up @@ -105,6 +105,7 @@ private void Visit(Type type, HashSet<Type> visited, ref bool requiresAsync)
validationAttributes ??= Array.Empty<ValidationAttribute>();
var hasValidationOnProperty = validationAttributes.Length > 0;
var hasSkipRecursionOnProperty = skipRecursionAttribute is not null;
var propertyTypeIsNonValidatable = IsNonValidatableType(property.PropertyType);
var enumerableType = GetEnumerableType(property.PropertyType);
if (enumerableType != null)
{
Expand All @@ -125,11 +126,12 @@ private void Visit(Type type, HashSet<Type> visited, ref bool requiresAsync)
var propertyTypeHasProperties = _cache.TryGetValue(property.PropertyType, out var typeCache) && typeCache.Properties.Length > 0;
var propertyTypeIsValidatableObject = typeof(IValidatableObject).IsAssignableFrom(property.PropertyType)
|| typeof(IAsyncValidatableObject).IsAssignableFrom(property.PropertyType);
var propertyTypeSupportsPolymorphism = !property.PropertyType.IsSealed;
var propertyTypeSupportsPolymorphism = !propertyTypeIsNonValidatable && !property.PropertyType.IsSealed;
var enumerableTypeHasProperties = enumerableType != null
&& _cache.TryGetValue(enumerableType, out var enumProperties)
&& enumProperties.Properties.Length > 0;
var recurse = (enumerableTypeHasProperties || propertyTypeHasProperties
var recurse = !propertyTypeIsNonValidatable
&& (enumerableTypeHasProperties || propertyTypeHasProperties
|| propertyTypeIsValidatableObject
|| propertyTypeSupportsPolymorphism)
&& !hasSkipRecursionOnProperty;
Expand Down Expand Up @@ -178,6 +180,28 @@ private static bool DoNotRecurseIntoPropertiesOf(Type type) =>
#endif
;

internal static bool IsNonValidatableType(Type type) =>
typeof(Delegate).IsAssignableFrom(type)
|| typeof(MemberInfo).IsAssignableFrom(type)
|| typeof(ParameterInfo).IsAssignableFrom(type)
|| typeof(Module).IsAssignableFrom(type)
|| typeof(Assembly).IsAssignableFrom(type)
|| IsKnownNonValidatableFrameworkType(type);

private static bool IsKnownNonValidatableFrameworkType(Type type)
{
var @namespace = type.Namespace;
return @namespace is not null
&& (@namespace == "System.Text.Json"
|| @namespace.StartsWith("System.Text.Json.", StringComparison.Ordinal)
|| @namespace == "Newtonsoft.Json.Linq"
|| @namespace.StartsWith("Newtonsoft.Json.Linq.", StringComparison.Ordinal)
|| @namespace == "Microsoft.AspNetCore.JsonPatch"
|| @namespace.StartsWith("Microsoft.AspNetCore.JsonPatch.", StringComparison.Ordinal)
|| @namespace == "Microsoft.AspNetCore.OData.Deltas"
|| @namespace.StartsWith("Microsoft.AspNetCore.OData.Deltas.", StringComparison.Ordinal));
}

private static (ValidationAttribute[]?, DisplayAttribute?, SkipRecursionAttribute?) GetPropertyAttributes(ParameterInfo[]? primaryCtorParameters, PropertyInfo property)
{
List<ValidationAttribute>? validationAttributes = null;
Expand Down
6 changes: 6 additions & 0 deletions tests/MiniValidation.UnitTests/FakeJValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Newtonsoft.Json.Linq;

public sealed class FakeJValue
{
public object First => throw new InvalidOperationException("Cannot access child value on Newtonsoft.Json.Linq.JValue.");
}
32 changes: 32 additions & 0 deletions tests/MiniValidation.UnitTests/Recursion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -509,4 +509,36 @@ public async Task DoesntThrow_When_Validates_Without_Recurse_And_Object_Has_Not_

Assert.True(isValid);
}

[Fact]
public void Valid_When_Model_Has_Func_Property()
{
var result = MiniValidator.TryValidate(new TestTypeWithFuncProperty(), out var errors);

Assert.True(result);
Assert.Empty(errors);
}

[Fact]
public void Valid_When_Model_Has_JsonSerializerOptions_Property()
{
var result = MiniValidator.TryValidate(new TestTypeWithJsonSerializerOptions(), out var errors);

Assert.True(result);
Assert.Empty(errors);
}

[Fact]
public void Valid_When_Object_Property_Has_JTokenLike_Value_With_Throwing_Getter()
{
var thingToValidate = new TestTypeWithObjectPayload
{
Payload = new Newtonsoft.Json.Linq.FakeJValue()
};

var result = MiniValidator.TryValidate(thingToValidate, out var errors);

Assert.True(result);
Assert.Empty(errors);
}
}
15 changes: 15 additions & 0 deletions tests/MiniValidation.UnitTests/TestTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,18 @@ class TestTypeWithNotImplementedProperty

public TestTypeForTypeDescriptor NotImplementedProperty => throw new Exception();
}

class TestTypeWithFuncProperty
{
public Func<object?> SomeFunc { get; set; } = () => null;
}

class TestTypeWithJsonSerializerOptions
{
public System.Text.Json.JsonSerializerOptions Options { get; set; } = new(System.Text.Json.JsonSerializerDefaults.Web);
}

class TestTypeWithObjectPayload
{
public object? Payload { get; set; }
}
Loading