File: ModelBinding\Metadata\DefaultModelMetadataProviderTest.cs
Web Access
Project: src\src\Mvc\Mvc.Core\test\Microsoft.AspNetCore.Mvc.Core.Test.csproj (Microsoft.AspNetCore.Mvc.Core.Test)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Reflection;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
 
public class DefaultModelMetadataProviderTest
{
    [Fact]
    public void GetMetadataForType_IncludesAttributes()
    {
        // Arrange
        var provider = CreateProvider();
 
        // Act
        var metadata = provider.GetMetadataForType(typeof(ModelType));
 
        // Assert
        var defaultMetadata = Assert.IsType<DefaultModelMetadata>(metadata);
 
        var attribute = Assert.IsType<ModelAttribute>(Assert.Single(defaultMetadata.Attributes.Attributes));
        Assert.Equal("OnType", attribute.Value);
    }
 
    [Fact]
    public void GetMetadataForType_Cached()
    {
        // Arrange
        var provider = CreateProvider();
 
        // Act
        var metadata1 = Assert.IsType<DefaultModelMetadata>(provider.GetMetadataForType(typeof(ModelType)));
        var metadata2 = Assert.IsType<DefaultModelMetadata>(provider.GetMetadataForType(typeof(ModelType)));
 
        // Assert
        Assert.Same(metadata1, metadata2);
        Assert.Same(metadata1.Attributes, metadata2.Attributes);
        Assert.Same(metadata1.BindingMetadata, metadata2.BindingMetadata);
        Assert.Same(metadata1.DisplayMetadata, metadata2.DisplayMetadata);
        Assert.Same(metadata1.ValidationMetadata, metadata2.ValidationMetadata);
    }
 
    [Fact]
    public void GetMetadataForObjectType_Cached()
    {
        // Arrange
        var provider = CreateProvider();
 
        // Act
        var metadata1 = provider.GetMetadataForType(typeof(object));
        var metadata2 = provider.GetMetadataForType(typeof(object));
 
        // Assert
        Assert.Same(metadata1, metadata2);
    }
 
    [Fact]
    public void GetMetadataForProperties_IncludesContainerMetadataForAllProperties()
    {
        // Arrange
        var provider = CreateProvider();
        var modelType = typeof(ModelType);
 
        // Act
        var metadata = provider.GetMetadataForProperties(modelType).ToArray();
 
        // Assert
        Assert.Collection(
            metadata,
            (propertyMetadata) =>
            {
                Assert.Equal("Property1", propertyMetadata.PropertyName);
                Assert.NotNull(propertyMetadata.ContainerMetadata);
                Assert.Equal(modelType, propertyMetadata.ContainerMetadata.ModelType);
            },
            (propertyMetadata) =>
            {
                Assert.Equal("Property2", propertyMetadata.PropertyName);
                Assert.NotNull(propertyMetadata.ContainerMetadata);
                Assert.Equal(modelType, propertyMetadata.ContainerMetadata.ModelType);
            });
    }
 
    [Fact]
    public void GetMetadataForProperties_IncludesAllProperties()
    {
        // Arrange
        var provider = CreateProvider();
 
        // Act
        var metadata = provider.GetMetadataForProperties(typeof(ModelType)).ToArray();
 
        // Assert
        Assert.Equal(2, metadata.Length);
        Assert.Single(metadata, m => m.PropertyName == "Property1");
        Assert.Single(metadata, m => m.PropertyName == "Property2");
    }
 
    [Fact]
    public void GetMetadataForProperties_IncludesAllProperties_ExceptIndexer()
    {
        // Arrange
        var provider = CreateProvider();
 
        // Act
        var metadata = provider.GetMetadataForProperties(typeof(ModelTypeWithIndexer)).ToArray();
 
        // Assert
        Assert.Single(metadata);
        Assert.Single(metadata, m => m.PropertyName == "Property1");
    }
 
    [Fact]
    public void GetMetadataForProperties_Cached()
    {
        // Arrange
        var provider = CreateProvider();
 
        // Act
        var properties1 = provider.GetMetadataForProperties(typeof(ModelType)).Cast<DefaultModelMetadata>().ToArray();
        var properties2 = provider.GetMetadataForProperties(typeof(ModelType)).Cast<DefaultModelMetadata>().ToArray();
 
        // Assert
        Assert.Equal(properties1.Length, properties2.Length);
        for (var i = 0; i < properties1.Length; i++)
        {
            Assert.Same(properties1[i], properties2[i]);
            Assert.Same(properties1[i].Attributes, properties2[i].Attributes);
            Assert.Same(properties1[i].BindingMetadata, properties2[i].BindingMetadata);
            Assert.Same(properties1[i].DisplayMetadata, properties2[i].DisplayMetadata);
            Assert.Same(properties1[i].ValidationMetadata, properties2[i].ValidationMetadata);
        }
    }
 
