Add localizers to validation attributes

This commit is contained in:
Kirthi Krishnamraju 2015-09-24 14:02:20 -07:00
parent 49acfd562e
commit 0889b18f95
43 changed files with 847 additions and 100 deletions

View File

@ -5,13 +5,14 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Microsoft.Framework.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class CompareAttributeAdapter : DataAnnotationsClientModelValidator<CompareAttribute>
{
public CompareAttributeAdapter(CompareAttribute attribute)
: base(new CompareAttributeWrapper(attribute))
public CompareAttributeAdapter(CompareAttribute attribute, IStringLocalizer stringLocalizer)
: base(new CompareAttributeWrapper(attribute), stringLocalizer)
{
if (attribute == null)
{

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.Framework.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
@ -14,13 +15,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public abstract class DataAnnotationsClientModelValidator<TAttribute> : IClientModelValidator
where TAttribute : ValidationAttribute
{
private readonly IStringLocalizer _stringLocalizer;
/// <summary>
/// Create a new instance of <see cref="DataAnnotationsClientModelValidator{TAttribute}"/>.
/// </summary>
/// <param name="attribute">The <typeparamref name="TAttribute"/> instance to validate.</param>
public DataAnnotationsClientModelValidator(TAttribute attribute)
/// <param name="stringLocalizer">The <see cref="IStringLocalizer"/>.</param>
public DataAnnotationsClientModelValidator(TAttribute attribute, IStringLocalizer stringLocalizer)
{
Attribute = attribute;
_stringLocalizer = stringLocalizer;
}
/// <summary>
@ -48,7 +52,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(modelMetadata));
}
return Attribute.FormatErrorMessage(modelMetadata.GetDisplayName());
var displayName = modelMetadata.GetDisplayName();
if (_stringLocalizer != null &&
!string.IsNullOrEmpty(Attribute.ErrorMessage) &&
string.IsNullOrEmpty(Attribute.ErrorMessageResourceName) &&
Attribute.ErrorMessageResourceType == null)
{
return _stringLocalizer[displayName];
}
return Attribute.FormatErrorMessage(displayName);
}
}
}

View File

@ -5,6 +5,8 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.Framework.Localization;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
@ -18,11 +20,27 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public class DataAnnotationsClientModelValidatorProvider : IClientModelValidatorProvider
{
// A factory for validators based on ValidationAttribute.
internal delegate IClientModelValidator
DataAnnotationsClientModelValidationFactory(ValidationAttribute attribute);
internal delegate IClientModelValidator DataAnnotationsClientModelValidationFactory(
ValidationAttribute attribute,
IStringLocalizer stringLocalizer);
private readonly Dictionary<Type, DataAnnotationsClientModelValidationFactory> _attributeFactories =
BuildAttributeFactoriesDictionary();
private readonly IOptions<MvcDataAnnotationsLocalizationOptions> _options;
private readonly IStringLocalizerFactory _stringLocalizerFactory;
/// <summary>
/// Create a new instance of <see cref="DataAnnotationsClientModelValidatorProvider"/>.
/// </summary>
/// <param name="options">The <see cref="IOptions{MvcDataAnnotationsLocalizationOptions}"/>.</param>
/// <param name="stringLocalizerFactory">The <see cref="IStringLocalizerFactory"/>.</param>
public DataAnnotationsClientModelValidatorProvider(
IOptions<MvcDataAnnotationsLocalizationOptions> options,
IStringLocalizerFactory stringLocalizerFactory)
{
_options = options;
_stringLocalizerFactory = stringLocalizerFactory;
}
internal Dictionary<Type, DataAnnotationsClientModelValidationFactory> AttributeFactories
{
@ -36,6 +54,15 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
throw new ArgumentNullException(nameof(context));
}
IStringLocalizer stringLocalizer = null;
if (_options.Value.DataAnnotationLocalizerProvider != null && _stringLocalizerFactory != null)
{
// This will pass first non-null type (either containerType or modelType) to delegate.
// Pass the root model type(container type) if it is non null, else pass the model type.
stringLocalizer = _options.Value.DataAnnotationLocalizerProvider(
context.ModelMetadata.ContainerType ?? context.ModelMetadata.ModelType,
_stringLocalizerFactory);
}
var hasRequiredAttribute = false;
@ -46,14 +73,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
DataAnnotationsClientModelValidationFactory factory;
if (_attributeFactories.TryGetValue(attribute.GetType(), out factory))
{
context.Validators.Add(factory(attribute));
context.Validators.Add(factory(attribute, stringLocalizer));
}
}
if (!hasRequiredAttribute && context.ModelMetadata.IsRequired)
{
// Add a default '[Required]' validator for generating HTML if necessary.
context.Validators.Add(new RequiredAttributeAdapter(new RequiredAttribute()));
context.Validators.Add(new RequiredAttributeAdapter(new RequiredAttribute(), stringLocalizer));
}
}
@ -63,47 +90,73 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
{
typeof(RegularExpressionAttribute),
(attribute) => new RegularExpressionAttributeAdapter((RegularExpressionAttribute)attribute)
(attribute, stringLocalizer) => new RegularExpressionAttributeAdapter(
(RegularExpressionAttribute)attribute,
stringLocalizer)
},
{
typeof(MaxLengthAttribute),
(attribute) => new MaxLengthAttributeAdapter((MaxLengthAttribute)attribute)
(attribute, stringLocalizer) => new MaxLengthAttributeAdapter(
(MaxLengthAttribute)attribute,
stringLocalizer)
},
{
typeof(MinLengthAttribute),
(attribute) => new MinLengthAttributeAdapter((MinLengthAttribute)attribute)
(attribute, stringLocalizer) => new MinLengthAttributeAdapter(
(MinLengthAttribute)attribute,
stringLocalizer)
},
{
typeof(CompareAttribute),
(attribute) => new CompareAttributeAdapter((CompareAttribute)attribute)
(attribute, stringLocalizer) => new CompareAttributeAdapter(
(CompareAttribute)attribute,
stringLocalizer)
},
{
typeof(RequiredAttribute),
(attribute) => new RequiredAttributeAdapter((RequiredAttribute)attribute)
(attribute, stringLocalizer) => new RequiredAttributeAdapter(
(RequiredAttribute)attribute,
stringLocalizer)
},
{
typeof(RangeAttribute),
(attribute) => new RangeAttributeAdapter((RangeAttribute)attribute)
(attribute, stringLocalizer) => new RangeAttributeAdapter(
(RangeAttribute)attribute,
stringLocalizer)
},
{
typeof(StringLengthAttribute),
(attribute) => new StringLengthAttributeAdapter((StringLengthAttribute)attribute)
(attribute, stringLocalizer) => new StringLengthAttributeAdapter(
(StringLengthAttribute)attribute,
stringLocalizer)
},
{
typeof(CreditCardAttribute),
(attribute) => new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "creditcard")
(attribute, stringLocalizer) => new DataTypeAttributeAdapter(
(DataTypeAttribute)attribute,
"creditcard",
stringLocalizer)
},
{
typeof(EmailAddressAttribute),
(attribute) => new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "email")
(attribute, stringLocalizer) => new DataTypeAttributeAdapter(
(DataTypeAttribute)attribute,
"email",
stringLocalizer)
},
{
typeof(PhoneAttribute),
(attribute) => new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "phone")
(attribute, stringLocalizer) => new DataTypeAttributeAdapter(
(DataTypeAttribute)attribute,
"phone",
stringLocalizer)
},
{
typeof(UrlAttribute),
(attribute) => new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "url")
(attribute, stringLocalizer) => new DataTypeAttributeAdapter(
(DataTypeAttribute)attribute,
"url",
stringLocalizer)
}
};
}

