diff --git a/src/MiniValidation/MiniValidator.cs b/src/MiniValidation/MiniValidator.cs index 9a0c531..c4296c5 100644 --- a/src/MiniValidation/MiniValidator.cs +++ b/src/MiniValidation/MiniValidator.cs @@ -371,6 +371,12 @@ private static async Task 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)) { @@ -384,7 +390,6 @@ private static async Task 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; @@ -418,6 +423,7 @@ private static async Task TryValidateImpl( } if (recurse && propertyValue is not null && + !TypeDetailsCache.IsNonValidatableType(propertyValueType!) && (property.Recurse || typeof(IValidatableObject).IsAssignableFrom(propertyValueType) || typeof(IAsyncValidatableObject).IsAssignableFrom(propertyValueType) diff --git a/src/MiniValidation/TypeDetailsCache.cs b/src/MiniValidation/TypeDetailsCache.cs index 678be62..2086de3 100644 --- a/src/MiniValidation/TypeDetailsCache.cs +++ b/src/MiniValidation/TypeDetailsCache.cs @@ -64,7 +64,7 @@ private void Visit(Type type, HashSet visited, ref bool requiresAsync) return; } - if (DoNotRecurseIntoPropertiesOf(type)) + if (DoNotRecurseIntoPropertiesOf(type) || IsNonValidatableType(type)) { _cache[type] = (_emptyPropertyDetails, false); return; @@ -105,6 +105,7 @@ private void Visit(Type type, HashSet visited, ref bool requiresAsync) validationAttributes ??= Array.Empty(); var hasValidationOnProperty = validationAttributes.Length > 0; var hasSkipRecursionOnProperty = skipRecursionAttribute is not null; + var propertyTypeIsNonValidatable = IsNonValidatableType(property.PropertyType); var enumerableType = GetEnumerableType(property.PropertyType); if (enumerableType != null) { @@ -125,11 +126,12 @@ private void Visit(Type type, HashSet 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; @@ -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? validationAttributes = null; diff --git a/tests/MiniValidation.UnitTests/FakeJValue.cs b/tests/MiniValidation.UnitTests/FakeJValue.cs new file mode 100644 index 0000000..fc2f012 --- /dev/null +++ b/tests/MiniValidation.UnitTests/FakeJValue.cs @@ -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."); +} diff --git a/tests/MiniValidation.UnitTests/Recursion.cs b/tests/MiniValidation.UnitTests/Recursion.cs index eac9ff9..169374a 100644 --- a/tests/MiniValidation.UnitTests/Recursion.cs +++ b/tests/MiniValidation.UnitTests/Recursion.cs @@ -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); + } } diff --git a/tests/MiniValidation.UnitTests/TestTypes.cs b/tests/MiniValidation.UnitTests/TestTypes.cs index 2d8e907..bc55c88 100644 --- a/tests/MiniValidation.UnitTests/TestTypes.cs +++ b/tests/MiniValidation.UnitTests/TestTypes.cs @@ -283,3 +283,18 @@ class TestTypeWithNotImplementedProperty public TestTypeForTypeDescriptor NotImplementedProperty => throw new Exception(); } + +class TestTypeWithFuncProperty +{ + public Func 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; } +}