// 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.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; using Microsoft.Framework.Internal; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata { // Integration tests for the default provider configuration. public class ModelMetadataProviderTest { [Fact] public void ModelMetadataProvider_UsesPredicateOnType() { // Arrange var type = typeof(User); var provider = CreateProvider(); var context = new ModelBindingContext(); var expected = new[] { "IsAdmin", "UserName" }; // Act var metadata = provider.GetMetadataForType(type); // Assert var predicate = metadata.PropertyBindingPredicateProvider.PropertyFilter; var matched = new HashSet(); foreach (var property in metadata.Properties) { if (predicate(context, property.PropertyName)) { matched.Add(property.PropertyName); } } Assert.Equal(expected, matched); } [Fact] public void ModelMetadataProvider_UsesPredicateOnParameter() { // Arrange var type = GetType(); var methodInfo = type.GetMethod( "ActionWithoutBindAttribute", BindingFlags.Instance | BindingFlags.NonPublic); var parameterInfo = methodInfo.GetParameters().Where(p => p.Name == "param").Single(); var provider = CreateProvider(); var context = new ModelBindingContext(); // Note it does an intersection for included -- only properties that // pass both predicates will be bound. var expected = new[] { "IsAdmin", "UserName" }; // Act var metadata = provider.GetMetadataForParameter( parameterInfo, attributes: null); // Assert var predicate = metadata.PropertyBindingPredicateProvider.PropertyFilter; Assert.NotNull(predicate); var matched = new HashSet(); foreach (var property in metadata.Properties) { if (predicate(context, property.PropertyName)) { matched.Add(property.PropertyName); } } Assert.Equal(expected, matched); } [Fact] public void ModelMetadataProvider_UsesPredicateOnParameter_Merge() { // Arrange var type = GetType(); var methodInfo = type.GetMethod( "ActionWithBindAttribute", BindingFlags.Instance | BindingFlags.NonPublic); var parameterInfo = methodInfo.GetParameters().Where(p => p.Name == "param").Single(); var provider = CreateProvider(); var context = new ModelBindingContext(); // Note it does an intersection for included -- only properties that // pass both predicates will be bound. var expected = new[] { "IsAdmin" }; // Act var metadata = provider.GetMetadataForParameter( parameterInfo, attributes: null); // Assert var predicate = metadata.PropertyBindingPredicateProvider.PropertyFilter; Assert.NotNull(predicate); var matched = new HashSet(); foreach (var property in metadata.Properties) { if (predicate(context, property.PropertyName)) { matched.Add(property.PropertyName); } } Assert.Equal(expected, matched); } [Fact] public void ModelMetadataProvider_ReadsModelNameProperty_ForParameters() { // Arrange var type = GetType(); var methodInfo = type.GetMethod( "ActionWithBindAttribute", BindingFlags.Instance | BindingFlags.NonPublic); var parameterInfo = methodInfo.GetParameters().Where(p => p.Name == "param").Single(); var provider = CreateProvider(); // Act var metadata = provider.GetMetadataForParameter( parameterInfo, attributes: null); // Assert Assert.Equal("ParameterPrefix", metadata.BinderModelName); } [Fact] public void ModelMetadataProvider_ReadsModelNameProperty_ForTypes() { // Arrange var type = typeof(User); var provider = CreateProvider(); // Act var metadata = provider.GetMetadataForType(type); // Assert Assert.Equal("TypePrefix", metadata.BinderModelName); } [Fact] public void ModelMetadataProvider_ReadsScaffoldColumnAttribute_ForShowForDisplay() { // Arrange var type = typeof(ScaffoldColumnModel); var provider = CreateProvider(); // Act & Assert Assert.True(provider.GetMetadataForProperty(type, "NoAttribute").ShowForDisplay); Assert.True(provider.GetMetadataForProperty(type, "ScaffoldColumnTrue").ShowForDisplay); Assert.False(provider.GetMetadataForProperty(type, "ScaffoldColumnFalse").ShowForDisplay); } [Fact] public void ModelMetadataProvider_ReadsScaffoldColumnAttribute_ForShowForEdit() { // Arrange var type = typeof(ScaffoldColumnModel); var provider = CreateProvider(); // Act & Assert Assert.True(provider.GetMetadataForProperty(type, "NoAttribute").ShowForEdit); Assert.True(provider.GetMetadataForProperty(type, "ScaffoldColumnTrue").ShowForEdit); Assert.False(provider.GetMetadataForProperty(type, "ScaffoldColumnFalse").ShowForEdit); } [Fact] public void HiddenInputWorksOnProperty_ForHideSurroundingHtml() { // Arrange var provider = CreateProvider(); var metadata = provider.GetMetadataForType(modelType: typeof(ClassWithHiddenProperties)); var property = metadata.Properties["DirectlyHidden"]; // Act var result = property.HideSurroundingHtml; // Assert Assert.True(result); } [Fact] public void HiddenInputWorksOnPropertyType_ForHideSurroundingHtml() { // Arrange var provider = CreateProvider(); var metadata = provider.GetMetadataForType(typeof(ClassWithHiddenProperties)); var property = metadata.Properties["OfHiddenType"]; // Act var result = property.HideSurroundingHtml; // Assert Assert.True(result); } [Fact] public void HiddenInputWorksOnProperty_ForTemplateHint() { // Arrange var provider = CreateProvider(); var metadata = provider.GetMetadataForType(typeof(ClassWithHiddenProperties)); var property = metadata.Properties["DirectlyHidden"]; // Act var result = property.TemplateHint; // Assert Assert.Equal("HiddenInput", result); } [Fact] public void HiddenInputWorksOnPropertyType_ForTemplateHint() { // Arrange var provider = CreateProvider(); var metadata = provider.GetMetadataForType(typeof(ClassWithHiddenProperties)); var property = metadata.Properties["OfHiddenType"]; // Act var result = property.TemplateHint; // Assert Assert.Equal("HiddenInput", result); } [Fact] public void GetMetadataForProperty_WithNoBinderModelName_GetsItFromType() { // Arrange var provider = CreateProvider(); // Act var propertyMetadata = provider.GetMetadataForProperty(typeof(Person), nameof(Person.Parent)); // Assert Assert.Equal("PersonType", propertyMetadata.BinderModelName); } [Fact] public void GetMetadataForProperty_WithBinderMetadataOnPropertyAndType_GetsMetadataFromProperty() { // Arrange var provider = CreateProvider(); // Act var propertyMetadata = provider.GetMetadataForProperty(typeof(Person), nameof(Person.GrandParent)); // Assert Assert.Equal("GrandParentProperty", propertyMetadata.BinderModelName); } [Fact] public void GetMetadataForParameter_WithNoBinderMetadata_GetsItFromType() { // Arrange var provider = CreateProvider(); var methodInfo = typeof(Person).GetMethod("Update"); var parameterInfo = methodInfo.GetParameters().Where(p => p.Name == "person").Single(); // Act var parameterMetadata = provider.GetMetadataForParameter( parameterInfo, attributes: null); // Assert Assert.Equal("PersonType", parameterMetadata.BinderModelName); } [Fact] public void GetMetadataForParameter_WithBinderDataOnParameterAndType_GetsMetadataFromParameter() { // Arrange var provider = CreateProvider(); var methodInfo = typeof(Person).GetMethod("Save"); var parameterInfo = methodInfo.GetParameters().Where(p => p.Name == "person").Single(); // Act var parameterMetadata = provider.GetMetadataForParameter( parameterInfo, attributes: null); // Assert Assert.Equal("PersonParameter", parameterMetadata.BinderModelName); } [Fact] public void GetMetadataForParameter_BinderModelNameOverride() { // Arrange var provider = CreateProvider(); var methodInfo = typeof(Person).GetMethod("Save"); var parameterInfo = methodInfo.GetParameters().Where(p => p.Name == "person").Single(); // Act var parameterMetadata = provider.GetMetadataForParameter( parameterInfo, attributes: new object[] { new ModelBinderAttribute() { Name = "Override" } }); // Assert Assert.Equal("Override", parameterMetadata.BinderModelName); } public static TheoryData> ExpectedAttributeDataStrings { get { return new TheoryData> { { new DataTypeAttribute("value"), metadata => metadata.DataTypeName }, { new DataTypeWithCustomDisplayFormat(), metadata => metadata.DisplayFormatString }, { new DataTypeWithCustomEditFormat(), metadata => metadata.EditFormatString }, { new DisplayAttribute { Description = "value" }, metadata => metadata.Description }, { new DisplayAttribute { Name = "value" }, metadata => metadata.DisplayName }, { new DisplayFormatAttribute { DataFormatString = "value" }, metadata => metadata.DisplayFormatString }, { // DisplayFormatString does not ignore [DisplayFormat] if ApplyFormatInEditMode==true. new DisplayFormatAttribute { ApplyFormatInEditMode = true, DataFormatString = "value" }, metadata => metadata.DisplayFormatString }, { new DisplayFormatAttribute { ApplyFormatInEditMode = true, DataFormatString = "value" }, metadata => metadata.EditFormatString }, { new DisplayFormatAttribute { NullDisplayText = "value" }, metadata => metadata.NullDisplayText }, { new TestModelNameProvider { Name = "value" }, metadata => metadata.BinderModelName }, { new UIHintAttribute("value"), metadata => metadata.TemplateHint }, }; } } [Theory] [MemberData(nameof(ExpectedAttributeDataStrings))] public void AttributesOverrideMetadataStrings(object attribute, Func accessor) { // Arrange var attributes = new[] { attribute }; var provider = CreateProvider(attributes); var metadata = provider.GetMetadataForType(typeof(string)); // Act var result = accessor(metadata); // Assert Assert.Equal("value", result); } [Fact] public void AttributesOverrideMetadataStrings_SimpleDisplayProperty() { // Arrange var attributes = new[] { new DisplayColumnAttribute("Property") }; var provider = CreateProvider(attributes); var metadata = provider.GetMetadataForType(typeof(string)); // Act var result = metadata.SimpleDisplayProperty; // Assert Assert.Equal("Property", result); } public static TheoryData, bool> ExpectedAttributeDataBooleans { get { return new TheoryData, bool> { { // Edit formats from [DataType] subclass affect HasNonDefaultEditFormat. new DataTypeWithCustomEditFormat(), metadata => metadata.HasNonDefaultEditFormat, true }, { // Edit formats from [DataType] do not affect HasNonDefaultEditFormat. new DataTypeAttribute(DataType.Date), metadata => metadata.HasNonDefaultEditFormat, false }, { new DisplayFormatAttribute { ConvertEmptyStringToNull = false }, metadata => metadata.ConvertEmptyStringToNull, false }, { new DisplayFormatAttribute { ConvertEmptyStringToNull = true }, metadata => metadata.ConvertEmptyStringToNull, true }, { // Changes only to DisplayFormatString do not affect HasNonDefaultEditFormat. new DisplayFormatAttribute { DataFormatString = "value" }, metadata => metadata.HasNonDefaultEditFormat, false }, { new DisplayFormatAttribute { ApplyFormatInEditMode = true, DataFormatString = "value" }, metadata => metadata.HasNonDefaultEditFormat, true }, { new DisplayFormatAttribute { HtmlEncode = false }, metadata => metadata.HtmlEncode, false }, { new DisplayFormatAttribute { HtmlEncode = true }, metadata => metadata.HtmlEncode, true }, { new EditableAttribute(allowEdit: false), metadata => metadata.IsReadOnly, true }, { new EditableAttribute(allowEdit: true), metadata => metadata.IsReadOnly, false }, { new HiddenInputAttribute { DisplayValue = false }, metadata => metadata.HideSurroundingHtml, true }, { new HiddenInputAttribute { DisplayValue = true }, metadata => metadata.HideSurroundingHtml, false }, { new HiddenInputAttribute(), metadata => string.Equals("HiddenInput", metadata.TemplateHint, StringComparison.Ordinal), true }, { new RequiredAttribute(), metadata => metadata.IsRequired, true }, }; } } [Theory] [MemberData(nameof(ExpectedAttributeDataBooleans))] public void AttributesOverrideMetadataBooleans( Attribute attribute, Func accessor, bool expectedResult) { // Arrange var attributes = new[] { attribute }; var provider = CreateProvider(attributes); var metadata = provider.GetMetadataForType(typeof(string)); // Act var result = accessor(metadata); // Assert Assert.Equal(expectedResult, result); } public static TheoryData DisplayAttribute_OverridesOrderData { get { return new TheoryData { { new DisplayAttribute(), ModelMetadata.DefaultOrder }, { new DisplayAttribute { Order = int.MinValue }, int.MinValue }, { new DisplayAttribute { Order = -100 }, -100 }, { new DisplayAttribute { Order = -1 }, -1 }, { new DisplayAttribute { Order = 0 }, 0 }, { new DisplayAttribute { Order = 1 }, 1 }, { new DisplayAttribute { Order = 200 }, 200 }, { new DisplayAttribute { Order = int.MaxValue }, int.MaxValue }, }; } } [Theory] [MemberData(nameof(DisplayAttribute_OverridesOrderData))] public void DisplayAttribute_OverridesOrder(DisplayAttribute attribute, int expectedOrder) { // Arrange var attributes = new[] { attribute }; var provider = CreateProvider(attributes); var metadata = provider.GetMetadataForType(typeof(string)); // Act var result = metadata.Order; // Assert Assert.Equal(expectedOrder, result); } [Fact] public void DisplayAttribute_Description() { // Arrange var display = new DisplayAttribute() { Description = "description" }; var provider = CreateProvider(new[] { display }); var metadata = provider.GetMetadataForType(typeof(string)); // Act var result = metadata.Description; // Assert Assert.Equal("description", result); } [Fact] public void DataTypeName_Null_IfHtmlEncodeTrue() { // Arrange var displayFormat = new DisplayFormatAttribute { HtmlEncode = true, }; var provider = CreateProvider(new[] { displayFormat }); var metadata = provider.GetMetadataForType(typeof(string)); // Act var result = metadata.DataTypeName; // Assert Assert.Null(result); } [Fact] public void DataTypeName_Html_IfHtmlEncodeFalse() { // Arrange var expected = "Html"; var displayFormat = new DisplayFormatAttribute { HtmlEncode = false, }; var provider = CreateProvider(new[] { displayFormat }); var metadata = provider.GetMetadataForType(typeof(string)); // Act var result = metadata.DataTypeName; // Assert Assert.Equal(expected, result); } [Fact] public void DataTypeName_AttributesHaveExpectedPrecedence() { // Arrange var expected = "MultilineText"; var dataType = new DataTypeAttribute(DataType.MultilineText); var displayFormat = new DisplayFormatAttribute { HtmlEncode = false, }; var provider = CreateProvider(new object[] { dataType, displayFormat }); var metadata = provider.GetMetadataForType(typeof(string)); // Act var result = metadata.DataTypeName; // Assert Assert.Equal(expected, result); } [Fact] public void DisplayFormatString_AttributesHaveExpectedPrecedence() { // Arrange var expected = "custom format"; var dataType = new DataTypeAttribute(DataType.Currency); var displayFormat = new DisplayFormatAttribute { DataFormatString = expected, }; var provider = CreateProvider(new object[] { displayFormat, dataType, }); var metadata = provider.GetMetadataForType(typeof(string)); // Act var result = metadata.DisplayFormatString; // Assert Assert.Equal(expected, result); } [Fact] public void EditFormatString_AttributesHaveExpectedPrecedence() { // Arrange var expected = "custom format"; var dataType = new DataTypeAttribute(DataType.Currency); var displayFormat = new DisplayFormatAttribute { ApplyFormatInEditMode = true, DataFormatString = expected, }; var provider = CreateProvider(new object[] { displayFormat, dataType, }); var metadata = provider.GetMetadataForType(typeof(string)); // Act var result = metadata.EditFormatString; // Assert Assert.Equal(expected, result); } [Fact] public void TemplateHint_AttributesHaveExpectedPrecedence() { // Arrange var expected = "this is a hint"; var hidden = new HiddenInputAttribute(); var uiHint = new UIHintAttribute(expected); var provider = CreateProvider(new object[] { hidden, uiHint, }); var metadata = provider.GetMetadataForType(typeof(string)); // Act var result = metadata.TemplateHint; // Assert Assert.Equal(expected, result); } [Fact] public void BinderTypeProviders_Null() { // Arrange var binderProviders = new[] { new TestBinderTypeProvider(), new TestBinderTypeProvider(), }; var provider = CreateProvider(binderProviders); // Act var metadata = provider.GetMetadataForType(typeof(string)); // Assert Assert.Null(metadata.BinderType); } [Fact] public void BinderTypeProviders_Fallback() { // Arrange var attributes = new[] { new TestBinderTypeProvider(), new TestBinderTypeProvider() { BinderType = typeof(string) } }; var provider = CreateProvider(attributes); // Act var metadata = provider.GetMetadataForType(typeof(string)); // Assert Assert.Same(typeof(string), metadata.BinderType); } [Fact] public void BinderTypeProviders_FirstAttributeHasPrecedence() { // Arrange var attributes = new[] { new TestBinderTypeProvider() { BinderType = typeof(int) }, new TestBinderTypeProvider() { BinderType = typeof(string) } }; var provider = CreateProvider(attributes); // Act var metadata = provider.GetMetadataForType(typeof(string)); // Assert Assert.Same(typeof(int), metadata.BinderType); } private IModelMetadataProvider CreateProvider(params object[] attributes) { return new AttributeInjectModelMetadataProvider(attributes); } private class TestBinderTypeProvider : IBinderTypeProviderMetadata { public Type BinderType { get; set; } public BindingSource BindingSource { get; set; } } private class DataTypeWithCustomDisplayFormat : DataTypeAttribute { public DataTypeWithCustomDisplayFormat() : base("Custom datatype") { DisplayFormat = new DisplayFormatAttribute { DataFormatString = "value", }; } } private class DataTypeWithCustomEditFormat : DataTypeAttribute { public DataTypeWithCustomEditFormat() : base("Custom datatype") { DisplayFormat = new DisplayFormatAttribute { ApplyFormatInEditMode = true, DataFormatString = "value", }; } } private void ActionWithoutBindAttribute(User param) { } private void ActionWithBindAttribute([Bind(new string[] { "IsAdmin" }, Prefix = "ParameterPrefix")] User param) { } public class TypeBasedBinderAttribute : Attribute, IBinderMetadata, IModelNameProvider { public string Name { get; set; } } public class NonTypeBasedBinderAttribute : Attribute, IBinderMetadata, IModelNameProvider { public string Name { get; set; } } [TypeBasedBinder(Name = "PersonType")] public class Person { public Person Parent { get; set; } [NonTypeBasedBinder(Name = "GrandParentProperty")] public Person GrandParent { get; set; } public void Update(Person person) { } public void Save([NonTypeBasedBinder(Name = "PersonParameter")] Person person) { } } private class ScaffoldColumnModel { public int NoAttribute { get; set; } [ScaffoldColumn(scaffold: true)] public int ScaffoldColumnTrue { get; set; } [ScaffoldColumn(scaffold: false)] public int ScaffoldColumnFalse { get; set; } } [HiddenInput(DisplayValue = false)] private class HiddenClass { public string Property { get; set; } } private class ClassWithHiddenProperties { [HiddenInput(DisplayValue = false)] public string DirectlyHidden { get; set; } public HiddenClass OfHiddenType { get; set; } } [Bind(new[] { nameof(IsAdmin), nameof(UserName) }, Prefix = "TypePrefix")] private class User { public int Id { get; set; } public bool IsAdmin { get; set; } public int UserName { get; set; } public int NotIncludedOrExcluded { get; set; } } private class AttributeInjectModelMetadataProvider : DefaultModelMetadataProvider { private readonly object[] _attributes; public AttributeInjectModelMetadataProvider(object[] attributes) : base(new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[] { new DefaultBindingMetadataProvider(), new DataAnnotationsMetadataDetailsProvider(), })) { _attributes = attributes; } protected override DefaultMetadataDetailsCache CreateTypeCacheEntry(ModelMetadataIdentity key) { var entry = base.CreateTypeCacheEntry(key); return new DefaultMetadataDetailsCache(key, _attributes.Concat(entry.Attributes).ToArray()); } } } }