DataAnnotations of Enum values use DataAnnotationLocalizerProvider

This commit is contained in:
Ryan Brandenburg 2018-05-21 12:09:56 -07:00
parent 418aac57f4
commit edf4e8fd9e
23 changed files with 697 additions and 58 deletions

View File

@ -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.

View File

@ -59,6 +59,18 @@ namespace Microsoft.AspNetCore.Mvc
/// </remarks>
Version_2_1,
/// <summary>
/// Sets the default value of settings on <see cref="MvcOptions"/> to match the behavior of
/// ASP.NET Core MVC 2.2.
/// </summary>
/// <remarks>
/// ASP.NET Core MVC 2.2 introduces compatibility switches for the following:
/// <list type="bullet">
/// <item><description><c>MvcDataAnnotationsLocalizationOptions.AllowDataAnnotationsLocalizationForEnumDisplayAttributes</c></description></item>
/// </list>
/// </remarks>
Version_2_2,
/// <summary>
/// Sets the default value of settings on <see cref="MvcOptions"/> to match the latest release. Use this
/// value with care, upgrading minor versions will cause breaking changes when using <see cref="Latest"/>.

View File

@ -27,6 +27,9 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
<IConfigureOptions<MvcDataAnnotationsLocalizationOptions>,
MvcDataAnnotationsLocalizationOptionsSetup>());
}
services.TryAddEnumerable(
ServiceDescriptor.Transient<IPostConfigureOptions<MvcDataAnnotationsLocalizationOptions>, MvcDataAnnotationsLocalizationConfigureCompatibilityOptions>());
}
}
}

View File

@ -182,7 +182,19 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var groupedDisplayNamesAndValues = new List<KeyValuePair<EnumGroupAndName, string>>();
var namesAndValues = new Dictionary<string, string>();
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))

View File

@ -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<MvcDataAnnotationsLocalizationOptions>
{
public MvcDataAnnotationsLocalizationConfigureCompatibilityOptions(
ILoggerFactory loggerFactory,
IOptions<MvcCompatibilityOptions> compatibilityOptions)
: base(loggerFactory, compatibilityOptions)
{
}
protected override IReadOnlyDictionary<string, object> DefaultValues
{
get
{
var values = new Dictionary<string, object>();
if (Version >= CompatibilityVersion.Version_2_2)
{
values[nameof(MvcDataAnnotationsLocalizationOptions.AllowDataAnnotationsLocalizationForEnumDisplayAttributes)] = true;
}
return values;
}
}
}
}

View File

@ -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
/// <summary>
/// Provides programmatic configuration for DataAnnotations localization in the MVC framework.
/// </summary>
public class MvcDataAnnotationsLocalizationOptions
public class MvcDataAnnotationsLocalizationOptions : IEnumerable<ICompatibilitySwitch>
{
private readonly CompatibilitySwitch<bool> _allowDataAnnotationsLocalizationForEnumDisplayAttributes;
private readonly ICompatibilitySwitch[] _switches;
/// <summary>
/// The delegate to invoke for creating <see cref="IStringLocalizer"/>.
/// </summary>
public Func<Type, IStringLocalizerFactory, IStringLocalizer> DataAnnotationLocalizerProvider;
/// <summary>
/// Instantiates a new instance of the <see cref="MvcDataAnnotationsLocalizationOptions"/> class.
/// </summary>
public MvcDataAnnotationsLocalizationOptions()
{
_allowDataAnnotationsLocalizationForEnumDisplayAttributes = new CompatibilitySwitch<bool>(nameof(AllowDataAnnotationsLocalizationForEnumDisplayAttributes));
_switches = new ICompatibilitySwitch[]
{
_allowDataAnnotationsLocalizationForEnumDisplayAttributes
};
}
/// <summary>
/// Gets or sets a value that determines if <see cref="MvcDataAnnotationsLocalizationOptions.DataAnnotationLocalizerProvider"/> should be used while localizing <see cref="Enum"/> types.
/// If set to <c>true</c> <see cref="MvcDataAnnotationsLocalizationOptions.DataAnnotationLocalizerProvider"/> will be used in localizing <see cref="Enum"/> types.
/// If set to <c>false</c> the localization will search for values in resource files for the <see cref="Enum"/>.
/// </summary>
/// <remarks>
/// <para>
/// This property is associated with a compatibility switch and can provide a different behavior depending on
/// the configured compatibility version for the application. See <see cref="CompatibilityVersion"/> for guidance and examples of setting the application's compatibility version.
/// </para>
/// <para>
/// 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 <see cref="CompatibilityVersion"/>.
/// </para>
/// <para>
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_0"/> or <see cref="CompatibilityVersion.Version_2_1"/> then
/// this setting will have the value <c>false</c> unless explicitly configured.
/// </para>
/// <para>
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_2"/> or
/// higher then this setting will have the value <c>true</c> unless explicitly configured.
/// </para>
/// </remarks>
public bool AllowDataAnnotationsLocalizationForEnumDisplayAttributes
{
get => _allowDataAnnotationsLocalizationForEnumDisplayAttributes.Value;
set => _allowDataAnnotationsLocalizationForEnumDisplayAttributes.Value = value;
}
public IEnumerator<ICompatibilitySwitch> GetEnumerator() => ((IEnumerable<ICompatibilitySwitch>)_switches).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator();
}
}

View File

@ -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(),
};

