// Copyright (c) .NET Foundation. 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; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.DataAnnotations { public enum TestEnum { [Display(Name = "DisplayNameValue")] DisplayNameValue } public class DataAnnotationsMetadataProviderTest { // Includes attributes with a 'simple' effect on display details. public static TheoryData, object> DisplayDetailsData { get { return new TheoryData, object> { { new DataTypeAttribute(DataType.Duration), d => d.DataTypeName, DataType.Duration.ToString() }, { new DisplayAttribute() { Description = "d" }, d => d.Description(), "d" }, { new DisplayAttribute() { Name = "DN" }, d => d.DisplayName(), "DN" }, { new DisplayAttribute() { Order = 3 }, d => d.Order, 3 }, { new DisplayAttribute() { Prompt = "Enter Value" }, d => d.Placeholder(), "Enter Value" }, { new DisplayColumnAttribute("Property"), d => d.SimpleDisplayProperty, "Property" }, { new DisplayFormatAttribute() { ConvertEmptyStringToNull = true }, d => d.ConvertEmptyStringToNull, true }, { new DisplayFormatAttribute() { DataFormatString = "{0:G}" }, d => d.DisplayFormatString, "{0:G}" }, { new DisplayFormatAttribute() { DataFormatString = "{0:G}" }, d => d.DisplayFormatStringProvider(), "{0:G}" }, { new DisplayFormatAttribute() { DataFormatString = "{0:G}", ApplyFormatInEditMode = true }, d => d.EditFormatString, "{0:G}" }, { new DisplayFormatAttribute() { DataFormatString = "{0:G}", ApplyFormatInEditMode = true }, d => d.EditFormatStringProvider(), "{0:G}" }, { new DisplayFormatAttribute() { DataFormatString = "{0:G}", ApplyFormatInEditMode = true }, d => d.HasNonDefaultEditFormat, true }, { new DisplayFormatAttribute() { HtmlEncode = false }, d => d.HtmlEncode, false }, { new DisplayFormatAttribute() { NullDisplayText = "(null)" }, d => d.NullDisplayText, "(null)" }, { new DisplayFormatAttribute() { NullDisplayText = "(null)" }, d => d.NullDisplayTextProvider(), "(null)" }, { new DisplayNameAttribute("DisplayNameValue"), d => d.DisplayName(), "DisplayNameValue"}, { new HiddenInputAttribute() { DisplayValue = false }, d => d.HideSurroundingHtml, true }, { new ScaffoldColumnAttribute(scaffold: false), d => d.ShowForDisplay, false }, { new ScaffoldColumnAttribute(scaffold: false), d => d.ShowForEdit, false }, { new UIHintAttribute("hintHint"), d => d.TemplateHint, "hintHint" }, }; } } [Theory] [MemberData(nameof(DisplayDetailsData))] public void CreateDisplayMetadata_SimpleAttributes( object attribute, Func accessor, object expected) { // Arrange var provider = CreateProvider(); var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(new object[] { attribute })); // Act provider.CreateDisplayMetadata(context); // Assert var value = accessor(context.DisplayMetadata); Assert.Equal(expected, value); } [Fact] public void CreateDisplayMetadata_FindsDisplayFormat_FromDataType() { // Arrange var provider = CreateProvider(); var dataType = new DataTypeAttribute(DataType.Currency); var displayFormat = dataType.DisplayFormat; // Non-null for DataType.Currency. var attributes = new[] { dataType, }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Same(displayFormat.DataFormatString, context.DisplayMetadata.DisplayFormatString); } [Fact] public void CreateDisplayMetadata_FindsDisplayFormat_OverridingDataType() { // Arrange var provider = CreateProvider(); var dataType = new DataTypeAttribute(DataType.Time); // Has a non-null DisplayFormat. var displayFormat = new DisplayFormatAttribute() // But these values override the values from DataType { DataFormatString = "Cool {0}", }; var attributes = new Attribute[] { dataType, displayFormat, }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Same(displayFormat.DataFormatString, context.DisplayMetadata.DisplayFormatString); } [Fact] public void CreateBindingMetadata_EditableAttributeFalse_SetsReadOnlyTrue() { // Arrange var provider = CreateProvider(); var editable = new EditableAttribute(allowEdit: false); var attributes = new Attribute[] { editable }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new BindingMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateBindingMetadata(context); // Assert Assert.True(context.BindingMetadata.IsReadOnly); } [Fact] public void CreateBindingMetadata_EditableAttributeTrue_SetsReadOnlyFalse() { // Arrange var provider = CreateProvider(); var editable = new EditableAttribute(allowEdit: true); var attributes = new Attribute[] { editable }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new BindingMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateBindingMetadata(context); // Assert Assert.False(context.BindingMetadata.IsReadOnly); } [Fact] public void CreateDisplayMetadata_DisplayAttribute_OverridesDisplayNameAttribute() { // Arrange var provider = CreateProvider(); var displayName = new DisplayNameAttribute("DisplayNameAttributeValue"); var display = new DisplayAttribute() { Name = "DisplayAttributeValue" }; var attributes = new Attribute[] { display, displayName }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal("DisplayAttributeValue", context.DisplayMetadata.DisplayName()); } [Fact] public void CreateDisplayMetadata_DisplayAttribute_OverridesDisplayNameAttribute_IfNameEmpty() { // Arrange var provider = CreateProvider(); var displayName = new DisplayNameAttribute("DisplayNameAttributeValue"); var display = new DisplayAttribute() { Name = string.Empty }; var attributes = new Attribute[] { display, displayName }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal(string.Empty, context.DisplayMetadata.DisplayName()); } [Fact] public void CreateDisplayMetadata_DisplayAttribute_DoesNotOverrideDisplayNameAttribute_IfNameNull() { // Arrange var provider = CreateProvider(); var displayName = new DisplayNameAttribute("DisplayNameAttributeValue"); var display = new DisplayAttribute() { Description = "This is a description" }; var attributes = new Attribute[] { display, displayName }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal("DisplayNameAttributeValue", context.DisplayMetadata.DisplayName()); } [Fact] public void CreateDisplayMetadata_DisplayNameAttribute_OnEnum_CompatShimOn() { // Arrange var sharedLocalizer = new Mock(MockBehavior.Strict); sharedLocalizer .Setup(s => s["DisplayNameValue"]) .Returns(new LocalizedString("DisplayNameValue", "Name from DisplayNameAttribute")); var stringLocalizerFactoryMock = new Mock(MockBehavior.Strict); stringLocalizerFactoryMock .Setup(s => s.Create(typeof(EmptyClass))) .Returns(() => sharedLocalizer.Object); var localizationOptions = new MvcDataAnnotationsLocalizationOptions(); localizationOptions.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) => { return stringLocalizerFactory.Create(typeof(EmptyClass)); }; var provider = CreateProvider(options: null, localizationOptions, stringLocalizerFactoryMock.Object); var displayName = new DisplayNameAttribute("DisplayNameValue"); var attributes = new Attribute[] { displayName }; var key = ModelMetadataIdentity.ForType(typeof(TestEnum)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Collection(context.DisplayMetadata.EnumGroupedDisplayNamesAndValues, (e) => { Assert.Equal("Name from DisplayNameAttribute", e.Key.Name); }); } [Fact] public void CreateDisplayMetadata_DisplayNameAttribute_LocalizesDisplayName() { // Arrange var sharedLocalizer = new Mock(MockBehavior.Strict); sharedLocalizer .Setup(s => s["DisplayNameValue"]) .Returns(new LocalizedString("DisplayNameValue", "Name from DisplayNameAttribute")); var stringLocalizerFactoryMock = new Mock(MockBehavior.Strict); stringLocalizerFactoryMock .Setup(s => s.Create(typeof(EmptyClass))) .Returns(() => sharedLocalizer.Object); var localizationOptions = new MvcDataAnnotationsLocalizationOptions(); localizationOptions.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) => { return stringLocalizerFactory.Create(typeof(EmptyClass)); }; var provider = CreateProvider(options: null, localizationOptions, stringLocalizerFactoryMock.Object); var displayName = new DisplayNameAttribute("DisplayNameValue"); var attributes = new Attribute[] { displayName }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal("Name from DisplayNameAttribute", context.DisplayMetadata.DisplayName()); } [Fact] public void CreateDisplayMetadata_DisplayAttribute_NameFromResources_UsesDataAnnotationLocalizerProvider() { // Arrange var sharedLocalizer = new Mock(MockBehavior.Loose); var stringLocalizerFactoryMock = new Mock(MockBehavior.Strict); stringLocalizerFactoryMock .Setup(s => s.Create(typeof(EmptyClass))) .Returns(() => sharedLocalizer.Object); var localizationOptions = new MvcDataAnnotationsLocalizationOptions(); var dataAnnotationLocalizerProviderWasUsed = false; localizationOptions.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) => { dataAnnotationLocalizerProviderWasUsed = true; return stringLocalizerFactory.Create(typeof(EmptyClass)); }; var provider = CreateProvider(options: null, localizationOptions, stringLocalizerFactoryMock.Object); var display = new DisplayAttribute() { Name = "DisplayName" }; var attributes = new Attribute[] { display }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); context.DisplayMetadata.DisplayName(); // Assert Assert.True(dataAnnotationLocalizerProviderWasUsed, "DataAnnotationLocalizerProvider wasn't used by DisplayMetadata"); } // This is IMPORTANT. Product code needs to use GetName() instead of .Name. It's easy to regress. [Fact] public void CreateDisplayMetadata_DisplayAttribute_NameFromResources_NullLocalizer() { // Arrange var provider = CreateProvider(); var display = new DisplayAttribute() { #if USE_REAL_RESOURCES Name = nameof(Test.Resources.DisplayAttribute_Name), ResourceType = typeof(Test.Resources), #else Name = nameof(DataAnnotations.Test.Resources.DisplayAttribute_Name), ResourceType = typeof(TestResources), #endif }; var attributes = new Attribute[] { display }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal("name from resources", context.DisplayMetadata.DisplayName()); } // This is IMPORTANT. Product code needs to use GetName() instead of .Name. It's easy to regress. [Fact] public void CreateDisplayMetadata_DisplayAttribute_NameFromResources_WithLocalizer() { // Arrange // Nothing on stringLocalizer should be called var stringLocalizer = new Mock(MockBehavior.Strict); var stringLocalizerFactory = new Mock(); stringLocalizerFactory .Setup(s => s.Create(It.IsAny())) .Returns(() => stringLocalizer.Object); var provider = CreateProvider(stringLocalizerFactory: stringLocalizerFactory.Object); var display = new DisplayAttribute() { #if USE_REAL_RESOURCES Name = nameof(Test.Resources.DisplayAttribute_Name), ResourceType = typeof(Test.Resources), #else Name = nameof(DataAnnotations.Test.Resources.DisplayAttribute_Name), ResourceType = typeof(TestResources), #endif }; var attributes = new Attribute[] { display }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal("name from resources", context.DisplayMetadata.DisplayName()); } // This is IMPORTANT. Product code needs to use GetDescription() instead of .Description. It's easy to regress. [Fact] public void CreateDisplayMetadata_DisplayAttribute_DescriptionFromResources_WithLocalizer() { // Arrange // Nothing on stringLocalizer should be called var stringLocalizer = new Mock(MockBehavior.Strict); var stringLocalizerFactory = new Mock(); stringLocalizerFactory .Setup(s => s.Create(It.IsAny())) .Returns(() => stringLocalizer.Object); var provider = CreateProvider(stringLocalizerFactory: stringLocalizerFactory.Object); var display = new DisplayAttribute() { #if USE_REAL_RESOURCES Description = nameof(Test.Resources.DisplayAttribute_Description), ResourceType = typeof(Test.Resources), #else Description = nameof(DataAnnotations.Test.Resources.DisplayAttribute_Description), ResourceType = typeof(TestResources), #endif }; var attributes = new Attribute[] { display }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal("description from resources", context.DisplayMetadata.Description()); } // This is IMPORTANT. Product code needs to use GetDescription() instead of .Description. It's easy to regress. [Fact] public void CreateDisplayMetadata_DisplayAttribute_DescriptionFromResources_NullLocalizer() { // Arrange var provider = CreateProvider(); var display = new DisplayAttribute() { #if USE_REAL_RESOURCES Description = nameof(Test.Resources.DisplayAttribute_Description), ResourceType = typeof(Test.Resources), #else Description = nameof(DataAnnotations.Test.Resources.DisplayAttribute_Description), ResourceType = typeof(TestResources), #endif }; var attributes = new Attribute[] { display }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal("description from resources", context.DisplayMetadata.Description()); } // This is IMPORTANT. Product code needs to use GetPrompt() instead of .Prompt. It's easy to regress. [Fact] public void CreateDisplayMetadata_DisplayAttribute_PromptFromResources_WithLocalizer() { // Arrange // Nothing on stringLocalizer should be called var stringLocalizer = new Mock(MockBehavior.Strict); var stringLocalizerFactory = new Mock(); stringLocalizerFactory .Setup(s => s.Create(It.IsAny())) .Returns(() => stringLocalizer.Object); var provider = CreateProvider(stringLocalizerFactory: stringLocalizerFactory.Object); var display = new DisplayAttribute() { #if USE_REAL_RESOURCES Prompt = nameof(Test.Resources.DisplayAttribute_Prompt), ResourceType = typeof(Test.Resources), #else Prompt = nameof(DataAnnotations.Test.Resources.DisplayAttribute_Prompt), ResourceType = typeof(TestResources), #endif }; var attributes = new Attribute[] { display }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal("prompt from resources", context.DisplayMetadata.Placeholder()); } // This is IMPORTANT. Product code needs to use GetPrompt() instead of .Prompt. It's easy to regress. [Fact] public void CreateDisplayMetadata_DisplayAttribute_PromptFromResources_NullLocalizer() { // Arrange var provider = CreateProvider(); var display = new DisplayAttribute() { #if USE_REAL_RESOURCES Prompt = nameof(Test.Resources.DisplayAttribute_Prompt), ResourceType = typeof(Test.Resources), #else Prompt = nameof(DataAnnotations.Test.Resources.DisplayAttribute_Prompt), ResourceType = typeof(TestResources), #endif }; var attributes = new Attribute[] { display }; var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal("prompt from resources", context.DisplayMetadata.Placeholder()); } [Fact] public void CreateDisplayMetadata_DisplayAttribute_LocalizeProperties() { // Arrange var stringLocalizer = new Mock(MockBehavior.Strict); stringLocalizer .Setup(s => s["Model_Name"]) .Returns(() => new LocalizedString("Model_Name", "name from localizer " + CultureInfo.CurrentCulture)); stringLocalizer .Setup(s => s["Model_Description"]) .Returns(() => new LocalizedString("Model_Description", "description from localizer " + CultureInfo.CurrentCulture)); stringLocalizer .Setup(s => s["Model_Prompt"]) .Returns(() => new LocalizedString("Model_Prompt", "prompt from localizer " + CultureInfo.CurrentCulture)); var stringLocalizerFactoryMock = new Mock(MockBehavior.Strict); stringLocalizerFactoryMock .Setup(f => f.Create(It.IsAny())) .Returns(stringLocalizer.Object); var localizationOptions = new MvcDataAnnotationsLocalizationOptions(); localizationOptions.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) => { return stringLocalizerFactory.Create(type); }; var provider = CreateProvider(options: null, localizationOptions, stringLocalizerFactoryMock.Object); var display = new DisplayAttribute() { Name = "Model_Name", Description = "Model_Description", Prompt = "Model_Prompt" }; var attributes = new Attribute[] { display }; var key = ModelMetadataIdentity.ForType(typeof(DataAnnotationsMetadataProviderTest)); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert using(new CultureReplacer("en-US", "en-US")) { Assert.Equal("name from localizer en-US", context.DisplayMetadata.DisplayName()); Assert.Equal("description from localizer en-US", context.DisplayMetadata.Description()); Assert.Equal("prompt from localizer en-US", context.DisplayMetadata.Placeholder()); } using(new CultureReplacer("fr-FR", "fr-FR")) { Assert.Equal("name from localizer fr-FR", context.DisplayMetadata.DisplayName()); Assert.Equal("description from localizer fr-FR", context.DisplayMetadata.Description()); Assert.Equal("prompt from localizer fr-FR", context.DisplayMetadata.Placeholder()); } } [Theory] [InlineData(typeof(EmptyClass), false)] [InlineData(typeof(ClassWithFields), false)] [InlineData(typeof(ClassWithProperties), false)] [InlineData(typeof(EmptyEnum), true)] [InlineData(typeof(EmptyEnum?), true)] [InlineData(typeof(EnumWithDisplayNames), true)] [InlineData(typeof(EnumWithDisplayNames?), true)] [InlineData(typeof(EnumWithDuplicates), true)] [InlineData(typeof(EnumWithDuplicates?), true)] [InlineData(typeof(EnumWithFlags), true)] [InlineData(typeof(EnumWithFlags?), true)] [InlineData(typeof(EnumWithFields), true)] [InlineData(typeof(EnumWithFields?), true)] [InlineData(typeof(EmptyStruct), false)] [InlineData(typeof(StructWithFields), false)] [InlineData(typeof(StructWithFields?), false)] [InlineData(typeof(StructWithProperties), false)] public void CreateDisplayMetadata_IsEnum_ReflectsModelType(Type type, bool expectedIsEnum) { // Arrange var provider = CreateProvider(); var key = ModelMetadataIdentity.ForType(type); var attributes = new object[0]; var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal(expectedIsEnum, context.DisplayMetadata.IsEnum); } [Theory] [InlineData(typeof(EmptyClass), false)] [InlineData(typeof(ClassWithFields), false)] [InlineData(typeof(ClassWithProperties), false)] [InlineData(typeof(EmptyEnum), false)] [InlineData(typeof(EmptyEnum?), false)] [InlineData(typeof(EnumWithDisplayNames), false)] [InlineData(typeof(EnumWithDisplayNames?), false)] [InlineData(typeof(EnumWithDuplicates), false)] [InlineData(typeof(EnumWithDuplicates?), false)] [InlineData(typeof(EnumWithFlags), true)] [InlineData(typeof(EnumWithFlags?), true)] [InlineData(typeof(EnumWithFields), false)] [InlineData(typeof(EnumWithFields?), false)] [InlineData(typeof(EmptyStruct), false)] [InlineData(typeof(StructWithFields), false)] [InlineData(typeof(StructWithFields?), false)] [InlineData(typeof(StructWithProperties), false)] public void CreateDisplayMetadata_IsFlagsEnum_ReflectsModelType(Type type, bool expectedIsFlagsEnum) { // Arrange var provider = CreateProvider(); var key = ModelMetadataIdentity.ForType(type); var attributes = new object[0]; var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal(expectedIsFlagsEnum, context.DisplayMetadata.IsFlagsEnum); } // Type -> expected EnumNamesAndValues public static TheoryData> EnumNamesData { get { return new TheoryData> { { typeof(ClassWithFields), null }, { typeof(StructWithFields), null }, { typeof(StructWithFields?), null }, { typeof(EmptyEnum), new Dictionary() }, { typeof(EmptyEnum?), new Dictionary() }, { typeof(EnumWithDisplayNames), new Dictionary { { nameof(EnumWithDisplayNames.MinusTwo), "-2" }, { nameof(EnumWithDisplayNames.MinusOne), "-1" }, { nameof(EnumWithDisplayNames.Zero), "0" }, { nameof(EnumWithDisplayNames.One), "1" }, { nameof(EnumWithDisplayNames.Two), "2" }, { nameof(EnumWithDisplayNames.Three), "3" }, } }, { typeof(EnumWithDisplayNames?), new Dictionary { { nameof(EnumWithDisplayNames.MinusTwo), "-2" }, { nameof(EnumWithDisplayNames.MinusOne), "-1" }, { nameof(EnumWithDisplayNames.Zero), "0" }, { nameof(EnumWithDisplayNames.One), "1" }, { nameof(EnumWithDisplayNames.Two), "2" }, { nameof(EnumWithDisplayNames.Three), "3" }, } }, { typeof(EnumWithDuplicates), new Dictionary { { nameof(EnumWithDuplicates.Zero), "0" }, { nameof(EnumWithDuplicates.None), "0" }, { nameof(EnumWithDuplicates.One), "1" }, { nameof(EnumWithDuplicates.Two), "2" }, { nameof(EnumWithDuplicates.Duece), "2" }, { nameof(EnumWithDuplicates.Three), "3" }, { nameof(EnumWithDuplicates.MoreThanTwo), "3" }, } }, { typeof(EnumWithDuplicates?), new Dictionary { { nameof(EnumWithDuplicates.Zero), "0" }, { nameof(EnumWithDuplicates.None), "0" }, { nameof(EnumWithDuplicates.One), "1" }, { nameof(EnumWithDuplicates.Two), "2" }, { nameof(EnumWithDuplicates.Duece), "2" }, { nameof(EnumWithDuplicates.Three), "3" }, { nameof(EnumWithDuplicates.MoreThanTwo), "3" }, } }, { typeof(EnumWithFlags), new Dictionary { { nameof(EnumWithFlags.All), "-1" }, { nameof(EnumWithFlags.Zero), "0" }, { nameof(EnumWithFlags.One), "1" }, { nameof(EnumWithFlags.Two), "2" }, { nameof(EnumWithFlags.Four), "4" }, } }, { typeof(EnumWithFlags?), new Dictionary { { nameof(EnumWithFlags.All), "-1" }, { nameof(EnumWithFlags.Zero), "0" }, { nameof(EnumWithFlags.One), "1" }, { nameof(EnumWithFlags.Two), "2" }, { nameof(EnumWithFlags.Four), "4" }, } }, { typeof(EnumWithFields), new Dictionary { { nameof(EnumWithFields.MinusTwo), "-2" }, { nameof(EnumWithFields.MinusOne), "-1" }, { nameof(EnumWithFields.Zero), "0" }, { nameof(EnumWithFields.One), "1" }, { nameof(EnumWithFields.Two), "2" }, { nameof(EnumWithFields.Three), "3" }, } }, { typeof(EnumWithFields?), new Dictionary { { nameof(EnumWithFields.MinusTwo), "-2" }, { nameof(EnumWithFields.MinusOne), "-1" }, { nameof(EnumWithFields.Zero), "0" }, { nameof(EnumWithFields.One), "1" }, { nameof(EnumWithFields.Two), "2" }, { nameof(EnumWithFields.Three), "3" }, } }, }; } } [Theory] [MemberData(nameof(EnumNamesData))] public void CreateDisplayMetadata_EnumNamesAndValues_ReflectsModelType( Type type, IReadOnlyDictionary expectedDictionary) { // Arrange var provider = CreateProvider(); var key = ModelMetadataIdentity.ForType(type); var attributes = new object[0]; var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert // This assertion does *not* require entry orders to match. Assert.Equal(expectedDictionary, context.DisplayMetadata.EnumNamesAndValues); } [Fact] public void CreateDisplayMetadata_DisplayName_LocalizeWithStringLocalizer() { // Arrange var expectedKeyValuePairs = new List> { new KeyValuePair(new EnumGroupAndName("Zero", string.Empty), "0"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDisplayNames.One)), "1"), new KeyValuePair(new EnumGroupAndName(string.Empty, "dos value"), "2"), new KeyValuePair(new EnumGroupAndName(string.Empty, "tres value"), "3"), new KeyValuePair(new EnumGroupAndName(string.Empty, "name from resources"), "-2"), new KeyValuePair(new EnumGroupAndName("Negatives", "menos uno value"), "-1"), }; var type = typeof(EnumWithDisplayNames); var attributes = new object[0]; var key = ModelMetadataIdentity.ForType(type); var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); var stringLocalizer = new Mock(MockBehavior.Strict); stringLocalizer .Setup(s => s[It.IsAny()]) .Returns((index) => new LocalizedString(index, index + " value")); var stringLocalizerFactoryMock = new Mock(MockBehavior.Strict); stringLocalizerFactoryMock .Setup(f => f.Create(It.IsAny())) .Returns(stringLocalizer.Object); var localizationOptions = new MvcDataAnnotationsLocalizationOptions(); localizationOptions.DataAnnotationLocalizerProvider = (modelType, stringLocalizerFactory) => stringLocalizerFactory.Create(modelType); var provider = CreateProvider(options: null, localizationOptions, stringLocalizerFactoryMock.Object); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal( expectedKeyValuePairs, context.DisplayMetadata.EnumGroupedDisplayNamesAndValues, KVPEnumGroupAndNameComparer.Instance); } // Type -> expected EnumDisplayNamesAndValues public static TheoryData>> EnumDisplayNamesData { get { return new TheoryData>> { { typeof(ClassWithFields), null }, { typeof(StructWithFields), null }, { typeof(EmptyEnum), new List>() }, { typeof(EmptyEnum?), new List>() }, { typeof(EnumWithDisplayNames), new List> { new KeyValuePair(new EnumGroupAndName("Zero", string.Empty), "0"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDisplayNames.One)), "1"), new KeyValuePair(new EnumGroupAndName(string.Empty, "dos"), "2"), new KeyValuePair(new EnumGroupAndName(string.Empty, "tres"), "3"), new KeyValuePair(new EnumGroupAndName(string.Empty, "name from resources"), "-2"), new KeyValuePair(new EnumGroupAndName("Negatives", "menos uno"), "-1"), } }, { typeof(EnumWithDisplayNames?), new List> { new KeyValuePair(new EnumGroupAndName("Zero", string.Empty), "0"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDisplayNames.One)), "1"), new KeyValuePair(new EnumGroupAndName(string.Empty, "dos"), "2"), new KeyValuePair(new EnumGroupAndName(string.Empty, "tres"), "3"), new KeyValuePair(new EnumGroupAndName(string.Empty, "name from resources"), "-2"), new KeyValuePair(new EnumGroupAndName("Negatives", "menos uno"), "-1"), } }, { // Note order duplicates appear cannot be inferred easily e.g. does not match the source. // Zero is before None but Two is before Duece in the class below. typeof(EnumWithDuplicates), new List> { new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.Zero)), "0"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.None)), "0"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.One)), "1"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.Duece)), "2"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.Two)), "2"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.MoreThanTwo)), "3"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.Three)), "3"), } }, { typeof(EnumWithDuplicates?), new List> { new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.Zero)), "0"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.None)), "0"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.One)), "1"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.Duece)), "2"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.Two)), "2"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.MoreThanTwo)), "3"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDuplicates.Three)), "3"), } }, { typeof(EnumWithFlags), new List> { new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFlags.Zero)), "0"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFlags.One)), "1"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFlags.Two)), "2"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFlags.Four)), "4"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFlags.All)), "-1"), } }, { typeof(EnumWithFlags?), new List> { new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFlags.Zero)), "0"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFlags.One)), "1"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFlags.Two)), "2"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFlags.Four)), "4"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFlags.All)), "-1"), } }, { typeof(EnumWithFields), new List> { new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFields.Zero)), "0"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFields.One)), "1"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFields.Two)), "2"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFields.Three)), "3"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFields.MinusTwo)), "-2"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFields.MinusOne)), "-1"), } }, { typeof(EnumWithFields?), new List> { new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFields.Zero)), "0"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFields.One)), "1"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFields.Two)), "2"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFields.Three)), "3"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFields.MinusTwo)), "-2"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithFields.MinusOne)), "-1"), } }, }; } } [Theory] [MemberData(nameof(EnumDisplayNamesData))] public void CreateDisplayMetadata_EnumGroupedDisplayNamesAndValues_ReflectsModelType( Type type, IEnumerable> expectedKeyValuePairs) { // Arrange var provider = CreateProvider(); var key = ModelMetadataIdentity.ForType(type); var attributes = new object[0]; var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal( expectedKeyValuePairs, context.DisplayMetadata.EnumGroupedDisplayNamesAndValues, KVPEnumGroupAndNameComparer.Instance); } [Fact] public void CreateDisplayMetadata_EnumGroupedDisplayNamesAndValues_ReflectsDisplayAttributeOrder() { // Arrange var expectedKeyValuePairs = new List> { new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDisplayOrder.Three)), "2"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDisplayOrder.Two)), "1"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDisplayOrder.One)), "0"), new KeyValuePair(new EnumGroupAndName(string.Empty, nameof(EnumWithDisplayOrder.Null)), "3"), }; var provider = CreateProvider(); var key = ModelMetadataIdentity.ForType(typeof(EnumWithDisplayOrder)); var attributes = new object[0]; var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); // Act provider.CreateDisplayMetadata(context); // Assert Assert.Equal( expectedKeyValuePairs, context.DisplayMetadata.EnumGroupedDisplayNamesAndValues, KVPEnumGroupAndNameComparer.Instance); } [Fact] public void CreateDisplayMetadata_EnumGroupedDisplayNamesAndValues_NameWithNoIStringLocalizerAndNoResourceType() { // Arrange & Act var enumNameAndGroup = GetLocalizedEnumGroupedDisplayNamesAndValues(useStringLocalizer: false); // Assert var groupTwo = Assert.Single(enumNameAndGroup, e => e.Value.Equals("2", StringComparison.Ordinal)); using(new CultureReplacer("en-US", "en-US")) { Assert.Equal("Loc_Two_Name", groupTwo.Key.Name); } using(new CultureReplacer("fr-FR", "fr-FR")) { Assert.Equal("Loc_Two_Name", groupTwo.Key.Name); } } [Fact] public void CreateDisplayMetadata_EnumGroupedDisplayNamesAndValues_NameWithIStringLocalizerAndNoResourceType() { // Arrange & Act var enumNameAndGroup = GetLocalizedEnumGroupedDisplayNamesAndValues(useStringLocalizer: true); // Assert var groupTwo = Assert.Single(enumNameAndGroup, e => e.Value.Equals("2", StringComparison.Ordinal)); using(new CultureReplacer("en-US", "en-US")) { Assert.Equal("Loc_Two_Name en-US", groupTwo.Key.Name); } using(new CultureReplacer("fr-FR", "fr-FR")) { Assert.Equal("Loc_Two_Name fr-FR", groupTwo.Key.Name); } } [Fact] public void CreateDisplayMetadata_EnumGroupedDisplayNamesAndValues_NameWithNoIStringLocalizerAndResourceType() { // Arrange & Act var enumNameAndGroup = GetLocalizedEnumGroupedDisplayNamesAndValues(useStringLocalizer: false); // Assert var groupThree = Assert.Single(enumNameAndGroup, e => e.Value.Equals("3", StringComparison.Ordinal)); using(new CultureReplacer("en-US", "en-US")) { Assert.Equal("type three name en-US", groupThree.Key.Name); } using(new CultureReplacer("fr-FR", "fr-FR")) { Assert.Equal("type three name fr-FR", groupThree.Key.Name); } } [Fact] public void CreateDisplayMetadata_EnumGroupedDisplayNamesAndValues_NameWithIStringLocalizerAndResourceType() { // Arrange & Act var enumNameAndGroup = GetLocalizedEnumGroupedDisplayNamesAndValues(useStringLocalizer: true); var groupThree = Assert.Single(enumNameAndGroup, e => e.Value.Equals("3", StringComparison.Ordinal)); // Assert using(new CultureReplacer("en-US", "en-US")) { Assert.Equal("type three name en-US", groupThree.Key.Name); } using(new CultureReplacer("fr-FR", "fr-FR")) { Assert.Equal("type three name fr-FR", groupThree.Key.Name); } } [Fact] public void CreateValidationMetadata_RequiredAttribute_SetsIsRequiredToTrue() { // Arrange var provider = CreateProvider(); var required = new RequiredAttribute(); var attributes = new Attribute[] { required }; var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)); var context = new ValidationMetadataProviderContext(key, GetModelAttributes(new object[0], attributes)); // Act provider.CreateValidationMetadata(context); // Assert Assert.True(context.ValidationMetadata.IsRequired); } [Theory] [InlineData(true)] [InlineData(false)] [InlineData(null)] public void CreateValidationMetadata_NoRequiredAttribute_IsRequiredLeftAlone(bool? initialValue) { // Arrange var provider = CreateProvider(); var attributes = new Attribute[] { }; var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)); var context = new ValidationMetadataProviderContext(key, GetModelAttributes(new object[0], attributes)); context.ValidationMetadata.IsRequired = initialValue; // Act provider.CreateValidationMetadata(context); // Assert Assert.Equal(initialValue, context.ValidationMetadata.IsRequired); } [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/11828")] public void CreateValidationMetadata_InfersRequiredAttribute_NoNonNullableProperty() { // Arrange var provider = CreateProvider(); var attributes = ModelAttributes.GetAttributesForProperty( typeof(NullableReferenceTypes), typeof(NullableReferenceTypes).GetProperty(nameof(NullableReferenceTypes.NonNullableReferenceType))); var key = ModelMetadataIdentity.ForProperty( typeof(NullableReferenceTypes), nameof(NullableReferenceTypes.NonNullableReferenceType), typeof(string)); var context = new ValidationMetadataProviderContext(key, attributes); // Act provider.CreateValidationMetadata(context); // Assert Assert.True(context.ValidationMetadata.IsRequired); var attribute = Assert.Single(context.ValidationMetadata.ValidatorMetadata, m => m is RequiredAttribute); Assert.True(((RequiredAttribute)attribute).AllowEmptyStrings); // non-Default for [Required] } [Fact] public void CreateValidationMetadata_InfersRequiredAttribute_NoNonNullableProperty_PrefersExistingRequiredAttribute() { // Arrange var provider = CreateProvider(); var attributes = ModelAttributes.GetAttributesForProperty( typeof(NullableReferenceTypes), typeof(NullableReferenceTypes).GetProperty(nameof(NullableReferenceTypes.NonNullableReferenceTypeWithRequired))); var key = ModelMetadataIdentity.ForProperty( typeof(NullableReferenceTypes), nameof(NullableReferenceTypes.NonNullableReferenceTypeWithRequired), typeof(string)); var context = new ValidationMetadataProviderContext(key, attributes); // Act provider.CreateValidationMetadata(context); // Assert Assert.True(context.ValidationMetadata.IsRequired); var attribute = Assert.Single(context.ValidationMetadata.ValidatorMetadata, m => m is RequiredAttribute a); Assert.Equal("Test", ((RequiredAttribute)attribute).ErrorMessage); Assert.False(((RequiredAttribute)attribute).AllowEmptyStrings); // Default for [Required] } [Fact] public void CreateValidationMetadata_SuppressRequiredInference_Noops() { // Arrange var provider = CreateProvider(options: new MvcOptions() { SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true, }); var attributes = ModelAttributes.GetAttributesForProperty( typeof(NullableReferenceTypes), typeof(NullableReferenceTypes).GetProperty(nameof(NullableReferenceTypes.NonNullableReferenceType))); var key = ModelMetadataIdentity.ForProperty( typeof(NullableReferenceTypes), nameof(NullableReferenceTypes.NonNullableReferenceType), typeof(string)); var context = new ValidationMetadataProviderContext(key, attributes); // Act provider.CreateValidationMetadata(context); // Assert Assert.Null(context.ValidationMetadata.IsRequired); Assert.DoesNotContain(context.ValidationMetadata.ValidatorMetadata, m => m is RequiredAttribute); } [Fact] public void CreateValidationMetadata_WillAddValidationAttributes_From_ValidationProviderAttribute() { // Arrange var provider = CreateProvider(); var validationProviderAttribute = new FooCompositeValidationAttribute( attributes: new List { new RequiredAttribute(), new StringLengthAttribute(5) }); var attributes = new Attribute[] { new EmailAddressAttribute(), validationProviderAttribute }; var key = ModelMetadataIdentity.ForProperty(typeof(string), "Length", typeof(string)); var context = new ValidationMetadataProviderContext(key, GetModelAttributes(new object[0], attributes)); // Act provider.CreateValidationMetadata(context); // Assert var expected = new List { new EmailAddressAttribute(), new RequiredAttribute(), new StringLengthAttribute(5) }; Assert.Equal(expected, actual: context.ValidationMetadata.ValidatorMetadata); } // [Required] has no effect on IsBindingRequired [Theory] [InlineData(true)] [InlineData(false)] public void CreateBindingMetadata_RequiredAttribute_IsBindingRequiredLeftAlone(bool initialValue) { // Arrange var provider = CreateProvider(); var attributes = new Attribute[] { new RequiredAttribute() }; var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)); var context = new BindingMetadataProviderContext(key, GetModelAttributes(new object[0], attributes)); context.BindingMetadata.IsBindingRequired = initialValue; // Act provider.CreateBindingMetadata(context); // Assert Assert.Equal(initialValue, context.BindingMetadata.IsBindingRequired); } [Theory] [InlineData(true)] [InlineData(false)] [InlineData(null)] public void CreateBindingDetails_NoEditableAttribute_IsReadOnlyLeftAlone(bool? initialValue) { // Arrange var provider = CreateProvider(); var attributes = new Attribute[] { }; var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)); var context = new BindingMetadataProviderContext(key, GetModelAttributes(new object[0], attributes)); context.BindingMetadata.IsReadOnly = initialValue; // Act provider.CreateBindingMetadata(context); // Assert Assert.Equal(initialValue, context.BindingMetadata.IsReadOnly); } [Fact] public void CreateValidationDetails_ValidatableObject_ReturnsObject() { // Arrange var provider = CreateProvider(); var attribute = new TestValidationAttribute(); var attributes = new Attribute[] { attribute }; var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)); var context = new ValidationMetadataProviderContext(key, GetModelAttributes(new object[0], attributes)); // Act provider.CreateValidationMetadata(context); // Assert var validatorMetadata = Assert.Single(context.ValidationMetadata.ValidatorMetadata); Assert.Same(attribute, validatorMetadata); } [Fact] public void CreateValidationDetails_ValidatableObject_AlreadyInContext_Ignores() { // Arrange var provider = CreateProvider(); var attribute = new TestValidationAttribute(); var attributes = new Attribute[] { attribute }; var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)); var context = new ValidationMetadataProviderContext(key, GetModelAttributes(new object[0], attributes)); context.ValidationMetadata.ValidatorMetadata.Add(attribute); // Act provider.CreateValidationMetadata(context); // Assert var validatorMetadata = Assert.Single(context.ValidationMetadata.ValidatorMetadata); Assert.Same(attribute, validatorMetadata); } [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/11828")] public void IsNonNullable_FindsNonNullableProperty() { // Arrange var type = typeof(NullableReferenceTypes); var property = type.GetProperty(nameof(NullableReferenceTypes.NonNullableReferenceType)); // Act var result = DataAnnotationsMetadataProvider.IsNonNullable(property.GetCustomAttributes(inherit: true)); // Assert Assert.True(result); } [Fact] public void IsNonNullable_FindsNullableProperty() { // Arrange var type = typeof(NullableReferenceTypes); var property = type.GetProperty(nameof(NullableReferenceTypes.NullableReferenceType)); // Act var result = DataAnnotationsMetadataProvider.IsNonNullable(property.GetCustomAttributes(inherit: true)); // Assert Assert.False(result); } [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/11828")] public void IsNonNullable_FindsNonNullableParameter() { // Arrange var type = typeof(NullableReferenceTypes); var method = type.GetMethod(nameof(NullableReferenceTypes.Method)); var parameter = method.GetParameters().Where(p => p.Name == "nonNullableParameter").Single(); // Act var result = DataAnnotationsMetadataProvider.IsNonNullable(parameter.GetCustomAttributes(inherit: true)); // Assert Assert.True(result); } [Fact] public void IsNonNullable_FindsNullableParameter() { // Arrange var type = typeof(NullableReferenceTypes); var method = type.GetMethod(nameof(NullableReferenceTypes.Method)); var parameter = method.GetParameters().Where(p => p.Name == "nullableParameter").Single(); // Act var result = DataAnnotationsMetadataProvider.IsNonNullable(parameter.GetCustomAttributes(inherit: true)); // Assert Assert.False(result); } private IEnumerable> GetLocalizedEnumGroupedDisplayNamesAndValues( bool useStringLocalizer) { var provider = CreateIStringLocalizerProvider(useStringLocalizer); var key = ModelMetadataIdentity.ForType(typeof(EnumWithLocalizedDisplayNames)); var attributes = new object[0]; var context = new DisplayMetadataProviderContext(key, GetModelAttributes(attributes)); provider.CreateDisplayMetadata(context); return context.DisplayMetadata.EnumGroupedDisplayNamesAndValues; } private DataAnnotationsMetadataProvider CreateProvider( MvcOptions options = null, MvcDataAnnotationsLocalizationOptions localizationOptions = null, IStringLocalizerFactory stringLocalizerFactory = null) { return new DataAnnotationsMetadataProvider( options ?? new MvcOptions(), Options.Create(localizationOptions ?? new MvcDataAnnotationsLocalizationOptions()), stringLocalizerFactory); } private DataAnnotationsMetadataProvider CreateIStringLocalizerProvider(bool useStringLocalizer) { var stringLocalizer = new Mock(MockBehavior.Strict); stringLocalizer .Setup(loc => loc[It.IsAny()]) .Returns((k => { return new LocalizedString(k, $"{k} {CultureInfo.CurrentCulture}"); })); var stringLocalizerFactory = new Mock(MockBehavior.Strict); stringLocalizerFactory .Setup(factory => factory.Create(typeof(EnumWithLocalizedDisplayNames))) .Returns(stringLocalizer.Object); var localizationOptions = new MvcDataAnnotationsLocalizationOptions(); localizationOptions.DataAnnotationLocalizerProvider = (modelType, localizerFactory) => localizerFactory.Create(modelType); return CreateProvider(options: null, localizationOptions, useStringLocalizer ? stringLocalizerFactory.Object : null); } private ModelAttributes GetModelAttributes(IEnumerable typeAttributes) => new ModelAttributes(typeAttributes, Array.Empty(), Array.Empty()); private ModelAttributes GetModelAttributes( IEnumerable typeAttributes, IEnumerable propertyAttributes) => new ModelAttributes(typeAttributes, propertyAttributes, Array.Empty()); private class KVPEnumGroupAndNameComparer : IEqualityComparer> { public static readonly IEqualityComparer> Instance = new KVPEnumGroupAndNameComparer(); private KVPEnumGroupAndNameComparer() { } public bool Equals(KeyValuePair x, KeyValuePair y) { using(new CultureReplacer(string.Empty, string.Empty)) { return x.Key.Name.Equals(y.Key.Name, StringComparison.Ordinal) && x.Key.Group.Equals(y.Key.Group, StringComparison.Ordinal); } } public int GetHashCode(KeyValuePair obj) { using(new CultureReplacer(string.Empty, string.Empty)) { return obj.Key.GetHashCode(); } } } private class TestValidationAttribute : ValidationAttribute, IClientModelValidator { public void AddValidation(ClientModelValidationContext context) { throw new NotImplementedException(); } } private class EmptyClass { } private class ClassWithFields { public const int Zero = 0; public const int One = 1; } private class ClassWithProperties { public int Id { get; set; } public string Name { get; set; } } private enum EnumWithLocalizedDisplayNames { [Display(Name = "Loc_Two_Name")] Two = 2, [Display(Name = nameof(TestResources.Type_Three_Name), ResourceType = typeof(TestResources))] Three = 3 } private enum EmptyEnum { } private enum EnumWithDisplayNames { [Display(Name = "tres")] Three = 3, [Display(Name = "dos")] Two = 2, // Display attribute exists but does not set Name. [Display(ShortName = "uno")] One = 1, [Display(Name = "", GroupName = "Zero")] Zero = 0, [Display(Name = "menos uno", GroupName = "Negatives")] MinusOne = -1, #if USE_REAL_RESOURCES [Display(Name = nameof(Test.Resources.DisplayAttribute_Name), ResourceType = typeof(Test.Resources))] #else [Display(Name = nameof(TestResources.DisplayAttribute_Name), ResourceType = typeof(TestResources))] #endif MinusTwo = -2, } private enum EnumWithDisplayOrder { [Display(Order = 3)] One, [Display(Order = 2)] Two, [Display(Order = 1)] Three, Null, } private enum EnumWithDuplicates { Zero = 0, One = 1, Three = 3, MoreThanTwo = 3, Two = 2, None = 0, Duece = 2, } [Flags] private enum EnumWithFlags { Four = 4, Two = 2, One = 1, Zero = 0, All = -1, } private enum EnumWithFields { MinusTwo = -2, MinusOne = -1, Three = 3, Two = 2, One = 1, Zero = 0, } private struct EmptyStruct { } private struct StructWithFields { public const int Zero = 0; public const int One = 1; } private struct StructWithProperties { public StructWithProperties(int id, string name) { Id = id; Name = name; } public int Id { get; private set; } public string Name { get; private set; } } private class FooCompositeValidationAttribute : ValidationProviderAttribute { private readonly IEnumerable _attributes; public FooCompositeValidationAttribute(IEnumerable attributes) { _attributes = attributes; } public override IEnumerable GetValidationAttributes() { return _attributes; } } #nullable enable private class NullableReferenceTypes { public string NonNullableReferenceType { get; set; } = default!; [Required(ErrorMessage = "Test")] public string NonNullableReferenceTypeWithRequired { get; set; } = default!; public string? NullableReferenceType { get; set; } = default!; public void Method(string nonNullableParameter, string? nullableParameter) { } } #nullable restore } }