diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationAttributeStore.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationAttributeStore.cs index cf0c70863829c..6fa0655bf6c8e 100644 --- a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationAttributeStore.cs +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/ValidationAttributeStore.cs @@ -124,8 +124,7 @@ private TypeStoreItem GetTypeStoreItem([DynamicallyAccessedMembers(TypeStoreItem { if (!_typeStoreItems.TryGetValue(type, out TypeStoreItem? item)) { - // use CustomAttributeExtensions.GetCustomAttributes() to get inherited attributes as well as direct ones - var attributes = CustomAttributeExtensions.GetCustomAttributes(type, true); + var attributes = TypeDescriptor.GetAttributes(type).Cast(); item = new TypeStoreItem(type, attributes); _typeStoreItems[type] = item; } @@ -170,7 +169,7 @@ internal StoreItem(IEnumerable attributes) /// private sealed class TypeStoreItem : StoreItem { - internal const DynamicallyAccessedMemberTypes DynamicallyAccessedTypes = DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties; + internal const DynamicallyAccessedMemberTypes DynamicallyAccessedTypes = DynamicallyAccessedMemberTypes.All; private readonly object _syncRoot = new object(); [DynamicallyAccessedMembers(DynamicallyAccessedTypes)] @@ -183,6 +182,7 @@ internal TypeStoreItem([DynamicallyAccessedMembers(DynamicallyAccessedTypes)] Ty _type = type; } + [RequiresUnreferencedCode("The Types of _type's properties cannot be statically discovered.")] internal PropertyStoreItem GetPropertyStoreItem(string propertyName) { if (!TryGetPropertyStoreItem(propertyName, out PropertyStoreItem? item)) @@ -194,6 +194,7 @@ internal PropertyStoreItem GetPropertyStoreItem(string propertyName) return item; } + [RequiresUnreferencedCode("The Types of _type's properties cannot be statically discovered.")] internal bool TryGetPropertyStoreItem(string propertyName, [NotNullWhen(true)] out PropertyStoreItem? item) { if (string.IsNullOrEmpty(propertyName)) @@ -215,23 +216,51 @@ internal bool TryGetPropertyStoreItem(string propertyName, [NotNullWhen(true)] o return _propertyStoreItems.TryGetValue(propertyName, out item); } + [RequiresUnreferencedCode("The Types of _type's properties cannot be statically discovered.")] private Dictionary CreatePropertyStoreItems() { var propertyStoreItems = new Dictionary(); - // exclude index properties to match old TypeDescriptor functionality - var properties = _type.GetRuntimeProperties() - .Where(prop => IsPublic(prop) && !prop.GetIndexParameters().Any()); - foreach (PropertyInfo property in properties) + var properties = TypeDescriptor.GetProperties(_type); + foreach (PropertyDescriptor property in properties) { - // use CustomAttributeExtensions.GetCustomAttributes() to get inherited attributes as well as direct ones - var item = new PropertyStoreItem(property.PropertyType, - CustomAttributeExtensions.GetCustomAttributes(property, true)); + var item = new PropertyStoreItem(property.PropertyType, GetExplicitAttributes(property).Cast()); propertyStoreItems[property.Name] = item; } return propertyStoreItems; } + + /// + /// Method to extract only the explicitly specified attributes from a + /// + /// + /// Normal TypeDescriptor semantics are to inherit the attributes of a property's type. This method + /// exists to suppress those inherited attributes. + /// + /// The property descriptor whose attributes are needed. + /// A new stripped of any attributes from the property's type. + [RequiresUnreferencedCode("The Type of propertyDescriptor.PropertyType cannot be statically discovered.")] + private AttributeCollection GetExplicitAttributes(PropertyDescriptor propertyDescriptor) + { + List attributes = new List(propertyDescriptor.Attributes.Cast()); + IEnumerable typeAttributes = TypeDescriptor.GetAttributes(propertyDescriptor.PropertyType).Cast(); + bool removedAttribute = false; + foreach (Attribute attr in typeAttributes) + { + for (int i = attributes.Count - 1; i >= 0; --i) + { + // We must use ReferenceEquals since attributes could Match if they are the same. + // Only ReferenceEquals will catch actual duplications. + if (object.ReferenceEquals(attr, attributes[i])) + { + attributes.RemoveAt(i); + removedAttribute = true; + } + } + } + return removedAttribute ? new AttributeCollection(attributes.ToArray()) : propertyDescriptor.Attributes; + } } /// diff --git a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Validator.cs b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Validator.cs index d9a874eb1dba7..c702de859bd40 100644 --- a/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Validator.cs +++ b/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations/Validator.cs @@ -514,17 +514,16 @@ private static IEnumerable GetObjectPropertyValidationErrors(ob private static ICollection> GetPropertyValues(object instance, ValidationContext validationContext) { - var properties = instance.GetType().GetRuntimeProperties() - .Where(p => ValidationAttributeStore.IsPublic(p) && !p.GetIndexParameters().Any()); - var items = new List>(properties.Count()); - foreach (var property in properties) + var properties = TypeDescriptor.GetProperties(instance); + var items = new List>(properties.Count); + foreach (PropertyDescriptor property in properties) { var context = CreateValidationContext(instance, validationContext); context.MemberName = property.Name; if (_store.GetPropertyValidationAttributes(context).Any()) { - items.Add(new KeyValuePair(context, property.GetValue(instance, null))); + items.Add(new KeyValuePair(context, property.GetValue(instance))); } } diff --git a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/ValidatorTests.cs b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/ValidatorTests.cs index d66e36a6e182f..aef154229d516 100644 --- a/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/ValidatorTests.cs +++ b/src/libraries/System.ComponentModel.Annotations/tests/System/ComponentModel/DataAnnotations/ValidatorTests.cs @@ -231,6 +231,108 @@ public void TryValidateObject_RequiredNull_Error() Assert.Contains("Required", Assert.Single(results).ErrorMessage); } + [Fact] + public static void TryValidateObject_returns_false_if_all_properties_are_valid_but_metadatatype_class_property_required_attribute_fails_validation() + { + var objectToBeValidated = new HasMetadataTypeToBeValidated() + { + PropertyToBeTested = "Valid Value" + }; + var validationContext = new ValidationContext(objectToBeValidated); + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeToBeValidated), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeToBeValidated)); + + var validationResults = new List(); + Assert.False(Validator.TryValidateObject(objectToBeValidated, validationContext, validationResults, true)); + Assert.Equal(1, validationResults.Count); + Assert.Equal("The SecondPropertyToBeTested field is required.", validationResults[0].ErrorMessage); + } + + [Fact] + public static void TryValidateObject_returns_false_if_all_properties_are_valid_but_metadatatype_class_property_attribute_fails_validation() + { + var objectToBeValidated = new HasMetadataTypeToBeValidated() + { + PropertyToBeTested = "Valid Value", + SecondPropertyToBeTested = "Invalid Value" + }; + var validationContext = new ValidationContext(objectToBeValidated); + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeToBeValidated), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeToBeValidated)); + + var validationResults = new List(); + Assert.False(Validator.TryValidateObject(objectToBeValidated, validationContext, validationResults, true)); + Assert.Equal(1, validationResults.Count); + Assert.Equal("The field SecondPropertyToBeTested must be a string or array type with a maximum length of '11'.", validationResults[0].ErrorMessage); + } + + [Fact] + public static void TryValidateObject_returns_false_if_all_properties_are_valid_but_metadatatype_class_has_unmatched_property_name() + { + var objectToBeValidated = new HasMetadataTypeWithUnmatchedProperties() + { + PropertyToBeTested = "Valid Value" + }; + var validationContext = new ValidationContext(objectToBeValidated); + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeWithUnmatchedProperties), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeWithUnmatchedProperties)); + + var validationResults = new List(); + var exception = Assert.Throws( + () => Validator.TryValidateObject(objectToBeValidated, validationContext, validationResults, true)); + Assert.Equal("The associated metadata type for type 'System.ComponentModel.DataAnnotations.Tests.ValidatorTests+HasMetadataTypeWithUnmatchedProperties' contains the following unknown properties or fields: SecondPropertyToBeTested. Please make sure that the names of these members match the names of the properties on the main type.", + exception.Message); + } + + [Fact] + public static void TryValidateObject_returns_false_if_property_attribute_is_not_removed_by_metadatatype_class() + { + var objectToBeValidated = new HasMetadataTypeToBeValidated() + { + PropertyToBeTested = "Invalid Value" + }; + var validationContext = new ValidationContext(objectToBeValidated); + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeToBeValidated), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeToBeValidated)); + + var validationResults = new List(); + Assert.False(Validator.TryValidateObject(objectToBeValidated, validationContext, validationResults, true)); + Assert.Equal(2, validationResults.Count); + Assert.Contains(validationResults, x => x.ErrorMessage == "ValidValueStringPropertyAttribute.IsValid failed for value Invalid Value"); + Assert.Contains(validationResults, x => x.ErrorMessage == "The SecondPropertyToBeTested field is required."); + } + + [Fact] + public static void TryValidateObject_returns_false_if_property_has_attributes_from_base_and_metadatatype_classes() + { + var objectToBeValidated = new HasMetadataTypeWithComplementaryRequirements() + { + SecondPropertyToBeTested = "Invalid Value" + }; + var validationContext = new ValidationContext(objectToBeValidated); + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeWithComplementaryRequirements), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeWithComplementaryRequirements)); + + var validationResults = new List(); + Assert.False(Validator.TryValidateObject(objectToBeValidated, validationContext, validationResults, true)); + Assert.Equal(2, validationResults.Count); + Assert.Contains(validationResults, x => x.ErrorMessage == "The SecondPropertyToBeTested field is not a valid phone number."); + Assert.Contains(validationResults, x => x.ErrorMessage == "The field SecondPropertyToBeTested must be a string or array type with a maximum length of '11'."); + } + + [Fact] + public static void TryValidateObject_returns_false_if_validation_fails_when_class_references_itself_as_a_metadatatype() + { + var objectToBeValidated = new SelfMetadataType() + { + PropertyToBeTested = "Invalid Value", + SecondPropertyToBeTested = "Invalid Value" + }; + var validationContext = new ValidationContext(objectToBeValidated); + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(SelfMetadataType), typeof(SelfMetadataType)), typeof(SelfMetadataType)); + + var validationResults = new List(); + Assert.False(Validator.TryValidateObject(objectToBeValidated, validationContext, validationResults, true)); + Assert.Equal(2, validationResults.Count); + Assert.Contains(validationResults, x => x.ErrorMessage == "ValidValueStringPropertyAttribute.IsValid failed for value Invalid Value"); + Assert.Contains(validationResults, x => x.ErrorMessage == "The SecondPropertyToBeTested field is not a valid phone number."); + } + public class RequiredFailure { [Required] @@ -365,6 +467,141 @@ public void ValidateObject_IValidatableObject_Null() Validator.ValidateObject(instance, context); } + + [Fact] + public static void ValidateObject_returns_false_if_all_properties_are_valid_but_metadatatype_class_property_required_attribute_fails_validation() + { + var objectToBeValidated = new HasMetadataTypeToBeValidated() + { + PropertyToBeTested = "Valid Value" + }; + var validationContext = new ValidationContext(objectToBeValidated); + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeToBeValidated), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeToBeValidated)); + + var exception = Assert.Throws( + () => Validator.ValidateObject(objectToBeValidated, validationContext, true)); + Assert.Equal("The SecondPropertyToBeTested field is required.", exception.ValidationResult.ErrorMessage); + } + + [Fact] + public static void ValidateObject_returns_false_if_all_properties_are_valid_but_metadatatype_class_property_attribute_fails_validation() + { + var objectToBeValidated = new HasMetadataTypeToBeValidated() + { + PropertyToBeTested = "Valid Value", + SecondPropertyToBeTested = "Invalid Value" + }; + var validationContext = new ValidationContext(objectToBeValidated); + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeToBeValidated), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeToBeValidated)); + + var exception = Assert.Throws( + () => Validator.ValidateObject(objectToBeValidated, validationContext, true)); + Assert.Equal("The field SecondPropertyToBeTested must be a string or array type with a maximum length of '11'.", exception.ValidationResult.ErrorMessage); + } + + [Fact] + public static void ValidateObject_returns_false_if_all_properties_are_valid_but_metadatatype_class_type_attribute_fails_validation() + { + var objectToBeValidated = new HasMetadataTypeToBeValidated() + { + PropertyToBeTested = "Valid Value", + SecondPropertyToBeTested = "TypeInvalid" + }; + var validationContext = new ValidationContext(objectToBeValidated); + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeToBeValidated), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeToBeValidated)); + + var exception = Assert.Throws( + () => Validator.ValidateObject(objectToBeValidated, validationContext, true)); + Assert.Equal("The SecondPropertyToBeTested field mustn't be \"TypeInvalid\".", exception.ValidationResult.ErrorMessage); + } + + [Fact] + public static void ValidateObject_returns_false_if_all_properties_are_valid_but_metadatatype_class_has_unmatched_property_name() + { + var objectToBeValidated = new HasMetadataTypeWithUnmatchedProperties() + { + PropertyToBeTested = "Valid Value" + }; + var validationContext = new ValidationContext(objectToBeValidated); + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeWithUnmatchedProperties), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeWithUnmatchedProperties)); + + var exception = Assert.Throws( + () => Validator.ValidateObject(objectToBeValidated, validationContext, true)); + Assert.Equal("The associated metadata type for type 'System.ComponentModel.DataAnnotations.Tests.ValidatorTests+HasMetadataTypeWithUnmatchedProperties' contains the following unknown properties or fields: SecondPropertyToBeTested. Please make sure that the names of these members match the names of the properties on the main type.", + exception.Message); + } + + [Fact] + public static void ValidateObject_returns_false_if_property_attribute_is_not_removed_by_metadatatype_class() + { + var objectToBeValidated = new HasMetadataTypeToBeValidated() + { + PropertyToBeTested = "Invalid Value" + }; + var validationContext = new ValidationContext(objectToBeValidated); + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeToBeValidated), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeToBeValidated)); + + var exception = Assert.Throws( + () => Validator.ValidateObject(objectToBeValidated, validationContext, true)); + Assert.Equal("ValidValueStringPropertyAttribute.IsValid failed for value Invalid Value", + exception.Message); + } + + [Fact] + public static void ValidateObject_returns_false_if_property_has_attributes_from_base_and_metadatatype_classes() + { + var objectToBeValidated = new HasMetadataTypeWithComplementaryRequirements() + { + PropertyToBeTested = "Invalid Value" + }; + var validationContext = new ValidationContext(objectToBeValidated); + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeWithComplementaryRequirements), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeWithComplementaryRequirements)); + + var exception = Assert.Throws( + () => Validator.ValidateObject(objectToBeValidated, validationContext, true)); + Assert.Equal("ValidValueStringPropertyAttribute.IsValid failed for value Invalid Value", + exception.Message); + + objectToBeValidated.PropertyToBeTested = null; + objectToBeValidated.SecondPropertyToBeTested = "Not Phone #"; + + exception = Assert.Throws( + () => Validator.ValidateObject(objectToBeValidated, validationContext, true)); + Assert.Equal("The SecondPropertyToBeTested field is not a valid phone number.", + exception.Message); + + objectToBeValidated.SecondPropertyToBeTested = "0800123456789"; + + exception = Assert.Throws( + () => Validator.ValidateObject(objectToBeValidated, validationContext, true)); + Assert.Equal("The field SecondPropertyToBeTested must be a string or array type with a maximum length of '11'.", + exception.Message); + } + + [Fact] + public static void ValidateObject_returns_false_if_validation_fails_when_class_references_itself_as_a_metadatatype() + { + var objectToBeValidated = new SelfMetadataType() + { + PropertyToBeTested = "Invalid Value" + }; + var validationContext = new ValidationContext(objectToBeValidated); + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(SelfMetadataType), typeof(SelfMetadataType)), typeof(SelfMetadataType)); + + var exception = Assert.Throws( + () => Validator.ValidateObject(objectToBeValidated, validationContext, true)); + Assert.Equal("ValidValueStringPropertyAttribute.IsValid failed for value Invalid Value", + exception.Message); + + objectToBeValidated.PropertyToBeTested = null; + objectToBeValidated.SecondPropertyToBeTested = "Not Phone #"; + + exception = Assert.Throws( + () => Validator.ValidateObject(objectToBeValidated, validationContext, true)); + Assert.Equal("The SecondPropertyToBeTested field is not a valid phone number.", + exception.Message); + } + #endregion ValidateObject #region TryValidateProperty @@ -521,6 +758,84 @@ public static void TryValidateProperty_returns_true_if_all_attributes_are_valid( Assert.Equal(0, validationResults.Count); } + [Fact] + public static void TryValidateProperty_returns_false_if_all_properties_are_valid_but_metadatatype_class_property_required_attribute_fails_validation() + { + var validationContext = new ValidationContext(new HasMetadataTypeToBeValidated()); + validationContext.MemberName = "SecondPropertyToBeTested"; + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeToBeValidated), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeToBeValidated)); + Assert.False(Validator.TryValidateProperty(null, validationContext, null)); + + var validationResults = new List(); + Assert.False(Validator.TryValidateProperty(null, validationContext, validationResults)); + Assert.Equal(1, validationResults.Count); + Assert.Equal("The SecondPropertyToBeTested field is required.", validationResults[0].ErrorMessage); + } + + [Fact] + public static void TryValidateProperty_returns_false_if_all_properties_are_valid_but_metadatatype_class_property_attribute_fails_validation() + { + var validationContext = new ValidationContext(new HasMetadataTypeToBeValidated()); + validationContext.MemberName = "SecondPropertyToBeTested"; + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeToBeValidated), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeToBeValidated)); + Assert.False(Validator.TryValidateProperty("Invalid Value", validationContext, null)); + + var validationResults = new List(); + Assert.False(Validator.TryValidateProperty("Invalid Value", validationContext, validationResults)); + Assert.Equal(1, validationResults.Count); + Assert.Equal("The field SecondPropertyToBeTested must be a string or array type with a maximum length of '11'.", validationResults[0].ErrorMessage); + } + + [Fact] + public static void TryValidateProperty_returns_true_if_property_attribute_is_not_removed_by_metadatatype_class() + { + var validationContext = new ValidationContext(new HasMetadataTypeToBeValidated()); + validationContext.MemberName = "PropertyToBeTested"; + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeToBeValidated), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeToBeValidated)); + Assert.False(Validator.TryValidateProperty("Invalid Value", validationContext, null)); + + var validationResults = new List(); + Assert.False(Validator.TryValidateProperty("Invalid Value", validationContext, validationResults)); + Assert.Equal("ValidValueStringPropertyAttribute.IsValid failed for value Invalid Value", validationResults[0].ErrorMessage); + } + + [Fact] + public static void TryValidateProperty_returns_true_if_property_has_attributes_from_base_and_metadatatype_classes() + { + var validationContext = new ValidationContext(new HasMetadataTypeWithComplementaryRequirements()); + validationContext.MemberName = "SecondPropertyToBeTested"; + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeWithComplementaryRequirements), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeWithComplementaryRequirements)); + Assert.False(Validator.TryValidateProperty("Invalid Value", validationContext, null)); + + var validationResults = new List(); + Assert.False(Validator.TryValidateProperty("Invalid Value", validationContext, validationResults)); + Assert.Equal(2, validationResults.Count); + Assert.Contains(validationResults, x => x.ErrorMessage == "The SecondPropertyToBeTested field is not a valid phone number."); + Assert.Contains(validationResults, x => x.ErrorMessage == "The field SecondPropertyToBeTested must be a string or array type with a maximum length of '11'."); + } + + [Fact] + public static void TryValidateProperty_returns_false_if_validation_fails_when_class_references_itself_as_a_metadatatype() + { + var validationContext = new ValidationContext(new SelfMetadataType()); + validationContext.MemberName = "PropertyToBeTested"; + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(SelfMetadataType), typeof(SelfMetadataType)), typeof(SelfMetadataType)); + Assert.False(Validator.TryValidateProperty("Invalid Value", validationContext, null)); + + var validationResults = new List(); + Assert.False(Validator.TryValidateProperty("Invalid Value", validationContext, validationResults)); + Assert.Equal(1, validationResults.Count); + Assert.Contains(validationResults, x => x.ErrorMessage == "ValidValueStringPropertyAttribute.IsValid failed for value Invalid Value"); + + validationContext.MemberName = "SecondPropertyToBeTested"; + Assert.False(Validator.TryValidateProperty("Invalid Value", validationContext, null)); + + validationResults.Clear(); + Assert.False(Validator.TryValidateProperty("Invalid Value", validationContext, validationResults)); + //Assert.Equal(1, validationResults.Count); + Assert.Contains(validationResults, x => x.ErrorMessage == "The SecondPropertyToBeTested field is not a valid phone number."); + } + #endregion TryValidateProperty #region ValidateProperty @@ -653,6 +968,89 @@ public static void ValidateProperty_succeeds_if_all_attributes_are_valid() Validator.ValidateProperty("Valid Value", validationContext); } + [Fact] + public static void ValidateProperty_returns_false_if_all_properties_are_valid_but_metadatatype_class_property_required_attribute_fails_validation() + { + var validationContext = new ValidationContext(new HasMetadataTypeToBeValidated()); + validationContext.MemberName = "SecondPropertyToBeTested"; + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeToBeValidated), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeToBeValidated)); + var exception = Assert.Throws( + () => Validator.ValidateProperty(null, validationContext)); + Assert.IsType(exception.ValidationAttribute); + Assert.Null(exception.Value); + } + + [Fact] + public static void ValidateProperty_returns_false_if_all_properties_are_valid_but_metadatatype_class_property_attribute_fails_validation() + { + var validationContext = new ValidationContext(new HasMetadataTypeToBeValidated()); + validationContext.MemberName = "SecondPropertyToBeTested"; + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeToBeValidated), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeToBeValidated)); + var exception = Assert.Throws( + () => Validator.ValidateProperty("Invalid Value", validationContext)); + Assert.IsType(exception.ValidationAttribute); + Assert.Equal("The field SecondPropertyToBeTested must be a string or array type with a maximum length of '11'.", exception.ValidationResult.ErrorMessage); + } + + [Fact] + public static void ValidateProperty_returns_false_if_property_attribute_is_not_removed_by_metadatatype_class() + { + var validationContext = new ValidationContext(new HasMetadataTypeToBeValidated()); + validationContext.MemberName = "PropertyToBeTested"; + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeToBeValidated), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeToBeValidated)); + var exception = Assert.Throws( + () => Validator.ValidateProperty("Invalid Value", validationContext)); + Assert.IsType(exception.ValidationAttribute); + Assert.Equal("ValidValueStringPropertyAttribute.IsValid failed for value Invalid Value", exception.ValidationResult.ErrorMessage); + Assert.Equal("Invalid Value", exception.Value); + } + + [Fact] + public static void ValidateProperty_returns_false_if_property_has_attributes_from_base_and_metadatatype_classes() + { + var validationContext = new ValidationContext(new HasMetadataTypeWithComplementaryRequirements()); + validationContext.MemberName = "PropertyToBeTested"; + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(HasMetadataTypeWithComplementaryRequirements), typeof(MetadataTypeToAddValidationAttributes)), typeof(HasMetadataTypeWithComplementaryRequirements)); + var exception = Assert.Throws( + () => Validator.ValidateProperty("Invalid Value", validationContext)); + Assert.IsType(exception.ValidationAttribute); + Assert.Equal("ValidValueStringPropertyAttribute.IsValid failed for value Invalid Value", exception.ValidationResult.ErrorMessage); + Assert.Equal("Invalid Value", exception.Value); + + validationContext.MemberName = "SecondPropertyToBeTested"; + exception = Assert.Throws( + () => Validator.ValidateProperty("Not Phone #", validationContext)); + Assert.IsType(exception.ValidationAttribute); + Assert.Equal("The SecondPropertyToBeTested field is not a valid phone number.", exception.ValidationResult.ErrorMessage); + Assert.Equal("Not Phone #", exception.Value); + + exception = Assert.Throws( + () => Validator.ValidateProperty("0800123456789", validationContext)); + Assert.IsType(exception.ValidationAttribute); + Assert.Equal("The field SecondPropertyToBeTested must be a string or array type with a maximum length of '11'.", exception.ValidationResult.ErrorMessage); + Assert.Equal("0800123456789", exception.Value); + } + + [Fact] + public static void ValidateProperty_returns_false_if_validation_fails_when_class_references_itself_as_a_metadatatype() + { + var validationContext = new ValidationContext(new SelfMetadataType()); + validationContext.MemberName = "PropertyToBeTested"; + TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(SelfMetadataType), typeof(SelfMetadataType)), typeof(SelfMetadataType)); + var exception = Assert.Throws( + () => Validator.ValidateProperty("Invalid Value", validationContext)); + Assert.IsType(exception.ValidationAttribute); + Assert.Equal("ValidValueStringPropertyAttribute.IsValid failed for value Invalid Value", exception.ValidationResult.ErrorMessage); + Assert.Equal("Invalid Value", exception.Value); + + validationContext.MemberName = "SecondPropertyToBeTested"; + exception = Assert.Throws( + () => Validator.ValidateProperty("Invalid Value", validationContext)); + Assert.IsType(exception.ValidationAttribute); + Assert.Equal("The SecondPropertyToBeTested field is not a valid phone number.", exception.ValidationResult.ErrorMessage); + Assert.Equal("Invalid Value", exception.Value); + } + #endregion ValidateProperty #region TryValidateValue @@ -997,5 +1395,52 @@ public class InvalidToBeValidated [ValidValueStringProperty] public string PropertyWithRequiredAttribute { get; set; } } + + public class HasMetadataTypeToBeValidated + { + [ValidValueStringProperty] + public string PropertyToBeTested { get; set; } + + public string SecondPropertyToBeTested { get; set; } + } + + public class HasMetadataTypeWithUnmatchedProperties + { + [ValidValueStringProperty] + public string PropertyToBeTested { get; set; } + + public string MismatchedNameProperty { get; set; } + } + + public class HasMetadataTypeWithComplementaryRequirements + { + [ValidValueStringProperty] + public string PropertyToBeTested { get; set; } + + [Phone] + public string SecondPropertyToBeTested { get; set; } + } + + public class SelfMetadataType + { + [ValidValueStringProperty] + public string PropertyToBeTested { get; set; } + + [Phone] + public string SecondPropertyToBeTested { get; set; } + } + + [CustomValidation(typeof(MetadataTypeToAddValidationAttributes), nameof(Validate))] + public class MetadataTypeToAddValidationAttributes + { + [Required] + [MaxLength(11)] + public string SecondPropertyToBeTested { get; set; } + + public static ValidationResult Validate(HasMetadataTypeToBeValidated value) + => value.SecondPropertyToBeTested == "TypeInvalid" + ? new ValidationResult("The SecondPropertyToBeTested field mustn't be \"TypeInvalid\".") + : ValidationResult.Success; + } } }