diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs index 94655e576a..d84900b92b 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/AssociatedMetadataProvider.cs @@ -157,7 +157,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private TModelMetadata CreateTypeInformation(Type type, IEnumerable associatedAttributes) { - var attributes = type.GetTypeInfo().GetCustomAttributes(); + var attributes = ModelAttributes.GetAttributesForType(type); if (associatedAttributes != null) { attributes = attributes.Concat(associatedAttributes); @@ -169,10 +169,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding private PropertyInformation CreatePropertyInformation(Type containerType, PropertyHelper helper) { var property = helper.Property; + var attributes = ModelAttributes.GetAttributesForProperty(containerType, property); + return new PropertyInformation { PropertyHelper = helper, - Prototype = CreateMetadataPrototype(ModelAttributes.GetAttributesForProperty(property), + Prototype = CreateMetadataPrototype(attributes, containerType, property.PropertyType, property.Name), diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelAttributes.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelAttributes.cs index 57f3d25f23..63cc0fa7e9 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelAttributes.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelAttributes.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -32,17 +33,57 @@ namespace Microsoft.AspNet.Mvc.ModelBinding /// /// Gets the attributes for the given . /// - /// A for which attributes need to be resolved. + /// The in which caller found . + /// + /// A for which attributes need to be resolved. /// /// An containing the attributes on the /// before the attributes on the type. - public static IEnumerable GetAttributesForProperty(PropertyInfo property) + public static IEnumerable GetAttributesForProperty([NotNull] Type type, [NotNull] PropertyInfo property) { // Return the property attributes first. var propertyAttributes = property.GetCustomAttributes(); var typeAttributes = property.PropertyType.GetTypeInfo().GetCustomAttributes(); - return propertyAttributes.Concat(typeAttributes); + propertyAttributes = propertyAttributes.Concat(typeAttributes); + + var modelMedatadataType = type.GetTypeInfo().GetCustomAttribute(); + if (modelMedatadataType != null) + { + var modelMedatadataProperty = modelMedatadataType.MetadataType.GetRuntimeProperty(property.Name); + if (modelMedatadataProperty != null) + { + var modelMedatadataAttributes = modelMedatadataProperty.GetCustomAttributes(); + propertyAttributes = propertyAttributes.Concat(modelMedatadataAttributes); + + var modelMetadataTypeAttributes = + modelMedatadataProperty.PropertyType.GetTypeInfo().GetCustomAttributes(); + propertyAttributes = propertyAttributes.Concat(modelMetadataTypeAttributes); + } + } + + return propertyAttributes; + } + + /// + /// Gets the attributes for the given . + /// + /// The for which attributes need to be resolved. + /// + /// An containing the attributes on the + /// . + public static IEnumerable GetAttributesForType([NotNull] Type type) + { + var attributes = type.GetTypeInfo().GetCustomAttributes(); + + var modelMedatadataType = type.GetTypeInfo().GetCustomAttribute(); + if (modelMedatadataType != null) + { + var modelMedatadataAttributes = modelMedatadataType.MetadataType.GetTypeInfo().GetCustomAttributes(); + attributes = attributes.Concat(modelMedatadataAttributes); + } + + return attributes; } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj b/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj index e343179838..a78b65bd1c 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Microsoft.AspNet.Mvc.ModelBinding.kproj @@ -14,9 +14,4 @@ 2.0 - - - - - \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/ModelMetadataTypeAttribute.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelMetadataTypeAttribute.cs new file mode 100644 index 0000000000..96f78ee930 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/ModelMetadataTypeAttribute.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// This attribute specifies the metadata class to associate with a data model class. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class ModelMetadataTypeAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The type of metadata class that is associated with a data model class. + public ModelMetadataTypeAttribute([NotNull] Type type) + { + MetadataType = type; + } + + /// + /// Gets the type of metadata class that is associated with a data model class. + /// + public Type MetadataType { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/AssociatedValidatorProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/AssociatedValidatorProvider.cs index 38f2aeb4a9..97f515fa9f 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/AssociatedValidatorProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/AssociatedValidatorProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -20,7 +21,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } protected abstract IEnumerable GetValidators(ModelMetadata metadata, - IEnumerable attributes); + IEnumerable attributes); private IEnumerable GetValidatorsForProperty(ModelMetadata metadata) { @@ -38,15 +39,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding "metadata"); } - var attributes = property.GetCustomAttributes(); + var attributes = ModelAttributes.GetAttributesForProperty(metadata.ContainerType, property); return GetValidators(metadata, attributes); } private IEnumerable GetValidatorsForType(ModelMetadata metadata) { - var attributes = metadata.ModelType - .GetTypeInfo() - .GetCustomAttributes(); + var attributes = ModelAttributes.GetAttributesForType(metadata.ModelType); + return GetValidators(metadata, attributes); } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorProvider.cs index d7d4e554a1..1231db90ef 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidatorProvider.cs @@ -49,7 +49,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } protected override IEnumerable GetValidators(ModelMetadata metadata, - IEnumerable attributes) + IEnumerable attributes) { var results = new List(); diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataMemberModelValidatorProvider.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataMemberModelValidatorProvider.cs index 9d7b738ec1..e5ecc1132a 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataMemberModelValidatorProvider.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataMemberModelValidatorProvider.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public class DataMemberModelValidatorProvider : AssociatedValidatorProvider { protected override IEnumerable GetValidators(ModelMetadata metadata, - IEnumerable attributes) + IEnumerable attributes) { // Types cannot be required; only properties can if (metadata.ContainerType == null || string.IsNullOrEmpty(metadata.PropertyName)) @@ -32,7 +32,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding return Enumerable.Empty(); } - internal static bool IsRequiredDataMember(Type containerType, IEnumerable attributes) + internal static bool IsRequiredDataMember(Type containerType, IEnumerable attributes) { var dataMemberAttribute = attributes.OfType() .FirstOrDefault(); diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataAttributesTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataAttributesTest.cs new file mode 100644 index 0000000000..01859e7b27 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Metadata/ModelMetadataAttributesTest.cs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ModelBinding +{ + public class ModelMetadataAttributesTest + { + [Fact] + public void GetAttributesForBaseProperty_IncludesMetadataAttributes() + { + // Arrange + var modelType = typeof(BaseViewModel); + var property = modelType.GetRuntimeProperties().FirstOrDefault(p => p.Name == "BaseProperty"); + + // Act + var attributes = ModelAttributes.GetAttributesForProperty(modelType, property); + + // Assert + Assert.Single(attributes.OfType()); + Assert.Single(attributes.OfType()); + } + + [Fact] + public void GetAttributesForTestProperty_ModelOverridesMetadataAttributes() + { + // Arrange + var modelType = typeof(BaseViewModel); + var property = modelType.GetRuntimeProperties().FirstOrDefault(p => p.Name == "TestProperty"); + + // Act + var attributes = ModelAttributes.GetAttributesForProperty(modelType, property); + + // Assert + var rangeAttribute = attributes.OfType().FirstOrDefault(); + Assert.NotNull(rangeAttribute); + Assert.Equal(0, (int)rangeAttribute.Minimum); + Assert.Equal(10, (int)rangeAttribute.Maximum); + Assert.Single(attributes.OfType()); + } + + [Fact] + public void GetAttributesForBasePropertyFromDerivedModel_IncludesMetadataAttributes() + { + // Arrange + var modelType = typeof(DerivedViewModel); + var property = modelType.GetRuntimeProperties().FirstOrDefault(p => p.Name == "BaseProperty"); + + // Act + var attributes = ModelAttributes.GetAttributesForProperty(modelType, property); + + // Assert + Assert.Single(attributes.OfType()); + Assert.Single(attributes.OfType()); + } + + [Fact] + public void GetAttributesForTestPropertyFromDerived_IncludesMetadataAttributes() + { + // Arrange + var modelType = typeof(DerivedViewModel); + var property = modelType.GetRuntimeProperties().FirstOrDefault(p => p.Name == "TestProperty"); + + // Act + var attributes = ModelAttributes.GetAttributesForProperty(modelType, property); + + // Assert + Assert.Single(attributes.OfType()); + Assert.Single(attributes.OfType()); + Assert.DoesNotContain(typeof(RangeAttribute), attributes); + } + + [Fact] + public void GetAttributesForVirtualPropertyFromDerived_IncludesMetadataAttributes() + { + // Arrange + var modelType = typeof(DerivedViewModel); + var property = modelType.GetRuntimeProperties().FirstOrDefault(p => p.Name == "VirtualProperty"); + + // Act + var attributes = ModelAttributes.GetAttributesForProperty(modelType, property); + + // Assert + Assert.Single(attributes.OfType()); + Assert.Single(attributes.OfType()); + } + + [Fact] + public void GetFromServiceAttributeFromBase_IncludesMetadataAttributes() + { + // Arrange + var modelType = typeof(DerivedViewModel); + var property = modelType.GetRuntimeProperties().FirstOrDefault(p => p.Name == "Calculator"); + + // Act + var attributes = ModelAttributes.GetAttributesForProperty(modelType, property); + + // Assert + Assert.Single(attributes.OfType()); + Assert.Single(attributes.OfType()); + } + + [Fact] + public void GetAttributesForType_IncludesMetadataAttributes() + { + // Arrange & Act + var attributes = ModelAttributes.GetAttributesForType(typeof(BaseViewModel)); + + // Assert + Assert.Single(attributes.OfType()); + } + + // Helper classes + + [ClassValidator] + private class BaseModel + { + [StringLength(10)] + public string BaseProperty { get; set; } + + [Range(10,100)] + [FromHeader] + public string TestProperty { get; set; } + + [Required] + public virtual int VirtualProperty { get; set; } + + [FromServices] + public ICalculator Calculator { get; set; } + } + + private class DerivedModel : BaseModel + { + [Required] + public string DerivedProperty { get; set; } + + [Required] + public new string TestProperty { get; set; } + + [Range(10,100)] + public override int VirtualProperty { get; set; } + + } + + [ModelMetadataType(typeof(BaseModel))] + private class BaseViewModel + { + [Range(0,10)] + public string TestProperty { get; set; } + + [Required] + public string BaseProperty { get; set; } + + [Required] + public ICalculator Calculator { get; set; } + } + + [ModelMetadataType(typeof(DerivedModel))] + private class DerivedViewModel : BaseViewModel + { + [StringLength(2)] + public new string TestProperty { get; set; } + + public int VirtualProperty { get; set; } + + } + + public interface ICalculator + { + int Operation(char @operator, int left, int right); + } + + private class ClassValidator : ValidationAttribute + { + public override Boolean IsValid(Object value) + { + return true; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/AssociatedValidatorProviderTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/AssociatedValidatorProviderTest.cs index cc66964b31..e96d67d176 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/AssociatedValidatorProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/AssociatedValidatorProviderTest.cs @@ -91,12 +91,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public abstract class TestableAssociatedValidatorProvider : AssociatedValidatorProvider { - protected override IEnumerable GetValidators(ModelMetadata metadata, IEnumerable attributes) + protected override IEnumerable GetValidators(ModelMetadata metadata, IEnumerable attributes) { return AbstractGetValidators(metadata, attributes); } - public abstract IEnumerable AbstractGetValidators(ModelMetadata metadata, IEnumerable attributes); + public abstract IEnumerable AbstractGetValidators(ModelMetadata metadata, IEnumerable attributes); } } }