    [Fact]
    public void GetMetadataForType_PropertiesCollection_Cached()
    {
        // Arrange
        var provider = CreateProvider();
 
        // Act
        var metadata1 = Assert.IsType<DefaultModelMetadata>(provider.GetMetadataForType(typeof(ModelType)));
        var metadata2 = Assert.IsType<DefaultModelMetadata>(provider.GetMetadataForType(typeof(ModelType)));
 
        // Assert
        Assert.Same(metadata1.Properties, metadata2.Properties);
    }
 
    [Fact]
    public void GetMetadataForProperties_IncludesMergedAttributes()
    {
        // Arrange
        var provider = CreateProvider();
 
        // Act
        var metadata = provider.GetMetadataForProperties(typeof(ModelType)).First();
 
        // Assert
        var defaultMetadata = Assert.IsType<DefaultModelMetadata>(metadata);
 
        var attributes = defaultMetadata.Attributes.Attributes.ToArray();
        Assert.Equal("OnProperty", Assert.IsType<ModelAttribute>(attributes[0]).Value);
        Assert.Equal("OnPropertyType", Assert.IsType<ModelAttribute>(attributes[1]).Value);
    }
 
    [Fact]
    public void GetMetadataForProperties_ExcludesHiddenProperties()
    {
        // Arrange
        var provider = CreateProvider();
 
        // Act
        var metadata = provider.GetMetadataForProperties(typeof(DerivedModelWithHiding));
 
        // Assert
        var propertyMetadata = Assert.Single(metadata);
        Assert.Equal(typeof(string), propertyMetadata.ModelType);
    }
 
    [Fact]
    public void GetMetadataForProperties_PropertyGetter_IsNullSafe()
    {
        // Arrange
        var provider = CreateProvider();
 
        // Act
        var metadata = provider.GetMetadataForProperties(typeof(ModelType));
 
        // Assert
        foreach (var property in metadata)
        {
            Assert.NotNull(property.PropertyGetter);
            Assert.Null(property.PropertyGetter(null));
        }
    }
 
    [Fact]
    public void GetMetadataForParameter_SuppliesEmptyAttributes_WhenParameterHasNoAttributes()
    {
        // Arrange
        var provider = CreateProvider();
        var parameters = typeof(ModelType)
            .GetMethod(nameof(ModelType.Method1))
            .GetParameters();
 
        // Act
        var metadata = provider.GetMetadataForParameter(parameters[0]);
 
        // Assert
        var defaultMetadata = Assert.IsType<DefaultModelMetadata>(metadata);
 
        // Not exactly "no attributes" due to SerializableAttribute on object.
        Assert.IsType<SerializableAttribute>(Assert.Single(defaultMetadata.Attributes.Attributes));
    }
 
    [Fact]
    public void GetMetadataForParameter_SuppliesAttributes_WhenParamHasAttributes()
    {
        // Arrange
        var provider = CreateProvider();
        var parameters = typeof(ModelType)
            .GetMethod(nameof(ModelType.Method1))
            .GetParameters();
 
        // Act
        var metadata = provider.GetMetadataForParameter(parameters[1]);
 
        // Assert
        var defaultMetadata = Assert.IsType<DefaultModelMetadata>(metadata);
        Assert.Collection(
            // Take(2) to ignore SerializableAttribute on object.
            defaultMetadata.Attributes.Attributes.Take(2),
            attribute =>
            {
                var modelAttribute = Assert.IsType<ModelAttribute>(attribute);
                Assert.Equal("ParamAttrib1", modelAttribute.Value);
            },
            attribute =>
            {
                var modelAttribute = Assert.IsType<ModelAttribute>(attribute);
                Assert.Equal("ParamAttrib2", modelAttribute.Value);
            });
    }
 
