diff --git a/samples/MvcSandbox/Startup.cs b/samples/MvcSandbox/Startup.cs index b963a03a6f..0b3360ec8a 100644 --- a/samples/MvcSandbox/Startup.cs +++ b/samples/MvcSandbox/Startup.cs @@ -14,7 +14,7 @@ namespace MvcSandbox // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_1); + services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_2_2); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs b/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs index 91372a0d19..0260f464a7 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/CompatibilityVersion.cs @@ -59,6 +59,18 @@ namespace Microsoft.AspNetCore.Mvc /// Version_2_1, + /// + /// Sets the default value of settings on to match the behavior of + /// ASP.NET Core MVC 2.2. + /// + /// + /// ASP.NET Core MVC 2.2 introduces compatibility switches for the following: + /// + /// MvcDataAnnotationsLocalizationOptions.AllowDataAnnotationsLocalizationForEnumDisplayAttributes + /// + /// + Version_2_2, + /// /// Sets the default value of settings on to match the latest release. Use this /// value with care, upgrading minor versions will cause breaking changes when using . diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsLocalizationServices.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsLocalizationServices.cs index f54cd9dd5d..3ef1fde707 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsLocalizationServices.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsLocalizationServices.cs @@ -27,6 +27,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal , MvcDataAnnotationsLocalizationOptionsSetup>()); } + + services.TryAddEnumerable( + ServiceDescriptor.Transient, MvcDataAnnotationsLocalizationConfigureCompatibilityOptions>()); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs index 0248cee5c5..b91e4b9501 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs @@ -182,7 +182,19 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var groupedDisplayNamesAndValues = new List>(); var namesAndValues = new Dictionary(); - var enumLocalizer = _stringLocalizerFactory?.Create(underlyingType); + + IStringLocalizer enumLocalizer = null; + if (_localizationOptions.AllowDataAnnotationsLocalizationForEnumDisplayAttributes) + { + if (_stringLocalizerFactory != null && _localizationOptions.DataAnnotationLocalizerProvider != null) + { + enumLocalizer = _localizationOptions.DataAnnotationLocalizerProvider(underlyingType, _stringLocalizerFactory); + } + } + else + { + enumLocalizer = _stringLocalizerFactory?.Create(underlyingType); + } var enumFields = Enum.GetNames(underlyingType) .Select(name => underlyingType.GetField(name)) diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/MvcDataAnnotationsLocalizationConfigureCompatibilityOptions.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/MvcDataAnnotationsLocalizationConfigureCompatibilityOptions.cs new file mode 100644 index 0000000000..e3eab734b7 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/MvcDataAnnotationsLocalizationConfigureCompatibilityOptions.cs @@ -0,0 +1,35 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.DataAnnotations +{ + internal class MvcDataAnnotationsLocalizationConfigureCompatibilityOptions : ConfigureCompatibilityOptions + { + public MvcDataAnnotationsLocalizationConfigureCompatibilityOptions( + ILoggerFactory loggerFactory, + IOptions compatibilityOptions) + : base(loggerFactory, compatibilityOptions) + { + } + + protected override IReadOnlyDictionary DefaultValues + { + get + { + var values = new Dictionary(); + + if (Version >= CompatibilityVersion.Version_2_2) + { + values[nameof(MvcDataAnnotationsLocalizationOptions.AllowDataAnnotationsLocalizationForEnumDisplayAttributes)] = true; + } + + return values; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/MvcDataAnnotationsLocalizationOptions.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/MvcDataAnnotationsLocalizationOptions.cs index 99d8742296..911c7139c6 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/MvcDataAnnotationsLocalizationOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/MvcDataAnnotationsLocalizationOptions.cs @@ -2,6 +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 Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Localization; namespace Microsoft.AspNetCore.Mvc.DataAnnotations @@ -9,11 +12,60 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations /// /// Provides programmatic configuration for DataAnnotations localization in the MVC framework. /// - public class MvcDataAnnotationsLocalizationOptions + public class MvcDataAnnotationsLocalizationOptions : IEnumerable { + private readonly CompatibilitySwitch _allowDataAnnotationsLocalizationForEnumDisplayAttributes; + private readonly ICompatibilitySwitch[] _switches; + /// /// The delegate to invoke for creating . /// public Func DataAnnotationLocalizerProvider; + + /// + /// Instantiates a new instance of the class. + /// + public MvcDataAnnotationsLocalizationOptions() + { + _allowDataAnnotationsLocalizationForEnumDisplayAttributes = new CompatibilitySwitch(nameof(AllowDataAnnotationsLocalizationForEnumDisplayAttributes)); + + _switches = new ICompatibilitySwitch[] + { + _allowDataAnnotationsLocalizationForEnumDisplayAttributes + }; + } + + /// + /// Gets or sets a value that determines if should be used while localizing types. + /// If set to true will be used in localizing types. + /// If set to false the localization will search for values in resource files for the . + /// + /// + /// + /// This property is associated with a compatibility switch and can provide a different behavior depending on + /// the configured compatibility version for the application. See for guidance and examples of setting the application's compatibility version. + /// + /// + /// Configuring the desired value of the compatibility switch by calling this property's setter will take precedence + /// over the value implied by the application's . + /// + /// + /// If the application's compatibility version is set to or then + /// this setting will have the value false unless explicitly configured. + /// + /// + /// If the application's compatibility version is set to or + /// higher then this setting will have the value true unless explicitly configured. + /// + /// + public bool AllowDataAnnotationsLocalizationForEnumDisplayAttributes + { + get => _allowDataAnnotationsLocalizationForEnumDisplayAttributes.Value; + set => _allowDataAnnotationsLocalizationForEnumDisplayAttributes.Value = value; + } + + public IEnumerator GetEnumerator() => ((IEnumerable)_switches).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator(); } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.TestCommon/TestModelMetadataProvider.cs b/test/Microsoft.AspNetCore.Mvc.Core.TestCommon/TestModelMetadataProvider.cs index 47f6b7d2ed..faff630e0b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.TestCommon/TestModelMetadataProvider.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.TestCommon/TestModelMetadataProvider.cs @@ -16,6 +16,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { public class TestModelMetadataProvider : DefaultModelMetadataProvider { + private static DataAnnotationsMetadataProvider CreateDefaultDataAnnotationsProvider(IStringLocalizerFactory stringLocalizerFactory) + { + var options = Options.Create(new MvcDataAnnotationsLocalizationOptions()); + options.Value.DataAnnotationLocalizerProvider = (modelType, localizerFactory) => localizerFactory.Create(modelType); + + return new DataAnnotationsMetadataProvider(options, stringLocalizerFactory); + } + // Creates a provider with all the defaults - includes data annotations public static ModelMetadataProvider CreateDefaultProvider(IStringLocalizerFactory stringLocalizerFactory = null) { @@ -23,9 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding { new DefaultBindingMetadataProvider(), new DefaultValidationMetadataProvider(), - new DataAnnotationsMetadataProvider( - Options.Create(new MvcDataAnnotationsLocalizationOptions()), - stringLocalizerFactory), + CreateDefaultDataAnnotationsProvider(stringLocalizerFactory), new DataMemberRequiredBindingMetadataProvider(), }; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.TestCommon/TestModelValidatorProvider.cs b/test/Microsoft.AspNetCore.Mvc.Core.TestCommon/TestModelValidatorProvider.cs index 96d714ee24..230f7970b4 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.TestCommon/TestModelValidatorProvider.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.TestCommon/TestModelValidatorProvider.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation @@ -12,15 +13,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation public class TestModelValidatorProvider : CompositeModelValidatorProvider { // Creates a provider with all the defaults - includes data annotations - public static CompositeModelValidatorProvider CreateDefaultProvider() + public static CompositeModelValidatorProvider CreateDefaultProvider(IStringLocalizerFactory stringLocalizerFactory = null) { + var options = Options.Create(new MvcDataAnnotationsLocalizationOptions()); + options.Value.DataAnnotationLocalizerProvider = (modelType, localizerFactory) => localizerFactory.Create(modelType); + var providers = new IModelValidatorProvider[] { new DefaultModelValidatorProvider(), new DataAnnotationsModelValidatorProvider( new ValidationAttributeAdapterProvider(), - Options.Create(new MvcDataAnnotationsLocalizationOptions()), - stringLocalizerFactory: null) + options, + stringLocalizerFactory) }; return new TestModelValidatorProvider(providers); @@ -31,4 +35,4 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation { } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs index ad36bec14c..10dfe11b01 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Internal; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using Moq; @@ -18,6 +17,12 @@ using Xunit; namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { + public enum TestEnum + { + [Display(Name = "DisplayNameValue")] + DisplayNameValue + } + public class DataAnnotationsMetadataProviderTest { // Includes attributes with a 'simple' effect on display details. @@ -270,6 +275,94 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal Assert.Equal("DisplayNameAttributeValue", context.DisplayMetadata.DisplayName()); } + [Fact] + public void CreateDisplayMetadata_DisplayNameAttribute_OnEnum_CompatSwitchWorks() + { + // Arrange + var unsharedLocalizer = new Mock(MockBehavior.Strict); + unsharedLocalizer + .Setup(s => s["DisplayNameValue"]) + .Returns(new LocalizedString("DisplaynameValue", "didn't use shared")); + + var sharedLocalizer = new Mock(MockBehavior.Strict); + sharedLocalizer + .Setup(s => s["DisplayNameValue"]) + .Returns(() => new LocalizedString("DisplayNameValue", "used shared")); + + var stringLocalizerFactoryMock = new Mock(MockBehavior.Strict); + stringLocalizerFactoryMock + .Setup(s => s.Create(typeof(TestEnum))) + .Returns(() => unsharedLocalizer.Object); + stringLocalizerFactoryMock + .Setup(s => s.Create(typeof(EmptyClass))) + .Returns(() => sharedLocalizer.Object); + + var localizationOptions = Options.Create(new MvcDataAnnotationsLocalizationOptions()); + localizationOptions.Value.AllowDataAnnotationsLocalizationForEnumDisplayAttributes = false; + localizationOptions.Value.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) => + { + return stringLocalizerFactory.Create(typeof(EmptyClass)); + }; + + var provider = new DataAnnotationsMetadataProvider( + localizationOptions, + stringLocalizerFactory: 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("didn't use shared", e.Key.Name)); + } + + [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 = Options.Create(new MvcDataAnnotationsLocalizationOptions()); + localizationOptions.Value.AllowDataAnnotationsLocalizationForEnumDisplayAttributes = true; + localizationOptions.Value.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) => + { + return stringLocalizerFactory.Create(typeof(EmptyClass)); + }; + + var provider = new DataAnnotationsMetadataProvider( + localizationOptions, + stringLocalizerFactory: 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() { @@ -568,16 +661,13 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal stringLocalizerFactoryMock .Setup(f => f.Create(It.IsAny())) .Returns(stringLocalizer.Object); - var options = Options.Create(new MvcDataAnnotationsLocalizationOptions()); options.Value.DataAnnotationLocalizerProvider = (type, stringLocalizerFactory) => { return stringLocalizerFactory.Create(type); }; - var provider = new DataAnnotationsMetadataProvider( - options, - stringLocalizerFactoryMock.Object); + var provider = new DataAnnotationsMetadataProvider(options, stringLocalizerFactoryMock.Object); var display = new DisplayAttribute() { @@ -594,13 +684,13 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal provider.CreateDisplayMetadata(context); // Assert - using (new CultureReplacer("en-US", "en-US")) + 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")) + 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()); @@ -840,14 +930,14 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal .Setup(s => s[It.IsAny()]) .Returns((index) => new LocalizedString(index, index + " value")); - 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( - Options.Create(new MvcDataAnnotationsLocalizationOptions()), - stringLocalizerFactory.Object); + var options = Options.Create(new MvcDataAnnotationsLocalizationOptions()); + options.Value.DataAnnotationLocalizerProvider = (modelType, stringLocalizerFactory) => stringLocalizerFactory.Create(modelType); + var provider = new DataAnnotationsMetadataProvider(options, stringLocalizerFactoryMock.Object); // Act provider.CreateDisplayMetadata(context); @@ -1036,12 +1126,12 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal // Assert var groupTwo = Assert.Single(enumNameAndGroup, e => e.Value.Equals("2", StringComparison.Ordinal)); - using (new CultureReplacer("en-US", "en-US")) + using(new CultureReplacer("en-US", "en-US")) { Assert.Equal("Loc_Two_Name", groupTwo.Key.Name); } - using (new CultureReplacer("fr-FR", "fr-FR")) + using(new CultureReplacer("fr-FR", "fr-FR")) { Assert.Equal("Loc_Two_Name", groupTwo.Key.Name); } @@ -1056,12 +1146,12 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal // Assert var groupTwo = Assert.Single(enumNameAndGroup, e => e.Value.Equals("2", StringComparison.Ordinal)); - using (new CultureReplacer("en-US", "en-US")) + using(new CultureReplacer("en-US", "en-US")) { Assert.Equal("Loc_Two_Name en-US", groupTwo.Key.Name); } - using (new CultureReplacer("fr-FR", "fr-FR")) + using(new CultureReplacer("fr-FR", "fr-FR")) { Assert.Equal("Loc_Two_Name fr-FR", groupTwo.Key.Name); } @@ -1076,12 +1166,12 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal // Assert var groupThree = Assert.Single(enumNameAndGroup, e => e.Value.Equals("3", StringComparison.Ordinal)); - using (new CultureReplacer("en-US", "en-US")) + using(new CultureReplacer("en-US", "en-US")) { Assert.Equal("type three name en-US", groupThree.Key.Name); } - using (new CultureReplacer("fr-FR", "fr-FR")) + using(new CultureReplacer("fr-FR", "fr-FR")) { Assert.Equal("type three name fr-FR", groupThree.Key.Name); } @@ -1096,12 +1186,12 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var groupThree = Assert.Single(enumNameAndGroup, e => e.Value.Equals("3", StringComparison.Ordinal)); // Assert - using (new CultureReplacer("en-US", "en-US")) + using(new CultureReplacer("en-US", "en-US")) { Assert.Equal("type three name en-US", groupThree.Key.Name); } - using (new CultureReplacer("fr-FR", "fr-FR")) + using(new CultureReplacer("fr-FR", "fr-FR")) { Assert.Equal("type three name fr-FR", groupThree.Key.Name); } @@ -1269,8 +1359,11 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal .Setup(factory => factory.Create(typeof(EnumWithLocalizedDisplayNames))) .Returns(stringLocalizer.Object); + var options = Options.Create(new MvcDataAnnotationsLocalizationOptions()); + options.Value.DataAnnotationLocalizerProvider = (modelType, localizerFactory) => localizerFactory.Create(modelType); + return new DataAnnotationsMetadataProvider( - Options.Create(new MvcDataAnnotationsLocalizationOptions()), + options, useStringLocalizer ? stringLocalizerFactory.Object : null); } @@ -1304,7 +1397,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public bool Equals(KeyValuePair x, KeyValuePair y) { - using (new CultureReplacer(string.Empty, string.Empty)) + 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); @@ -1313,7 +1406,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public int GetHashCode(KeyValuePair obj) { - using (new CultureReplacer(string.Empty, string.Empty)) + using(new CultureReplacer(string.Empty, string.Empty)) { return obj.Key.GetHashCode(); } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/DataAnnotationTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/DataAnnotationTests.cs new file mode 100644 index 0000000000..a67fb5dd83 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/DataAnnotationTests.cs @@ -0,0 +1,38 @@ +using System.Net; +using System.Threading.Tasks; +using RazorWebSite; +using Microsoft.AspNetCore.Hosting; +using Xunit; +using System.Linq; +using System.Net.Http; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class DataAnnotationTests : IClassFixture> + { + private HttpClient Client { get; set; } + + public DataAnnotationTests(MvcTestFixture fixture) + { + var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(builder => + { + builder.UseStartup(); + }); + Client = factory.CreateDefaultClient(); + } + + private const string EnumUrl = "http://localhost/Enum/Enum"; + + [Fact] + public async Task DataAnnotationLocalizionOfEnums_FromDataAnnotationLocalizerProvider() + { + // Arrange & Act + var response = await Client.GetAsync(EnumUrl); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("FirstOptionDisplay from singletype", content); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Localization.Test/MvcLocalizationMvcBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Localization.Test/MvcLocalizationMvcBuilderExtensionsTest.cs index 1b0b4e947f..8b6fe4bebe 100644 --- a/test/Microsoft.AspNetCore.Mvc.Localization.Test/MvcLocalizationMvcBuilderExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Localization.Test/MvcLocalizationMvcBuilderExtensionsTest.cs @@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Xunit; @@ -104,6 +106,9 @@ namespace Microsoft.AspNetCore.Mvc.Localization.Test { // Arrange var builder = new TestMvcBuilder(); + + builder.Services.AddSingleton(NullLoggerFactory.Instance); + var dataAnnotationLocalizerProvider = new Func((type, factory) => { return null; diff --git a/test/Microsoft.AspNetCore.Mvc.Localization.Test/MvcLocalizationMvcCoreBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Localization.Test/MvcLocalizationMvcCoreBuilderExtensionsTest.cs index 1412d35c94..3956d79f17 100644 --- a/test/Microsoft.AspNetCore.Mvc.Localization.Test/MvcLocalizationMvcCoreBuilderExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Localization.Test/MvcLocalizationMvcCoreBuilderExtensionsTest.cs @@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Xunit; @@ -104,6 +106,9 @@ namespace Microsoft.AspNetCore.Mvc.Localization.Test { // Arrange var builder = new TestMvcCoreBuilder(); + + builder.Services.AddSingleton(NullLoggerFactory.Instance); + var dataAnnotationLocalizerProvider = new Func((type, factory) => { return null; diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs index 30f7f4d0da..a8b6659a91 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs @@ -659,6 +659,7 @@ Environment.NewLine; Mock.Of(), viewEngine.Object, provider, + localizerFactory: null, innerHelper => new StubbyHtmlHelper(innerHelper)); helper.ViewData["Property1"] = "True"; @@ -701,6 +702,7 @@ Environment.NewLine; Mock.Of(), viewEngine.Object, provider, + localizerFactory: null, innerHelper => new StubbyHtmlHelper(innerHelper)); // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates. @@ -742,6 +744,7 @@ Environment.NewLine; Mock.Of(), viewEngine.Object, provider, + localizerFactory: null, innerHelper => new StubbyHtmlHelper(innerHelper)); helper.ViewData["Property1"] = "True"; @@ -784,6 +787,7 @@ Environment.NewLine; Mock.Of(), viewEngine.Object, provider, + localizerFactory: null, innerHelper => new StubbyHtmlHelper(innerHelper)); // TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates. diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs index 75649f3223..24377c95eb 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs @@ -80,6 +80,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering CreateUrlHelper(), CreateViewEngine(), metadataProvider, + localizerFactory: null, innerHelperWrapper: null, htmlGenerator: htmlGenerator, idAttributeDotReplacement: null); @@ -92,6 +93,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering CreateUrlHelper(), CreateViewEngine(), TestModelMetadataProvider.CreateDefaultProvider(), + localizerFactory: null, innerHelperWrapper: null, htmlGenerator: null, idAttributeDotReplacement: null); @@ -106,6 +108,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering CreateUrlHelper(), CreateViewEngine(), TestModelMetadataProvider.CreateDefaultProvider(), + localizerFactory: null, innerHelperWrapper: null, htmlGenerator: null, idAttributeDotReplacement: idAttributeDotReplacement); @@ -127,6 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering CreateUrlHelper(), CreateViewEngine(), provider, + localizerFactory: null, innerHelperWrapper: null, htmlGenerator: null, idAttributeDotReplacement: idAttributeDotReplacement); @@ -167,7 +171,8 @@ namespace Microsoft.AspNetCore.Mvc.Rendering model, CreateUrlHelper(), viewEngine, - TestModelMetadataProvider.CreateDefaultProvider(stringLocalizerFactory)); + TestModelMetadataProvider.CreateDefaultProvider(stringLocalizerFactory), + stringLocalizerFactory); } public static HtmlHelper GetHtmlHelper( @@ -180,6 +185,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering CreateUrlHelper(), viewEngine, TestModelMetadataProvider.CreateDefaultProvider(), + localizerFactory: null, innerHelperWrapper); } @@ -187,9 +193,10 @@ namespace Microsoft.AspNetCore.Mvc.Rendering TModel model, IUrlHelper urlHelper, ICompositeViewEngine viewEngine, - IModelMetadataProvider provider) + IModelMetadataProvider provider, + IStringLocalizerFactory localizerFactory = null) { - return GetHtmlHelper(model, urlHelper, viewEngine, provider, innerHelperWrapper: null); + return GetHtmlHelper(model, urlHelper, viewEngine, provider, localizerFactory, innerHelperWrapper: null); } public static HtmlHelper GetHtmlHelper( @@ -197,6 +204,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering IUrlHelper urlHelper, ICompositeViewEngine viewEngine, IModelMetadataProvider provider, + IStringLocalizerFactory localizerFactory, Func innerHelperWrapper) { var viewData = new ViewDataDictionary(provider); @@ -207,6 +215,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering urlHelper, viewEngine, provider, + localizerFactory, innerHelperWrapper, htmlGenerator: null, idAttributeDotReplacement: null); @@ -217,6 +226,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering IUrlHelper urlHelper, ICompositeViewEngine viewEngine, IModelMetadataProvider provider, + IStringLocalizerFactory localizerFactory, Func innerHelperWrapper, IHtmlGenerator htmlGenerator, string idAttributeDotReplacement) @@ -229,14 +239,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering { options.HtmlHelperOptions.IdAttributeDotReplacement = idAttributeDotReplacement; } - var localizationOptionsAccesor = new Mock>(); - localizationOptionsAccesor.SetupGet(o => o.Value).Returns(new MvcDataAnnotationsLocalizationOptions()); + var localizationOptions = new MvcDataAnnotationsLocalizationOptions(); + var localizationOptionsAccesor = Options.Create(localizationOptions); options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider( new ValidationAttributeAdapterProvider(), - localizationOptionsAccesor.Object, - stringLocalizerFactory: null)); + localizationOptionsAccesor, + localizerFactory)); var urlHelperFactory = new Mock(); urlHelperFactory diff --git a/test/WebSites/RazorWebSite/Controllers/DataAnnotationController.cs b/test/WebSites/RazorWebSite/Controllers/DataAnnotationController.cs new file mode 100644 index 0000000000..7d4bdb2c4a --- /dev/null +++ b/test/WebSites/RazorWebSite/Controllers/DataAnnotationController.cs @@ -0,0 +1,16 @@ +// 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 RazorWebSite.Models; +using Microsoft.AspNetCore.Mvc; + +namespace RazorWebSite.Controllers +{ + public class EnumController : Controller + { + public IActionResult Enum() + { + return View(new EnumModel{ Id = ModelEnum.FirstOption }); + } + } +} diff --git a/test/WebSites/RazorWebSite/Models/EnumModel.cs b/test/WebSites/RazorWebSite/Models/EnumModel.cs new file mode 100644 index 0000000000..3a993cc65a --- /dev/null +++ b/test/WebSites/RazorWebSite/Models/EnumModel.cs @@ -0,0 +1,20 @@ +// 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.ComponentModel.DataAnnotations; + +namespace RazorWebSite.Models +{ + public enum ModelEnum + { + [Display(Name = "FirstOptionDisplay")] + FirstOption, + SecondOptions + } + + public class EnumModel + { + [Display(Name = "ModelEnum")] + public ModelEnum Id { get; set; } + } +} diff --git a/test/WebSites/RazorWebSite/Program.cs b/test/WebSites/RazorWebSite/Program.cs new file mode 100644 index 0000000000..6e8622bb7b --- /dev/null +++ b/test/WebSites/RazorWebSite/Program.cs @@ -0,0 +1,26 @@ +// 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.IO; +using Microsoft.AspNetCore.Hosting; + +namespace RazorWebSite +{ + public class Program + { + public static void Main(string[] args) + { + var host = CreateWebHostBuilder(args) + .Build(); + + host.Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + new WebHostBuilder() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup() + .UseKestrel() + .UseIISIntegration(); + } +} diff --git a/test/WebSites/RazorWebSite/Resources/Models/ModelEnum.resx b/test/WebSites/RazorWebSite/Resources/Models/ModelEnum.resx new file mode 100644 index 0000000000..af20175466 --- /dev/null +++ b/test/WebSites/RazorWebSite/Resources/Models/ModelEnum.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + first option from enum resx + + \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Resources/SingleType.resx b/test/WebSites/RazorWebSite/Resources/SingleType.resx new file mode 100644 index 0000000000..8fa8c677c9 --- /dev/null +++ b/test/WebSites/RazorWebSite/Resources/SingleType.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + FirstOptionDisplay from singletype + + \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/SingleType.cs b/test/WebSites/RazorWebSite/SingleType.cs new file mode 100644 index 0000000000..ab8ec1e9c6 --- /dev/null +++ b/test/WebSites/RazorWebSite/SingleType.cs @@ -0,0 +1,10 @@ +// 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. + +namespace RazorWebSite +{ + public class SingleType + { + + } +} diff --git a/test/WebSites/RazorWebSite/Startup.cs b/test/WebSites/RazorWebSite/Startup.cs index 17fee9d6d7..d0a4244558 100644 --- a/test/WebSites/RazorWebSite/Startup.cs +++ b/test/WebSites/RazorWebSite/Startup.cs @@ -3,10 +3,8 @@ using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Reflection; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Razor.TagHelpers; @@ -72,21 +70,5 @@ namespace RazorWebSite app.UseMvcWithDefaultRoute(); } - - public static void Main(string[] args) - { - var host = CreateWebHostBuilder(args) - .Build(); - - host.Run(); - } - - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - new WebHostBuilder() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseStartup() - .UseKestrel() - .UseIISIntegration(); } } - diff --git a/test/WebSites/RazorWebSite/StartupDataAnnotations.cs b/test/WebSites/RazorWebSite/StartupDataAnnotations.cs new file mode 100644 index 0000000000..7ec35e86fa --- /dev/null +++ b/test/WebSites/RazorWebSite/StartupDataAnnotations.cs @@ -0,0 +1,54 @@ +// 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.Collections.Generic; +using System.Globalization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace RazorWebSite +{ + public class StartupDataAnnotations + { + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddLocalization(options => options.ResourcesPath = "Resources"); + + services + .AddMvc() + .AddViewLocalization() + .AddDataAnnotationsLocalization((options) => + { + options.DataAnnotationLocalizerProvider = + (modelType, stringLocalizerFactory) => stringLocalizerFactory.Create(typeof(SingleType)); + }); + services.Configure(options => options.CompatibilityVersion = CompatibilityVersion.Latest); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US", "en-US"), + SupportedCultures = new List + { + new CultureInfo("en-US") + }, + SupportedUICultures = new List + { + new CultureInfo("en-US") + } + }); + app.UseStaticFiles(); + + // Add MVC to the request pipeline + app.UseMvcWithDefaultRoute(); + } + } +} diff --git a/test/WebSites/RazorWebSite/Views/Enum/Enum.cshtml b/test/WebSites/RazorWebSite/Views/Enum/Enum.cshtml new file mode 100644 index 0000000000..684d00ffa1 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/Enum/Enum.cshtml @@ -0,0 +1,6 @@ +@model RazorWebSite.Models.EnumModel + +
+ @Html.DisplayNameFor(model => model.Id) + @Html.DisplayFor(model => model.Id) +