View File

@ -5,12 +5,15 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.Framework.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class DataAnnotationsModelValidator : IModelValidator
{
public DataAnnotationsModelValidator(ValidationAttribute attribute)
private IStringLocalizer _stringLocalizer;
public DataAnnotationsModelValidator(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
{
if (attribute == null)
{
@ -18,9 +21,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
}
Attribute = attribute;
_stringLocalizer = stringLocalizer;
}
public ValidationAttribute Attribute { get; private set; }
public ValidationAttribute Attribute { get; }
public bool IsRequired
{
@ -59,7 +63,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
errorMemberName = null;
}
var validationResult = new ModelValidationResult(errorMemberName, result.ErrorMessage);
string errorMessage = null;
if (_stringLocalizer != null &&
!string.IsNullOrEmpty(Attribute.ErrorMessage) &&
string.IsNullOrEmpty(Attribute.ErrorMessageResourceName) &&
Attribute.ErrorMessageResourceType == null)
{
errorMessage = _stringLocalizer[Attribute.ErrorMessage];
}
var validationResult = new ModelValidationResult(errorMemberName, errorMessage ?? result.ErrorMessage);
return new ModelValidationResult[] { validationResult };
}

View File

@ -2,10 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using Microsoft.Framework.Localization;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
@ -16,11 +17,35 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
/// </summary>
public class DataAnnotationsModelValidatorProvider : IModelValidatorProvider
{
private readonly IOptions<MvcDataAnnotationsLocalizationOptions> _options;
private readonly IStringLocalizerFactory _stringLocalizerFactory;
/// <summary>
/// Create a new instance of <see cref="DataAnnotationsModelValidatorProvider"/>.
/// </summary>
/// <param name="options">The <see cref="IOptions{MvcDataAnnotationsLocalizationOptions}"/>.</param>
/// <param name="stringLocalizerFactory">The <see cref="IStringLocalizerFactory"/>.</param>
public DataAnnotationsModelValidatorProvider(
IOptions<MvcDataAnnotationsLocalizationOptions> options,
IStringLocalizerFactory stringLocalizerFactory)
{
_options = options;
_stringLocalizerFactory = stringLocalizerFactory;
}
public void GetValidators(ModelValidatorProviderContext context)
{
IStringLocalizer stringLocalizer = null;
if (_options.Value.DataAnnotationLocalizerProvider != null && _stringLocalizerFactory != null)
{
stringLocalizer = _options.Value.DataAnnotationLocalizerProvider(
context.ModelMetadata.ContainerType ?? context.ModelMetadata.ModelType,
_stringLocalizerFactory);
}
foreach (var attribute in context.ValidatorMetadata.OfType<ValidationAttribute>())
{
context.Validators.Add(new DataAnnotationsModelValidator(attribute));
context.Validators.Add(new DataAnnotationsModelValidator(attribute, stringLocalizer));
}
// Produce a validator if the type supports IValidatableObject

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Framework.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
@ -14,10 +15,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
/// </summary>
public class DataTypeAttributeAdapter : DataAnnotationsClientModelValidator<DataTypeAttribute>
{
public DataTypeAttributeAdapter(
DataTypeAttribute attribute,
string ruleName)
: base(attribute)
public DataTypeAttributeAdapter(DataTypeAttribute attribute, string ruleName, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
if (string.IsNullOrEmpty(ruleName))
{

View File

@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc.DataAnnotations.Internal;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
namespace Microsoft.Framework.DependencyInjection
{
/// <summary>
/// Extension methods for configuring MVC data annotations localization.
/// </summary>
public static class MvcDataAnnotationsMvcBuilderExtensions
{
/// <summary>
/// Adds MVC data annotations localization to the application.
/// </summary>
/// <param name="builder">The <see cref="IMvcBuilder"/>.</param>
/// <returns>The <see cref="IMvcBuilder"/>.</returns>
public static IMvcBuilder AddDataAnnotationsLocalization(this IMvcBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
return AddDataAnnotationsLocalization(builder, setupAction: null);
}
/// <summary>
/// Adds MVC data annotations localization to the application.
/// </summary>
/// <param name="builder">The <see cref="IMvcBuilder"/>.</param>
/// <param name="setupAction">The action to configure <see cref="MvcDataAnnotationsLocalizationOptions"/>.
/// </param>
/// <returns>The <see cref="IMvcBuilder"/>.</returns>
public static IMvcBuilder AddDataAnnotationsLocalization(
this IMvcBuilder builder,
Action<MvcDataAnnotationsLocalizationOptions> setupAction)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
DataAnnotationsLocalizationServices.AddDataAnnotationsLocalizationServices(
builder.Services,
setupAction);
return builder;
}
}
}

View File

@ -4,13 +4,22 @@
using System;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.DataAnnotations.Internal;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Framework.DependencyInjection.Extensions;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.Framework.DependencyInjection
{
/// <summary>
/// Extensions for configuring MVC data annotations using an <see cref="IMvcBuilder"/>.
/// </summary>
public static class MvcDataAnnotationsMvcCoreBuilderExtensions
{
/// <summary>
/// Registers MVC data annotations.
/// </summary>
/// <param name="builder">The <see cref="IMvcBuilder"/>.</param>
/// <returns>The <see cref="IMvcBuilder"/>.</returns>
public static IMvcCoreBuilder AddDataAnnotations(this IMvcCoreBuilder builder)
{
if (builder == null)
@ -22,11 +31,39 @@ namespace Microsoft.Framework.DependencyInjection
return builder;
}
/// <summary>
/// Registers an action to configure <see cref="MvcDataAnnotationsLocalizationOptions"/> for MVC data
/// annotations localization.
/// </summary>
/// <param name="builder">The <see cref="IMvcBuilder"/>.</param>
/// <param name="setupAction">An <see cref="Action{MvcDataAnnotationsLocalizationOptions}"/>.</param>
/// <returns>The <see cref="IMvcBuilder"/>.</returns>
public static IMvcCoreBuilder AddDataAnnotationsLocalization(
this IMvcCoreBuilder builder,
Action<MvcDataAnnotationsLocalizationOptions> setupAction)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
AddDataAnnotationsLocalizationServices(builder.Services, setupAction);
return builder;
}
// Internal for testing.
internal static void AddDataAnnotationsServices(IServiceCollection services)
{
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcDataAnnotationsMvcOptionsSetup>());
}
// Internal for testing.
internal static void AddDataAnnotationsLocalizationServices(
IServiceCollection services,
Action<MvcDataAnnotationsLocalizationOptions> setupAction)
{
DataAnnotationsLocalizationServices.AddDataAnnotationsLocalizationServices(services, setupAction);
}
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Extensions;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.DataAnnotations.Internal
{
public static class DataAnnotationsLocalizationServices
{
public static void AddDataAnnotationsLocalizationServices(
IServiceCollection services,
Action<MvcDataAnnotationsLocalizationOptions> setupAction)
{
services.AddLocalization();
if (setupAction != null)
{
services.Configure(setupAction);
}
else
{
services.TryAddEnumerable(
ServiceDescriptor.Transient
<IConfigureOptions<MvcDataAnnotationsLocalizationOptions>,
MvcDataAnnotationsLocalizationOptionsSetup>());
}
}
}
}

View File

@ -0,0 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.DataAnnotations.Internal
{
/// <summary>
/// Sets up default options for <see cref="MvcDataAnnotationsLocalizationOptions"/>.
/// </summary>
public class MvcDataAnnotationsLocalizationOptionsSetup : ConfigureOptions<MvcDataAnnotationsLocalizationOptions>
{
public MvcDataAnnotationsLocalizationOptionsSetup()
: base(ConfigureMvc)
{
}
public static void ConfigureMvc(MvcDataAnnotationsLocalizationOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
options.DataAnnotationLocalizerProvider = (modelType, stringLocalizerFactory) =>
stringLocalizerFactory.Create(modelType);
}
}
}

View File

@ -1,8 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc.ModelBinding.Metadata;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Localization;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.DataAnnotations.Internal
@ -12,15 +15,23 @@ namespace Microsoft.AspNet.Mvc.DataAnnotations.Internal
/// </summary>
public class MvcDataAnnotationsMvcOptionsSetup : ConfigureOptions<MvcOptions>
{
public MvcDataAnnotationsMvcOptionsSetup()
: base(ConfigureMvc)
public MvcDataAnnotationsMvcOptionsSetup(IServiceProvider serviceProvider)
: base(options => ConfigureMvc(options, serviceProvider))
{
}
public static void ConfigureMvc(MvcOptions options)
public static void ConfigureMvc(MvcOptions options, IServiceProvider serviceProvider)
{
var dataAnnotationLocalizationOptions =
serviceProvider.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>();
// This service will be registered only if AddDataAnnotationsLocalization() is added to service collection.
var stringLocalizerFactory = serviceProvider.GetService<IStringLocalizerFactory>();
options.ModelMetadataDetailsProviders.Add(new DataAnnotationsMetadataProvider());
options.ModelValidatorProviders.Add(new DataAnnotationsModelValidatorProvider());
options.ModelValidatorProviders.Add(new DataAnnotationsModelValidatorProvider(
dataAnnotationLocalizationOptions,
stringLocalizerFactory));
}
}
}

View File

@ -4,13 +4,14 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.Framework.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class MaxLengthAttributeAdapter : DataAnnotationsClientModelValidator<MaxLengthAttribute>
{
public MaxLengthAttributeAdapter(MaxLengthAttribute attribute)
: base(attribute)
public MaxLengthAttributeAdapter(MaxLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
}

View File

@ -4,13 +4,14 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.Framework.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class MinLengthAttributeAdapter : DataAnnotationsClientModelValidator<MinLengthAttribute>
{
public MinLengthAttributeAdapter(MinLengthAttribute attribute)
: base(attribute)
public MinLengthAttributeAdapter(MinLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
}

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Framework.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
/// <summary>
/// Provides programmatic configuration for DataAnnotations localization in the MVC framework.
/// </summary>
public class MvcDataAnnotationsLocalizationOptions
{
/// <summary>
/// The delegate to invoke for creating <see cref="IStringLocalizer"/>.
/// </summary>
public Func<Type, IStringLocalizerFactory, IStringLocalizer> DataAnnotationLocalizerProvider;
}
}

View File

@ -4,13 +4,14 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.Framework.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class RangeAttributeAdapter : DataAnnotationsClientModelValidator<RangeAttribute>
{
public RangeAttributeAdapter(RangeAttribute attribute)
: base(attribute)
public RangeAttributeAdapter(RangeAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
}

View File

@ -4,13 +4,14 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.Framework.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class RegularExpressionAttributeAdapter : DataAnnotationsClientModelValidator<RegularExpressionAttribute>
{
public RegularExpressionAttributeAdapter(RegularExpressionAttribute attribute)
: base(attribute)
public RegularExpressionAttributeAdapter(RegularExpressionAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
}

View File

@ -4,13 +4,14 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.Framework.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class RequiredAttributeAdapter : DataAnnotationsClientModelValidator<RequiredAttribute>
{
public RequiredAttributeAdapter(RequiredAttribute attribute)
: base(attribute)
public RequiredAttributeAdapter(RequiredAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
}

View File

@ -4,13 +4,14 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.Framework.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class StringLengthAttributeAdapter : DataAnnotationsClientModelValidator<StringLengthAttribute>
{
public StringLengthAttributeAdapter(StringLengthAttribute attribute)
: base(attribute)
public StringLengthAttributeAdapter(StringLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
}

View File

@ -11,7 +11,8 @@
"dependencies": {
"Microsoft.AspNet.Mvc.Core": "6.0.0-*",
"Microsoft.Framework.ClosedGenericMatcher.Sources": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.CopyOnWriteDictionary.Sources": { "version": "1.0.0-*", "type": "build" }
"Microsoft.Framework.CopyOnWriteDictionary.Sources": { "version": "1.0.0-*", "type": "build" },
"Microsoft.Framework.Localization": "1.0.0-*"
},
"frameworks": {

View File

@ -1,7 +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.
using System;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Localization;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal
@ -14,16 +17,24 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal
/// <summary>
/// Initializes a new instance of <see cref="MvcViewOptionsSetup"/>.
/// </summary>
public MvcViewOptionsSetup()
: base(ConfigureMvc)
public MvcViewOptionsSetup(IServiceProvider serviceProvider)
: base(options => ConfigureMvc(options, serviceProvider))
{
}
public static void ConfigureMvc(MvcViewOptions options)
public static void ConfigureMvc(
MvcViewOptions options,
IServiceProvider serviceProvider)
{
var dataAnnotationsLocalizationOptions =
serviceProvider.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>();
var stringLocalizerFactory = serviceProvider.GetService<IStringLocalizerFactory>();
// Set up client validators
options.ClientModelValidatorProviders.Add(new DefaultClientModelValidatorProvider());
options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider());
options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider(
dataAnnotationsLocalizationOptions,
stringLocalizerFactory));
options.ClientModelValidatorProviders.Add(new NumericClientModelValidatorProvider());
}
}

View File

@ -242,7 +242,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
ModelBinder = innerBinder ?? CreateIntBinder(),
MetadataProvider = metataProvider,
ValidatorProvider = new DataAnnotationsModelValidatorProvider()
ValidatorProvider = new DataAnnotationsModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null)
}
};
return bindingContext;

View File

@ -72,7 +72,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new MutableObjectModelBinder()
};
var validator = new DataAnnotationsModelValidatorProvider();
var validator = new DataAnnotationsModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel();
var modelStateDictionary = new ModelStateDictionary();
var values = new Dictionary<string, object>
@ -111,7 +113,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new MutableObjectModelBinder()
};
var validator = new DataAnnotationsModelValidatorProvider();
var validator = new DataAnnotationsModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel { MyProperty = "Old-Value" };
var modelStateDictionary = new ModelStateDictionary();
var values = new Dictionary<string, object>
@ -185,7 +189,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new MutableObjectModelBinder()
};
var validator = new DataAnnotationsModelValidatorProvider();
var validator = new DataAnnotationsModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel {
MyProperty = "Old-Value",
IncludedProperty = "Old-IncludedPropertyValue",
@ -274,7 +280,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new MutableObjectModelBinder()
};
var validator = new DataAnnotationsModelValidatorProvider();
var validator = new DataAnnotationsModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel
{
MyProperty = "Old-Value",
@ -326,7 +334,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new MutableObjectModelBinder()
};
var validator = new DataAnnotationsModelValidatorProvider();
var validator = new DataAnnotationsModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel
{
MyProperty = "Old-Value",
@ -532,7 +542,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new MutableObjectModelBinder()
};
var validator = new DataAnnotationsModelValidatorProvider();
var validator = new DataAnnotationsModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel
{
MyProperty = "Old-Value",
@ -623,7 +635,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new MutableObjectModelBinder()
};
var validator = new DataAnnotationsModelValidatorProvider();
var validator = new DataAnnotationsModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel { MyProperty = "Old-Value" };
var modelStateDictionary = new ModelStateDictionary();
var values = new Dictionary<string, object>

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var metadata = metadataProvider.GetMetadataForProperty(typeof(PropertyDisplayNameModel), "MyProperty");
var attribute = new CompareAttribute("OtherProperty");
var adapter = new CompareAttributeAdapter(attribute);
var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
@ -50,7 +50,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, metadataProvider, requestServices);
var adapter = new CompareAttributeAdapter(attribute);
var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null);
// Act
var rules = adapter.GetClientValidationRules(context);
@ -76,7 +76,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, metadataProvider, requestServices);
var adapter = new CompareAttributeAdapter(attribute);
var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null);
// Act
var rules = adapter.GetClientValidationRules(context);
@ -102,7 +102,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, metadataProvider, requestServices);
var adapter = new CompareAttributeAdapter(attribute);
var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null);
// Act
var rules = adapter.GetClientValidationRules(context);

