Localize DataAnnotations (#5129)

This commit is contained in:
Ryan Brandenburg 2016-08-16 11:52:15 -07:00 committed by GitHub
parent 8de4ddc010
commit 889af50318
9 changed files with 270 additions and 28 deletions

View File

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25123.0
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Localization;
namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{
@ -20,6 +21,13 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
IDisplayMetadataProvider,
IValidationMetadataProvider
{
private readonly IStringLocalizerFactory _stringLocalizerFactory;
public DataAnnotationsMetadataProvider(IStringLocalizerFactory stringLocalizerFactory)
{
_stringLocalizerFactory = stringLocalizerFactory;
}
/// <inheritdoc />
public void CreateBindingMetadata(BindingMetadataProviderContext context)
{
@ -80,11 +88,23 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
displayMetadata.DataTypeName = DataType.Html.ToString();
}
var containerType = context.Key.ContainerType ?? context.Key.ModelType;
var localizer = _stringLocalizerFactory?.Create(containerType);
// Description
if (displayAttribute != null)
{
displayMetadata.Description = () => displayAttribute.GetDescription();
displayMetadata.Placeholder = () => displayAttribute.GetPrompt();
if (localizer != null &&
!string.IsNullOrEmpty(displayAttribute.Description) &&
displayAttribute.ResourceType == null)
{
displayMetadata.Description = () => localizer[displayAttribute.Description];
}
else
{
displayMetadata.Description = () => displayAttribute.GetDescription();
}
}
// DisplayFormatString
@ -96,7 +116,16 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
// DisplayName
if (displayAttribute != null)
{
displayMetadata.DisplayName = () => displayAttribute.GetName();
if (localizer != null &&
!string.IsNullOrEmpty(displayAttribute.Name) &&
displayAttribute.ResourceType == null)
{
displayMetadata.DisplayName = () => localizer[displayAttribute.Name];
}
else
{
displayMetadata.DisplayName = () => displayAttribute.GetName();
}
}
// EditFormatString
@ -189,6 +218,21 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
displayMetadata.Order = displayAttribute.GetOrder().Value;
}
// Placeholder
if (displayAttribute != null)
{
if (localizer != null &&
!string.IsNullOrEmpty(displayAttribute.Prompt) &&
displayAttribute.ResourceType == null)
{
displayMetadata.Placeholder = () => localizer[displayAttribute.Prompt];
}
else
{
displayMetadata.Placeholder = () => displayAttribute.GetPrompt();
}
}
// ShowForDisplay
if (scaffoldColumnAttribute != null)
{

View File

@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var stringLocalizerFactory = serviceProvider.GetService<IStringLocalizerFactory>();
var validationAttributeAdapterProvider = serviceProvider.GetRequiredService<IValidationAttributeAdapterProvider>();
options.ModelMetadataDetailsProviders.Add(new DataAnnotationsMetadataProvider());
options.ModelMetadataDetailsProviders.Add(new DataAnnotationsMetadataProvider(stringLocalizerFactory));
options.ModelValidatorProviders.Add(new DataAnnotationsModelValidatorProvider(
validationAttributeAdapterProvider,

View File

@ -8,6 +8,8 @@ using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
@ -58,7 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
object expected)
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var key = ModelMetadataIdentity.ForType(typeof(string));
var context = new DisplayMetadataProviderContext(key, new ModelAttributes(new object[] { attribute }));
@ -75,7 +77,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
public void CreateDisplayMetadata_FindsDisplayFormat_FromDataType()
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var dataType = new DataTypeAttribute(DataType.Currency);
var displayFormat = dataType.DisplayFormat; // Non-null for DataType.Currency.
@ -95,7 +97,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
public void CreateDisplayMetadata_FindsDisplayFormat_OverridingDataType()
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(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
@ -118,7 +120,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
public void CreateBindingMetadata_EditableAttributeFalse_SetsReadOnlyTrue()
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var editable = new EditableAttribute(allowEdit: false);
@ -137,7 +139,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
public void CreateBindingMetadata_EditableAttributeTrue_SetsReadOnlyFalse()
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var editable = new EditableAttribute(allowEdit: true);
@ -152,13 +154,47 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
Assert.False(context.BindingMetadata.IsReadOnly);
}
// 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 display = new DisplayAttribute()
{
#if USE_REAL_RESOURCES
Name = nameof(Test.Resources.DisplayAttribute_Name),
ResourceType = typeof(Test.Resources),
#else
Name = nameof(DataAnnotations.Test.Resources.DisplayAttribute_Name),
ResourceType = typeof(TestResources),
#endif
};
var attributes = new Attribute[] { display };
var key = ModelMetadataIdentity.ForType(typeof(string));
var context = new DisplayMetadataProviderContext(key, new ModelAttributes(attributes));
// Act
provider.CreateDisplayMetadata(context);
// Assert
Assert.Equal("name from resources", context.DisplayMetadata.DisplayName());
}
// This is IMPORTANT. Product code needs to use GetName() instead of .Name. It's easy to regress.
[Fact]
public void CreateDisplayMetadata_DisplayAttribute_NameFromResources()
public void CreateDisplayMetadata_DisplayAttribute_NameFromResources_WithLocalizer()
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
// Nothing on stringLocalizer should be called
var stringLocalizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
var stringLocalizerFactory = new Mock<IStringLocalizerFactory>();
stringLocalizerFactory
.Setup(s => s.Create(It.IsAny<Type>()))
.Returns(() => stringLocalizer.Object);
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory.Object);
var display = new DisplayAttribute()
{
@ -184,10 +220,16 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
// This is IMPORTANT. Product code needs to use GetDescription() instead of .Description. It's easy to regress.
[Fact]
public void CreateDisplayMetadata_DisplayAttribute_DescriptionFromResources()
public void CreateDisplayMetadata_DisplayAttribute_DescriptionFromResources_WithLocalizer()
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
// Nothing on stringLocalizer should be called
var stringLocalizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
var stringLocalizerFactory = new Mock<IStringLocalizerFactory>();
stringLocalizerFactory
.Setup(s => s.Create(It.IsAny<Type>()))
.Returns(() => stringLocalizer.Object);
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory.Object);
var display = new DisplayAttribute()
{
@ -211,6 +253,141 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
Assert.Equal("description from resources", context.DisplayMetadata.Description());
}
// This is IMPORTANT. Product code needs to use GetDescription() instead of .Description. It's easy to regress.
[Fact]
public void CreateDisplayMetadata_DisplayAttribute_DescriptionFromResources_NullLocalizer()
{
// Arrange
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var display = new DisplayAttribute()
{
#if USE_REAL_RESOURCES
Description = nameof(Test.Resources.DisplayAttribute_Description),
ResourceType = typeof(Test.Resources),
#else
Description = nameof(DataAnnotations.Test.Resources.DisplayAttribute_Description),
ResourceType = typeof(TestResources),
#endif
};
var attributes = new Attribute[] { display };
var key = ModelMetadataIdentity.ForType(typeof(string));
var context = new DisplayMetadataProviderContext(key, new ModelAttributes(attributes));
// Act
provider.CreateDisplayMetadata(context);
// Assert
Assert.Equal("description from resources", context.DisplayMetadata.Description());
}
// This is IMPORTANT. Product code needs to use GetPrompt() instead of .Prompt. It's easy to regress.
[Fact]
public void CreateDisplayMetadata_DisplayAttribute_PromptFromResources_WithLocalizer()
{
// Arrange
// Nothing on stringLocalizer should be called
var stringLocalizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
var stringLocalizerFactory = new Mock<IStringLocalizerFactory>();
stringLocalizerFactory
.Setup(s => s.Create(It.IsAny<Type>()))
.Returns(() => stringLocalizer.Object);
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory.Object);
var display = new DisplayAttribute()
{
#if USE_REAL_RESOURCES
Prompt = nameof(Test.Resources.DisplayAttribute_Prompt),
ResourceType = typeof(Test.Resources),
#else
Prompt = nameof(DataAnnotations.Test.Resources.DisplayAttribute_Prompt),
ResourceType = typeof(TestResources),
#endif
};
var attributes = new Attribute[] { display };
var key = ModelMetadataIdentity.ForType(typeof(string));
var context = new DisplayMetadataProviderContext(key, new ModelAttributes(attributes));
// Act
provider.CreateDisplayMetadata(context);
// Assert
Assert.Equal("prompt from resources", context.DisplayMetadata.Placeholder());
}
// This is IMPORTANT. Product code needs to use GetPrompt() instead of .Prompt. It's easy to regress.
[Fact]
public void CreateDisplayMetadata_DisplayAttribute_PromptFromResources_NullLocalizer()
{
// Arrange
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var display = new DisplayAttribute()
{
#if USE_REAL_RESOURCES
Prompt = nameof(Test.Resources.DisplayAttribute_Prompt),
ResourceType = typeof(Test.Resources),
#else
Prompt = nameof(DataAnnotations.Test.Resources.DisplayAttribute_Prompt),
ResourceType = typeof(TestResources),
#endif
};
var attributes = new Attribute[] { display };
var key = ModelMetadataIdentity.ForType(typeof(string));
var context = new DisplayMetadataProviderContext(key, new ModelAttributes(attributes));
// Act
provider.CreateDisplayMetadata(context);
// Assert
Assert.Equal("prompt from resources", context.DisplayMetadata.Placeholder());
}
[Fact]
public void CreateDisplayMetadata_DisplayAttribute_LocalizeProperties()
{
// Arrange
var stringLocalizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
stringLocalizer
.Setup(s => s["Model_Name"])
.Returns(new LocalizedString("Model_Name", "name from localizer"));
stringLocalizer
.Setup(s => s["Model_Description"])
.Returns(new LocalizedString("Model_Description", "description from localizer"));
stringLocalizer
.Setup(s => s["Model_Prompt"])
.Returns(new LocalizedString("Model_Prompt", "prompt from localizer"));
var stringLocalizerFactory = new Mock<IStringLocalizerFactory>(MockBehavior.Strict);
stringLocalizerFactory
.Setup(f => f.Create(It.IsAny<Type>()))
.Returns(stringLocalizer.Object);
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory.Object);
var display = new DisplayAttribute()
{
Name = "Model_Name",
Description = "Model_Description",
Prompt = "Model_Prompt"
};
var attributes = new Attribute[] { display };
var key = ModelMetadataIdentity.ForType(typeof(DataAnnotationsMetadataProviderTest));
var context = new DisplayMetadataProviderContext(key, new ModelAttributes(attributes));
// Act
provider.CreateDisplayMetadata(context);
// Assert
Assert.Equal("name from localizer", context.DisplayMetadata.DisplayName());
Assert.Equal("description from localizer", context.DisplayMetadata.Description());
Assert.Equal("prompt from localizer", context.DisplayMetadata.Placeholder());
}
[Theory]
[InlineData(typeof(EmptyClass), false)]
[InlineData(typeof(ClassWithFields), false)]
@ -232,7 +409,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
public void CreateDisplayMetadata_IsEnum_ReflectsModelType(Type type, bool expectedIsEnum)
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var key = ModelMetadataIdentity.ForType(type);
var attributes = new object[0];
@ -266,7 +443,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
public void CreateDisplayMetadata_IsFlagsEnum_ReflectsModelType(Type type, bool expectedIsFlagsEnum)
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var key = ModelMetadataIdentity.ForType(type);
var attributes = new object[0];
@ -398,7 +575,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
IReadOnlyDictionary<string, string> expectedDictionary)
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var key = ModelMetadataIdentity.ForType(type);
var attributes = new object[0];
@ -532,7 +709,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
IEnumerable<KeyValuePair<EnumGroupAndName, string>> expectedKeyValuePairs)
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var key = ModelMetadataIdentity.ForType(type);
var attributes = new object[0];
@ -554,7 +731,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
public void CreateValidationMetadata_RequiredAttribute_SetsIsRequiredToTrue()
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var required = new RequiredAttribute();
@ -576,7 +753,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
public void CreateValidationMetadata_NoRequiredAttribute_IsRequiredLeftAlone(bool? initialValue)
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var attributes = new Attribute[] { };
var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string));
@ -597,7 +774,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
public void CreateBindingMetadata_RequiredAttribute_IsBindingRequiredLeftAlone(bool initialValue)
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var attributes = new Attribute[] { new RequiredAttribute() };
var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string));
@ -618,7 +795,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
public void CreateBindingDetails_NoEditableAttribute_IsReadOnlyLeftAlone(bool? initialValue)
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var attributes = new Attribute[] { };
var key = ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string));
@ -636,7 +813,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
public void CreateValidationDetails_ValidatableObject_ReturnsObject()
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var attribute = new TestValidationAttribute();
var attributes = new Attribute[] { attribute };
@ -655,7 +832,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
public void CreateValidationDetails_ValidatableObject_AlreadyInContext_Ignores()
{
// Arrange
var provider = new DataAnnotationsMetadataProvider();
var provider = new DataAnnotationsMetadataProvider(stringLocalizerFactory: null);
var attribute = new TestValidationAttribute();
var attributes = new Attribute[] { attribute };

View File

@ -1050,7 +1050,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[]
{
new DefaultBindingMetadataProvider(),
new DataAnnotationsMetadataProvider(),
new DataAnnotationsMetadataProvider(stringLocalizerFactory: null),
}),
new TestOptionsManager<MvcOptions>())
{

View File

@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public static string DisplayAttribute_Name { get; } = Resources.DisplayAttribute_Name;
public static string DisplayAttribute_Prompt { get; } = Resources.DisplayAttribute_Prompt;
public static string DisplayAttribute_CultureSensitiveName =>
Resources.DisplayAttribute_Name + CultureInfo.CurrentUICulture;

View File

@ -58,6 +58,22 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Test
return GetString("DisplayAttribute_Name");
}
/// <summary>
/// prompt from resources
/// </summary>
internal static string DisplayAttribute_Prompt
{
get { return GetString("DisplayAttribute_Prompt"); }
}
/// <summary>
/// prompt from resources
/// </summary>
internal static string FormatDisplayAttribute_Prompt()
{
return GetString("DisplayAttribute_Prompt");
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -126,4 +126,7 @@
<data name="DisplayAttribute_Name" xml:space="preserve">
<value>name from resources</value>
</data>
<data name="DisplayAttribute_Prompt" xml:space="preserve">
<value>prompt from resources</value>
</data>
</root>

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
new DefaultBindingMetadataProvider(),
new DefaultValidationMetadataProvider(),
new DataAnnotationsMetadataProvider(),
new DataAnnotationsMetadataProvider(stringLocalizerFactory: null),
new DataMemberRequiredBindingMetadataProvider(),
};
@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
new DefaultBindingMetadataProvider(),
new DefaultValidationMetadataProvider(),
new DataAnnotationsMetadataProvider(),
new DataAnnotationsMetadataProvider(stringLocalizerFactory: null),
new DataMemberRequiredBindingMetadataProvider(),
};
@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
new DefaultBindingMetadataProvider(),
new DefaultValidationMetadataProvider(),
new DataAnnotationsMetadataProvider(),
new DataAnnotationsMetadataProvider(stringLocalizerFactory: null),
detailsProvider
}),
new TestOptionsManager<MvcOptions>())