    [Fact]
    public void GetMetadataForParameter_Cached()
    {
        // Arrange
        var provider = CreateProvider();
        var parameter = typeof(ModelType)
            .GetMethod(nameof(ModelType.Method1))
            .GetParameters()[1];
 
        // Act
        var metadata1 = provider.GetMetadataForParameter(parameter);
        var metadata2 = provider.GetMetadataForParameter(parameter);
 
        // Assert
        Assert.Same(metadata1, metadata2);
    }
 
    [Fact]
    public void GetMetadataForParameter_WithModelType_ReturnsCombinedModelMetadata()
    {
        // Arrange
        var parameter = GetType()
            .GetMethod(nameof(GetMetadataForParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Instance)
            .GetParameters()[0];
        var provider = CreateProvider();
 
        // Act
        var metadata = provider.GetMetadataForParameter(parameter, typeof(DerivedModelType));
 
        // Assert
        Assert.Equal(ModelMetadataKind.Parameter, metadata.MetadataKind);
        Assert.Equal(typeof(DerivedModelType), metadata.ModelType);
 
        var defaultModelMetadata = Assert.IsType<DefaultModelMetadata>(metadata);
 
        Assert.Collection(
            defaultModelMetadata.Attributes.Attributes,
            a => Assert.Equal("OnParameter", Assert.IsType<ModelAttribute>(a).Value),
            a => Assert.Equal("OnDerivedType", Assert.IsType<ModelAttribute>(a).Value),
            a => Assert.Equal("OnType", Assert.IsType<ModelAttribute>(a).Value));
 
        Assert.Collection(
            metadata.Properties.OrderBy(p => p.Name),
            p =>
            {
                Assert.Equal(nameof(DerivedModelType.DerivedProperty), p.Name);
 
                var defaultPropertyMetadata = Assert.IsType<DefaultModelMetadata>(p);
                Assert.Collection(
                    defaultPropertyMetadata.Attributes.Attributes.OfType<ModelAttribute>(),
                    a => Assert.Equal("OnDerivedProperty", Assert.IsType<ModelAttribute>(a).Value));
            },
            p =>
            {
                Assert.Equal(nameof(DerivedModelType.Property1), p.Name);
 
                var defaultPropertyMetadata = Assert.IsType<DefaultModelMetadata>(p);
                Assert.Collection(
                    defaultPropertyMetadata.Attributes.Attributes.OfType<ModelAttribute>(),
                    a => Assert.Equal("OnProperty", Assert.IsType<ModelAttribute>(a).Value),
                    a => Assert.Equal("OnPropertyType", Assert.IsType<ModelAttribute>(a).Value));
            },
            p =>
            {
                Assert.Equal(nameof(DerivedModelType.Property2), p.Name);
            });
    }
 
    [Fact]
    public void GetMetadataForParameter_WithModelType_CachesResults()
    {
        // Arrange
        var parameter = GetType()
            .GetMethod(nameof(GetMetadataForParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Instance)
            .GetParameters()[0];
        var provider = CreateProvider();
 
        // Act
        var metadata1 = provider.GetMetadataForParameter(parameter, typeof(DerivedModelType));
        var metadata2 = provider.GetMetadataForParameter(parameter, typeof(DerivedModelType));
 
        // Assert
        Assert.Same(metadata1, metadata2);
    }
 
    [Fact]
    public void GetMetadataForParameter_WithModelType_VariesByModelType()
    {
        // Arrange
        var parameter = GetType()
            .GetMethod(nameof(GetMetadataForParameterTestMethod), BindingFlags.NonPublic | BindingFlags.Instance)
            .GetParameters()[0];
        var provider = CreateProvider();
 
        // Act
        var metadata1 = provider.GetMetadataForParameter(parameter, typeof(DerivedModelType));
        var metadata2 = provider.GetMetadataForParameter(parameter, typeof(object));
 
        // Assert
        Assert.NotSame(metadata1, metadata2);
    }
 
    [Fact]
    public void GetMetadataForProperty_WithModelType_ReturnsCombinedModelMetadata()
    {
        // Arrange
        var property = typeof(TestContainer)
            .GetProperty(nameof(TestContainer.ModelProperty));
        var provider = CreateProvider();
 
        // Act
        var metadata = provider.GetMetadataForProperty(property, typeof(DerivedModelType));
 
        // Assert
        Assert.Equal(ModelMetadataKind.Property, metadata.MetadataKind);
        Assert.Equal(typeof(DerivedModelType), metadata.ModelType);
 
        var defaultModelMetadata = Assert.IsType<DefaultModelMetadata>(metadata);
 
        Assert.Collection(
            defaultModelMetadata.Attributes.Attributes,
            a => Assert.Equal("OnProperty", Assert.IsType<ModelAttribute>(a).Value),
            a => Assert.Equal("OnDerivedType", Assert.IsType<ModelAttribute>(a).Value),
            a => Assert.Equal("OnType", Assert.IsType<ModelAttribute>(a).Value));
 
        Assert.Collection(
            metadata.Properties.OrderBy(p => p.Name),
            p =>
            {
                Assert.Equal(nameof(DerivedModelType.DerivedProperty), p.Name);
 
                var defaultPropertyMetadata = Assert.IsType<DefaultModelMetadata>(p);
                Assert.Collection(
                    defaultPropertyMetadata.Attributes.Attributes.OfType<ModelAttribute>(),
                    a => Assert.Equal("OnDerivedProperty", Assert.IsType<ModelAttribute>(a).Value));
            },
            p =>
            {
                Assert.Equal(nameof(DerivedModelType.Property1), p.Name);
 
                var defaultPropertyMetadata = Assert.IsType<DefaultModelMetadata>(p);
                Assert.Collection(
                    defaultPropertyMetadata.Attributes.Attributes.OfType<ModelAttribute>(),
                    a => Assert.Equal("OnProperty", Assert.IsType<ModelAttribute>(a).Value),
                    a => Assert.Equal("OnPropertyType", Assert.IsType<ModelAttribute>(a).Value));
            },
            p =>
            {
                Assert.Equal(nameof(DerivedModelType.Property2), p.Name);
            });
    }
 
    [Fact]
    public void GetMetadataForProperty_WithModelType_CachesResults()
    {
        // Arrange
        var property = typeof(TestContainer)
            .GetProperty(nameof(TestContainer.ModelProperty));
        var provider = CreateProvider();
 
        // Act
        var metadata1 = provider.GetMetadataForProperty(property, typeof(DerivedModelType));
        var metadata2 = provider.GetMetadataForProperty(property, typeof(DerivedModelType));
 
        // Assert
        Assert.Same(metadata1, metadata2);
    }
 
    [Fact]
    public void GetMetadataForProperty_WithModelType_VariesByModelType()
    {
        // Arrange
        var property = typeof(TestContainer)
            .GetProperty(nameof(TestContainer.ModelProperty));
        var provider = CreateProvider();
 
        // Act
        var metadata1 = provider.GetMetadataForProperty(property, typeof(DerivedModelType));
        var metadata2 = provider.GetMetadataForProperty(property, typeof(object));
 
        // Assert
        Assert.NotSame(metadata1, metadata2);
    }
 
    private static DefaultModelMetadataProvider CreateProvider()
    {
        return new DefaultModelMetadataProvider(
            new EmptyCompositeMetadataDetailsProvider(),
            Options.Create(new MvcOptions()));
    }
 
    [Model("OnType")]
    private class ModelType
    {
        [Model("OnProperty")]
        public PropertyType Property1 { get; } = new PropertyType();
 
        public PropertyType Property2 { get; set; }
 
        public void Method1(
            object paramWithNoAttributes,
            [Model("ParamAttrib1"), Model("ParamAttrib2")] object paramWithTwoAttributes)
        {
        }
    }
 
    [Model("OnPropertyType")]
    private class PropertyType
    {
    }
 
    [AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
    private class ModelAttribute : Attribute
    {
        public ModelAttribute(string value)
        {
            Value = value;
        }
 
        public string Value { get; }
    }
 
    private class ModelTypeWithIndexer
    {
        public PropertyType this[string key] => null;
 
        public PropertyType Property1 { get; set; }
    }
 
    private void GetMetadataForParameterTestMethod([Model("OnParameter")] ModelType parameter)
    {
    }
 
    private class BaseModelWithHiding
    {
        public int Property { get; set; }
    }
 
    private class DerivedModelWithHiding : BaseModelWithHiding
    {
        public new string Property { get; set; }
    }
 
    [Model("OnDerivedType")]
    private class DerivedModelType : ModelType
    {
        [Model("OnDerivedProperty")]
        public string DerivedProperty { get; set; }
    }
 
    private class TestContainer
    {
        [Model("OnProperty")]
        public ModelType ModelProperty { get; set; }
    }
}