diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs index f8b91947a8..c0f2ee588f 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs @@ -9,6 +9,7 @@ using System.Reflection; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { @@ -22,9 +23,18 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal IValidationMetadataProvider { private readonly IStringLocalizerFactory _stringLocalizerFactory; + private readonly MvcDataAnnotationsLocalizationOptions _localizationOptions; - public DataAnnotationsMetadataProvider(IStringLocalizerFactory stringLocalizerFactory) + public DataAnnotationsMetadataProvider( + IOptions options, + IStringLocalizerFactory stringLocalizerFactory) { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _localizationOptions = options.Value; _stringLocalizerFactory = stringLocalizerFactory; } @@ -88,9 +98,12 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal displayMetadata.DataTypeName = DataType.Html.ToString(); } - var containerType = context.Key.ContainerType ?? context.Key.ModelType; - var localizer = _stringLocalizerFactory?.Create(containerType); + IStringLocalizer localizer = null; + if (_stringLocalizerFactory != null && _localizationOptions.DataAnnotationLocalizerProvider != null) + { + localizer = _localizationOptions.DataAnnotationLocalizerProvider(containerType, _stringLocalizerFactory); + } // Description if (displayAttribute != null) diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/MvcDataAnnotationsMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/MvcDataAnnotationsMvcOptionsSetup.cs index 6e28344dc2..1b94e5c157 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/MvcDataAnnotationsMvcOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/MvcDataAnnotationsMvcOptionsSetup.cs @@ -50,7 +50,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal throw new ArgumentNullException(nameof(options)); } - options.ModelMetadataDetailsProviders.Add(new DataAnnotationsMetadataProvider(_stringLocalizerFactory)); + options.ModelMetadataDetailsProviders.Add(new DataAnnotationsMetadataProvider( + _dataAnnotationLocalizationOptions, + _stringLocalizerFactory)); options.ModelValidatorProviders.Add(new DataAnnotationsModelValidatorProvider( _validationAttributeAdapterProvider, diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs index f53fa7d348..c8ee22183d 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs @@ -2,12 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; -using System.Linq; -using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; @@ -65,7 +62,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal object expected) { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var key = ModelMetadataIdentity.ForType(typeof(string)); var context = new DisplayMetadataProviderContext(key, new ModelAttributes(new object[] { attribute })); @@ -82,7 +81,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateDisplayMetadata_FindsDisplayFormat_FromDataType() { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var dataType = new DataTypeAttribute(DataType.Currency); var displayFormat = dataType.DisplayFormat; // Non-null for DataType.Currency. @@ -102,7 +103,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateDisplayMetadata_FindsDisplayFormat_OverridingDataType() { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var dataType = new DataTypeAttribute(DataType.Time); // Has a non-null DisplayFormat. var displayFormat = new DisplayFormatAttribute() // But these values override the values from DataType @@ -125,7 +128,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateBindingMetadata_EditableAttributeFalse_SetsReadOnlyTrue() { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var editable = new EditableAttribute(allowEdit: false); @@ -144,7 +149,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateBindingMetadata_EditableAttributeTrue_SetsReadOnlyFalse() { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var editable = new EditableAttribute(allowEdit: true); @@ -159,12 +166,52 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal Assert.False(context.BindingMetadata.IsReadOnly); } + [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 options = new TestOptionsManager(); + bool dataAnnotationLocalizerProviderWasUsed = false; + options.Value.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) => + { + dataAnnotationLocalizerProviderWasUsed = true; + return stringLocalizerFactory.Create(typeof(EmptyClass)); + }; + + var provider = new DataAnnotationsMetadataProvider(options, stringLocalizerFactoryMock.Object); + + var display = new DisplayAttribute() + { + Name = "DisplayName" + }; + + var attributes = new Attribute[] { display }; + var key = ModelMetadataIdentity.ForType(typeof(string)); + var context = new DisplayMetadataProviderContext(key, new ModelAttributes(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 = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var display = new DisplayAttribute() { @@ -199,7 +246,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal stringLocalizerFactory .Setup(s => s.Create(It.IsAny())) .Returns(() => stringLocalizer.Object); - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory.Object); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory.Object); var display = new DisplayAttribute() { @@ -234,7 +283,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal stringLocalizerFactory .Setup(s => s.Create(It.IsAny())) .Returns(() => stringLocalizer.Object); - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory.Object); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory.Object); var display = new DisplayAttribute() { @@ -263,7 +314,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateDisplayMetadata_DisplayAttribute_DescriptionFromResources_NullLocalizer() { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var display = new DisplayAttribute() { @@ -298,7 +351,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal stringLocalizerFactory .Setup(s => s.Create(It.IsAny())) .Returns(() => stringLocalizer.Object); - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory.Object); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory.Object); var display = new DisplayAttribute() { @@ -327,7 +382,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateDisplayMetadata_DisplayAttribute_PromptFromResources_NullLocalizer() { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var display = new DisplayAttribute() { @@ -366,12 +423,20 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal .Setup(s => s["Model_Prompt"]) .Returns(() => new LocalizedString("Model_Prompt", "prompt from localizer " + CultureInfo.CurrentCulture)); - var stringLocalizerFactory = new Mock(MockBehavior.Strict); - stringLocalizerFactory + var stringLocalizerFactoryMock = new Mock(MockBehavior.Strict); + stringLocalizerFactoryMock .Setup(f => f.Create(It.IsAny())) .Returns(stringLocalizer.Object); - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory.Object); + var options = new TestOptionsManager(); + options.Value.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) => + { + return stringLocalizerFactory.Create(type); + }; + + var provider = new DataAnnotationsMetadataProvider( + options, + stringLocalizerFactoryMock.Object); var display = new DisplayAttribute() { @@ -423,7 +488,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateDisplayMetadata_IsEnum_ReflectsModelType(Type type, bool expectedIsEnum) { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var key = ModelMetadataIdentity.ForType(type); var attributes = new object[0]; @@ -457,7 +524,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateDisplayMetadata_IsFlagsEnum_ReflectsModelType(Type type, bool expectedIsFlagsEnum) { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var key = ModelMetadataIdentity.ForType(type); var attributes = new object[0]; @@ -589,7 +658,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal IReadOnlyDictionary expectedDictionary) { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var key = ModelMetadataIdentity.ForType(type); var attributes = new object[0]; @@ -633,7 +704,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal .Setup(f => f.Create(It.IsAny())) .Returns(stringLocalizer.Object); - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory.Object); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory.Object); // Act provider.CreateDisplayMetadata(context); @@ -765,7 +838,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal IEnumerable> expectedKeyValuePairs) { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var key = ModelMetadataIdentity.ForType(type); var attributes = new object[0]; @@ -865,7 +940,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateValidationMetadata_RequiredAttribute_SetsIsRequiredToTrue() { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var required = new RequiredAttribute(); @@ -887,7 +964,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateValidationMetadata_NoRequiredAttribute_IsRequiredLeftAlone(bool? initialValue) { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var attributes = new Attribute[] { }; var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)); @@ -908,7 +987,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateBindingMetadata_RequiredAttribute_IsBindingRequiredLeftAlone(bool initialValue) { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var attributes = new Attribute[] { new RequiredAttribute() }; var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)); @@ -929,7 +1010,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateBindingDetails_NoEditableAttribute_IsReadOnlyLeftAlone(bool? initialValue) { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var attributes = new Attribute[] { }; var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)); @@ -947,7 +1030,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateValidationDetails_ValidatableObject_ReturnsObject() { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var attribute = new TestValidationAttribute(); var attributes = new Attribute[] { attribute }; @@ -966,7 +1051,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public void CreateValidationDetails_ValidatableObject_AlreadyInContext_Ignores() { // Arrange - var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null); + var provider = new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null); var attribute = new TestValidationAttribute(); var attributes = new Attribute[] { attribute }; @@ -1012,6 +1099,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal .Returns(stringLocalizer.Object); return new DataAnnotationsMetadataProvider( + new TestOptionsManager(), useStringLocalizer ? stringLocalizerFactory.Object : null); } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelMetadataProviderTest.cs index c413d822e8..b42894bdbf 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelMetadataProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelMetadataProviderTest.cs @@ -1050,7 +1050,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[] { new DefaultBindingMetadataProvider(), - new DataAnnotationsMetadataProvider(stringLocalizerFactory: null), + new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null), }), new TestOptionsManager()) { diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs index 4b9a418f64..ef5b220eab 100644 --- a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; @@ -21,7 +22,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { new DefaultBindingMetadataProvider(), new DefaultValidationMetadataProvider(), - new DataAnnotationsMetadataProvider(stringLocalizerFactory), + new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory), new DataMemberRequiredBindingMetadataProvider(), }; @@ -35,7 +38,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { new DefaultBindingMetadataProvider(), new DefaultValidationMetadataProvider(), - new DataAnnotationsMetadataProvider(stringLocalizerFactory: null), + new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null), new DataMemberRequiredBindingMetadataProvider(), }; @@ -70,7 +75,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { new DefaultBindingMetadataProvider(), new DefaultValidationMetadataProvider(), - new DataAnnotationsMetadataProvider(stringLocalizerFactory: null), + new DataAnnotationsMetadataProvider( + new TestOptionsManager(), + stringLocalizerFactory: null), detailsProvider }), new TestOptionsManager())