View File

@ -17,7 +17,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public void GetValidators_AddsRequiredAttribute_ForIsRequiredTrue()
{
// Arrange
var provider = new DataAnnotationsClientModelValidatorProvider();
var provider = new DataAnnotationsClientModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var metadata = _metadataProvider.GetMetadataForProperty(
typeof(DummyRequiredAttributeHelperClass),
@ -37,7 +39,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public void GetValidators_DoesNotAddRequiredAttribute_ForIsRequiredFalse()
{
// Arrange
var provider = new DataAnnotationsClientModelValidatorProvider();
var provider = new DataAnnotationsClientModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var metadata = _metadataProvider.GetMetadataForProperty(
typeof(DummyRequiredAttributeHelperClass),
@ -56,7 +60,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public void GetValidators_DoesNotAddExtraRequiredAttribute_IfAttributeIsSpecifiedExplicitly()
{
// Arrange
var provider = new DataAnnotationsClientModelValidatorProvider();
var provider = new DataAnnotationsClientModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var metadata = _metadataProvider.GetMetadataForProperty(
typeof(DummyRequiredAttributeHelperClass),
@ -122,11 +128,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
Type expectedAdapterType)
{
// Arrange
var adapters = new DataAnnotationsClientModelValidatorProvider().AttributeFactories;
var adapters = new DataAnnotationsClientModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null)
.AttributeFactories;
var adapterFactory = adapters.Single(kvp => kvp.Key == attribute.GetType()).Value;
// Act
var adapter = adapterFactory(attribute);
var adapter = adapterFactory(attribute, stringLocalizer: null);
// Assert
Assert.IsType(expectedAdapterType, adapter);
@ -150,11 +159,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
string expectedRuleName)
{
// Arrange
var adapters = new DataAnnotationsClientModelValidatorProvider().AttributeFactories;
var adapters = new DataAnnotationsClientModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null)
.AttributeFactories;
var adapterFactory = adapters.Single(kvp => kvp.Key == attribute.GetType()).Value;
// Act
var adapter = adapterFactory(attribute);
var adapter = adapterFactory(attribute, stringLocalizer: null);
// Assert
var dataTypeAdapter = Assert.IsType<DataTypeAttributeAdapter>(adapter);
@ -165,7 +177,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public void UnknownValidationAttribute_IsNotAddedAsValidator()
{
// Arrange
var provider = new DataAnnotationsClientModelValidatorProvider();
var provider = new DataAnnotationsClientModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var metadata = _metadataProvider.GetMetadataForType(typeof(DummyClassWithDummyValidationAttribute));
var providerContext = new ClientValidatorProviderContext(metadata);

View File

@ -21,7 +21,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public void GetValidators_ReturnsValidatorForIValidatableObject()
{
// Arrange
var provider = new DataAnnotationsModelValidatorProvider();
var provider = new DataAnnotationsModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var mockValidatable = Mock.Of<IValidatableObject>();
var metadata = _metadataProvider.GetMetadataForType(mockValidatable.GetType());
@ -40,7 +42,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public void UnknownValidationAttributeGetsDefaultAdapter()
{
// Arrange
var provider = new DataAnnotationsModelValidatorProvider();
var provider = new DataAnnotationsModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var metadata = _metadataProvider.GetMetadataForType(typeof(DummyClassWithDummyValidationAttribute));
var providerContext = new ModelValidatorProviderContext(metadata);
@ -69,7 +73,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public void IValidatableObjectGetsAValidator()
{
// Arrange
var provider = new DataAnnotationsModelValidatorProvider();
var provider = new DataAnnotationsModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var mockValidatable = new Mock<IValidatableObject>();
var metadata = _metadataProvider.GetMetadataForType(mockValidatable.Object.GetType());

View File

@ -7,6 +7,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
#endif
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Localization;
#if DNX451
using Moq;
using Moq.Protected;
@ -26,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var attribute = new RequiredAttribute();
// Act
var validator = new DataAnnotationsModelValidator(attribute);
var validator = new DataAnnotationsModelValidator(attribute, stringLocalizer : null);
// Assert
Assert.Same(attribute, validator.Attribute);
@ -67,7 +68,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
})
.Returns(ValidationResult.Success)
.Verifiable();
var validator = new DataAnnotationsModelValidator(attribute.Object);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validationContext = CreateValidationContext(modelExplorer);
// Act
@ -89,7 +90,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var attribute = new Mock<ValidationAttribute> { CallBase = true };
attribute.Setup(a => a.IsValid(modelExplorer.Model)).Returns(true);
var validator = new DataAnnotationsModelValidator(attribute.Object);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validationContext = CreateValidationContext(modelExplorer);
// Act
@ -110,7 +111,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var attribute = new Mock<ValidationAttribute> { CallBase = true };
attribute.Setup(a => a.IsValid(modelExplorer.Model)).Returns(false);
var validator = new DataAnnotationsModelValidator(attribute.Object);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validationContext = CreateValidationContext(modelExplorer);
// Act
@ -134,7 +135,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
attribute.Protected()
.Setup<ValidationResult>("IsValid", ItExpr.IsAny<object>(), ItExpr.IsAny<ValidationContext>())
.Returns(ValidationResult.Success);
var validator = new DataAnnotationsModelValidator(attribute.Object);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validationContext = CreateValidationContext(modelExplorer);
// Act
@ -158,7 +159,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
attribute.Protected()
.Setup<ValidationResult>("IsValid", ItExpr.IsAny<object>(), ItExpr.IsAny<ValidationContext>())
.Returns(new ValidationResult(errorMessage, memberNames: null));
var validator = new DataAnnotationsModelValidator(attribute.Object);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validationContext = CreateValidationContext(modelExplorer);
@ -184,7 +185,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
.Setup<ValidationResult>("IsValid", ItExpr.IsAny<object>(), ItExpr.IsAny<ValidationContext>())
.Returns(new ValidationResult(errorMessage, new[] { "FirstName" }));
var validator = new DataAnnotationsModelValidator(attribute.Object);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validationContext = CreateValidationContext(modelExplorer);
// Act
@ -207,7 +208,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
.Setup<ValidationResult>("IsValid", ItExpr.IsAny<object>(), ItExpr.IsAny<ValidationContext>())
.Returns(new ValidationResult("Name error", new[] { "Name" }));
var validator = new DataAnnotationsModelValidator(attribute.Object);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validationContext = CreateValidationContext(modelExplorer);
// Act
@ -217,15 +218,46 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
ModelValidationResult validationResult = Assert.Single(results);
Assert.Equal("Name", validationResult.MemberName);
}
[Fact]
public void ValidateWithIsValidFalse_StringLocalizerReturnsLocalizerErrorMessage()
{
// Arrange
var modelExplorer = _metadataProvider
.GetModelExplorerForType(typeof(string), "Hello")
.GetExplorerForProperty("Length");
var attribute = new Mock<ValidationAttribute> { CallBase = true };
attribute.Setup(a => a.IsValid(modelExplorer.Model)).Returns(false);
attribute.Object.ErrorMessage = "Length";
var localizedString = new LocalizedString("Length", "Longueur est invalide");
var stringLocalizer = new Mock<IStringLocalizer>();
stringLocalizer.Setup(s => s["Length"]).Returns(localizedString);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer.Object);
var validationContext = CreateValidationContext(modelExplorer);
// Act
var result = validator.Validate(validationContext);
// Assert
var validationResult = result.Single();
Assert.Equal("", validationResult.MemberName);
Assert.Equal("Longueur est invalide", validationResult.Message);
}
#endif
[Fact]
public void IsRequiredTests()
{
// Arrange & Act & Assert
Assert.False(new DataAnnotationsModelValidator(new RangeAttribute(10, 20)).IsRequired);
Assert.True(new DataAnnotationsModelValidator(new RequiredAttribute()).IsRequired);
Assert.True(new DataAnnotationsModelValidator(new DerivedRequiredAttribute()).IsRequired);
Assert.False(new DataAnnotationsModelValidator(new RangeAttribute(10, 20), stringLocalizer: null)
.IsRequired);
Assert.True(new DataAnnotationsModelValidator(new RequiredAttribute(), stringLocalizer: null).IsRequired);
Assert.True(new DataAnnotationsModelValidator(new DerivedRequiredAttribute(), stringLocalizer: null)
.IsRequired);
}
private static ModelValidationContext CreateValidationContext(ModelExplorer modelExplorer)

View File

@ -4,6 +4,8 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Localization;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
@ -18,7 +20,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var attribute = new MaxLengthAttribute(10);
var adapter = new MaxLengthAttributeAdapter(attribute);
var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: null);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);
@ -44,7 +46,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), propertyName);
var attribute = new MaxLengthAttribute(5) { ErrorMessage = message };
var adapter = new MaxLengthAttributeAdapter(attribute);
var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: null);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);
@ -59,5 +61,38 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
Assert.Equal(5, rule.ValidationParameters["max"]);
Assert.Equal("Length must be at most 5", rule.ErrorMessage);
}
}
#if DNX451
[Fact]
[ReplaceCulture]
public void ClientRulesWithMaxLengthAttribute_StringLocalizer_ReturnsLocalizedErrorString()
{
// Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var errorKey = metadata.GetDisplayName();
var attribute = new MaxLengthAttribute(10);
attribute.ErrorMessage = errorKey;
var localizedString = new LocalizedString(errorKey, "Longueur est invalide");
var stringLocalizer = new Mock<IStringLocalizer>();
stringLocalizer.Setup(s => s[errorKey]).Returns(localizedString);
var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer.Object);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);
// Act
var rules = adapter.GetClientValidationRules(context);
// Assert
var rule = Assert.Single(rules);
Assert.Equal("maxlength", rule.ValidationType);
Assert.Equal(1, rule.ValidationParameters.Count);
Assert.Equal(10, rule.ValidationParameters["max"]);
Assert.Equal("Longueur est invalide", rule.ErrorMessage);
}
#endif
}
}

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var attribute = new MinLengthAttribute(6);
var adapter = new MinLengthAttributeAdapter(attribute);
var adapter = new MinLengthAttributeAdapter(attribute, stringLocalizer: null);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);
@ -44,7 +44,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), propertyName);
var attribute = new MinLengthAttribute(2) { ErrorMessage = message };
var adapter = new MinLengthAttributeAdapter(attribute);
var adapter = new MinLengthAttributeAdapter(attribute, stringLocalizer: null);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var attribute = new RangeAttribute(typeof(decimal), "0", "100");
var adapter = new RangeAttributeAdapter(attribute);
var adapter = new RangeAttributeAdapter(attribute, stringLocalizer: null);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var attribute = new RequiredAttribute();
var adapter = new RequiredAttributeAdapter(attribute);
var adapter = new RequiredAttributeAdapter(attribute, stringLocalizer: null);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var attribute = new StringLengthAttribute(8);
var adapter = new StringLengthAttributeAdapter(attribute);
var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: null);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);
@ -42,7 +42,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var attribute = new StringLengthAttribute(10) { MinimumLength = 3 };
var adapter = new StringLengthAttributeAdapter(attribute);
var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: null);
var serviceCollection = new ServiceCollection();
var requestServices = serviceCollection.BuildServiceProvider();
var context = new ClientModelValidationContext(metadata, provider, requestServices);