View File

@ -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
{
}
}
}
}

View File

@ -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<IStringLocalizer>(MockBehavior.Strict);
unsharedLocalizer
.Setup(s => s["DisplayNameValue"])
.Returns(new LocalizedString("DisplaynameValue", "didn't use shared"));
var sharedLocalizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
sharedLocalizer
.Setup(s => s["DisplayNameValue"])
.Returns(() => new LocalizedString("DisplayNameValue", "used shared"));
var stringLocalizerFactoryMock = new Mock<IStringLocalizerFactory>(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<IStringLocalizer>(MockBehavior.Strict);
sharedLocalizer
.Setup(s => s["DisplayNameValue"])
.Returns(new LocalizedString("DisplayNameValue", "Name from DisplayNameAttribute"));
var stringLocalizerFactoryMock = new Mock<IStringLocalizerFactory>(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<Type>()))
.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<string>()])
.Returns<string>((index) => new LocalizedString(index, index + " value"));
var stringLocalizerFactory = new Mock<IStringLocalizerFactory>(MockBehavior.Strict);
stringLocalizerFactory
var stringLocalizerFactoryMock = new Mock<IStringLocalizerFactory>(MockBehavior.Strict);
stringLocalizerFactoryMock
.Setup(f => f.Create(It.IsAny<Type>()))
.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<EnumGroupAndName, string> x, KeyValuePair<EnumGroupAndName, string> 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<EnumGroupAndName, string> obj)
{
using (new CultureReplacer(string.Empty, string.Empty))
using(new CultureReplacer(string.Empty, string.Empty))
{
return obj.Key.GetHashCode();
}

View File

@ -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<MvcTestFixture<StartupDataAnnotations>>
{
private HttpClient Client { get; set; }
public DataAnnotationTests(MvcTestFixture<StartupDataAnnotations> fixture)
{
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(builder =>
{
builder.UseStartup<StartupDataAnnotations>();
});
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);
}
}
}

View File

@ -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<ILoggerFactory>(NullLoggerFactory.Instance);
var dataAnnotationLocalizerProvider = new Func<Type, IStringLocalizerFactory, IStringLocalizer>((type, factory) =>
{
return null;

View File

@ -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<ILoggerFactory>(NullLoggerFactory.Instance);
var dataAnnotationLocalizerProvider = new Func<Type, IStringLocalizerFactory, IStringLocalizer>((type, factory) =>
{
return null;

View File

@ -659,6 +659,7 @@ Environment.NewLine;
Mock.Of<IUrlHelper>(),
viewEngine.Object,
provider,
localizerFactory: null,
innerHelper => new StubbyHtmlHelper(innerHelper));
helper.ViewData["Property1"] = "True";
@ -701,6 +702,7 @@ Environment.NewLine;
Mock.Of<IUrlHelper>(),
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<IUrlHelper>(),
viewEngine.Object,
provider,
localizerFactory: null,
innerHelper => new StubbyHtmlHelper(innerHelper));
helper.ViewData["Property1"] = "True";
@ -784,6 +787,7 @@ Environment.NewLine;
Mock.Of<IUrlHelper>(),
viewEngine.Object,
provider,
localizerFactory: null,
innerHelper => new StubbyHtmlHelper(innerHelper));
// TemplateBuilder sets FormattedModelValue before calling TemplateRenderer and it's used in most templates.

View File

@ -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<TModel> GetHtmlHelper<TModel>(
@ -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<TModel> GetHtmlHelper<TModel>(
@ -197,6 +204,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
IUrlHelper urlHelper,
ICompositeViewEngine viewEngine,
IModelMetadataProvider provider,
IStringLocalizerFactory localizerFactory,
Func<IHtmlHelper, IHtmlHelper> innerHelperWrapper)
{
var viewData = new ViewDataDictionary<TModel>(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<IHtmlHelper, IHtmlHelper> innerHelperWrapper,
IHtmlGenerator htmlGenerator,
string idAttributeDotReplacement)
@ -229,14 +239,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
{
options.HtmlHelperOptions.IdAttributeDotReplacement = idAttributeDotReplacement;
}
var localizationOptionsAccesor = new Mock<IOptions<MvcDataAnnotationsLocalizationOptions>>();
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<IUrlHelperFactory>();
urlHelperFactory

View File

@ -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 });
}
}
}

View File

@ -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; }
}
}

View File

@ -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<Startup>()
.UseKestrel()
.UseIISIntegration();
}
}

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="FirstOptionDisplay" xml:space="preserve">
<value>first option from enum resx</value>
</data>
</root>

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="FirstOptionDisplay" xml:space="preserve">
<value>FirstOptionDisplay from singletype</value>
</data>
</root>

View File

@ -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
{
}
}

View File

@ -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<Startup>()
.UseKestrel()
.UseIISIntegration();
}
}

View File

@ -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<MvcCompatibilityOptions>(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<CultureInfo>
{
new CultureInfo("en-US")
},
SupportedUICultures = new List<CultureInfo>
{
new CultureInfo("en-US")
}
});
app.UseStaticFiles();
// Add MVC to the request pipeline
app.UseMvcWithDefaultRoute();
}
}
}

View File

@ -0,0 +1,6 @@
@model RazorWebSite.Models.EnumModel
<div>
@Html.DisplayNameFor(model => model.Id)
@Html.DisplayFor(model => model.Id)
</div>