View File

@ -7,6 +7,7 @@ using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Testing;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
@ -121,5 +122,32 @@ Salut John ! Vous êtes en 2015 an aujourd'hui est Thursday";
// Assert
Assert.Equal(expected, body.Trim());
}
[Fact]
public async Task Localization_InvalidModel_ValidationAttributes_ReturnsLocalizedErrorMessage()
{
// Arrange
var expected =
@"<span class=""field-validation-error"" data-valmsg-for=""Name"" data-valmsg-replace=""true"">Nom non valide. Longueur minimale de nom est 4</span>
<span class=""field-validation-error"" data-valmsg-for=""Product.ProductName"" data-valmsg-replace=""true"">Nom du produit est invalide</span>
<div class=""editor-label""><label for=""Name"">Name</label></div>
<div class=""editor-field""><input class=""input-validation-error text-box single-line"" data-val=""true"" data-val-minlength=""Nom non valide. Longueur minimale de nom est 4"" data-val-minlength-min=""4"" id=""Name"" name=""Name"" type=""text"" value=""A"" /> <span class=""field-validation-error"" data-valmsg-for=""Name"" data-valmsg-replace=""true"">Nom non valide. Longueur minimale de nom est 4</span></div>
<div class=""editor-label""><label for=""Product_ProductName"">ProductName</label></div>
<div class=""editor-field""><input class=""input-validation-error text-box single-line"" data-val=""true"" data-val-required=""Nom du produit est invalide"" id=""Product_ProductName"" name=""Product.ProductName"" type=""text"" value="""" /> <span class=""field-validation-error"" data-valmsg-for=""Product.ProductName"" data-valmsg-replace=""true"">Nom du produit est invalide</span></div>";
var cultureCookie = "c=fr|uic=fr";
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Home/GetInvalidUser");
request.Headers.Add(
"Cookie",
new CookieHeaderValue("ASPNET_CULTURE", cultureCookie).ToString());
// Act
var response = await Client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(expected, body.Trim(), ignoreLineEndingDifferences: true);
}
}
}

View File

@ -1,10 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc.DataAnnotations.Internal;
using Microsoft.AspNet.Mvc.Formatters.Json.Internal;
using Microsoft.AspNet.Mvc.Internal;
using Microsoft.AspNet.Mvc.TestCommon;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.IntegrationTests
@ -15,7 +16,10 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
{
Value = new MvcOptions();
MvcCoreMvcOptionsSetup.ConfigureMvc(Value);
MvcDataAnnotationsMvcOptionsSetup.ConfigureMvc(Value);
var collection = new ServiceCollection().AddOptions();
MvcDataAnnotationsMvcOptionsSetup.ConfigureMvc(
Value,
collection.BuildServiceProvider());
MvcJsonMvcOptionsSetup.ConfigureMvc(Value, SerializerSettingsProvider.CreateSerializerSettings());
}

View File

@ -13,7 +13,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var providers = new IClientModelValidatorProvider[]
{
new DefaultClientModelValidatorProvider(),
new DataAnnotationsClientModelValidatorProvider(),
new DataAnnotationsClientModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null),
};
return new TestClientModelValidatorProvider(providers);

View File

@ -13,7 +13,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var providers = new IModelValidatorProvider[]
{
new DefaultModelValidatorProvider(),
new DataAnnotationsModelValidatorProvider(),
new DataAnnotationsModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null),
};
return new TestModelValidatorProvider(providers);

View File

@ -1753,7 +1753,9 @@ namespace Microsoft.AspNet.Mvc.Test
{
ModelBinder = binder,
ValueProvider = provider,
ValidatorProvider = new DataAnnotationsModelValidatorProvider()
ValidatorProvider = new DataAnnotationsModelValidatorProvider(
options: null,
stringLocalizerFactory: null)
};
return new TestableController()

View File

@ -219,7 +219,13 @@ namespace Microsoft.AspNet.Mvc.Rendering
{
options.HtmlHelperOptions.IdAttributeDotReplacement = idAttributeDotReplacement;
}
options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider());
var localizationOptionsAccesor = new Mock<IOptions<MvcDataAnnotationsLocalizationOptions>>();
localizationOptionsAccesor.SetupGet(o => o.Value).Returns(new MvcDataAnnotationsLocalizationOptions());
options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider(
localizationOptionsAccesor.Object,
stringLocalizerFactory: null));
var optionsAccessor = new Mock<IOptions<MvcViewOptions>>();
optionsAccessor
.SetupGet(o => o.Value)
@ -235,6 +241,9 @@ namespace Microsoft.AspNet.Mvc.Rendering
serviceProvider
.Setup(s => s.GetService(typeof(IViewComponentHelper)))
.Returns(new Mock<IViewComponentHelper>().Object);
serviceProvider
.Setup(s => s.GetService(typeof(IViewComponentHelper)))
.Returns(new Mock<IViewComponentHelper>().Object);
httpContext.RequestServices = serviceProvider.Object;
if (htmlGenerator == null)

View File

@ -1,6 +1,7 @@
// 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 LocalizationWebSite.Models;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Localization;
@ -25,5 +26,17 @@ namespace LocalizationWebSite.Controllers
ViewData["Message"] = _localizer["Learn More"];
return View();
}
public IActionResult GetInvalidUser()
{
var user = new User
{
Name = "A",
Product = new Product()
};
TryValidateModel(user);
return View(user);
}
}
}

View File

@ -0,0 +1,21 @@
// 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 LocalizationWebSite.Models
{
public class User
{
[MinLength(4, ErrorMessage = "Name")]
public string Name { get; set; }
public Product Product { get; set; }
}
public class Product
{
[Required(ErrorMessage = "ProductName")]
public string ProductName { get; set; }
}
}

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="ProductName" xml:space="preserve">
<value>Nom du produit est invalide</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="Name" xml:space="preserve">
<value>Nom non valide. Longueur minimale de nom est 4</value>
</data>
</root>

View File

@ -10,7 +10,10 @@ namespace LocalizationWebSite
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddViewLocalization(options => options.ResourcesPath = "Resources");
services
.AddMvc()
.AddViewLocalization(options => options.ResourcesPath = "Resources")
.AddDataAnnotationsLocalization();
}
public void Configure(IApplicationBuilder app)
@ -18,7 +21,7 @@ namespace LocalizationWebSite
app.UseCultureReplacer();
app.UseRequestLocalization();
app.UseMvcWithDefaultRoute();
}
}

View File

@ -0,0 +1,8 @@
@model LocalizationWebSite.Models.User
@Html.ValidationMessage("Name")
@Html.ValidationMessage("Product.ProductName")
@Html.EditorForModel()
@Html.EditorFor(model => model.Product)