Support overrides for client-side validation messages

- #2969
- `RemoteAttribute` did not support `IStringLocalizer` overrides
 - use same `MvcDataAnnotationsLocalizationOptions` property as for other `ValidationAttribute`s
- error message `NumericClientModelValidator` added could not be overridden
 - not related to `IStringLocalizer` because users have no way to set the resource lookup key
 - extend `IModelBindingMessageProvider` to add the necessary `Func<,>`
- also correct problem using resources with `RemoteAttribute` and add lots of tests
This commit is contained in:
Doug Bunting 2016-01-28 22:26:38 -08:00
parent d6843b5a9d
commit 26a14a25ab
18 changed files with 631 additions and 158 deletions

View File

@ -51,5 +51,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </summary> /// </summary>
/// <value>Default <see cref="string"/> is "The value '{0}' is invalid.".</value> /// <value>Default <see cref="string"/> is "The value '{0}' is invalid.".</value>
Func<string, string> ValueIsInvalidAccessor { get; } Func<string, string> ValueIsInvalidAccessor { get; }
/// <summary>
/// Error message HTML and tag helpers add for client-side validation of numeric formats. Visible in the
/// browser if the field for a <c>float</c> property (for example) does not have a correctly-formatted value.
/// </summary>
/// <value>Default <see cref="string"/> is "The field {0} must be a number.".</value>
Func<string, string> ValueMustBeANumberAccessor { get; }
} }
} }

View File

@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
messageProvider.AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid; messageProvider.AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid;
messageProvider.UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid; messageProvider.UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid;
messageProvider.ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid; messageProvider.ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid;
messageProvider.ValueMustBeANumberAccessor = Resources.FormatHtmlGeneration_ValueMustBeNumber;
// Set up ModelBinding // Set up ModelBinding
options.ModelBinders.Add(new BinderTypeBasedModelBinder()); options.ModelBinders.Add(new BinderTypeBasedModelBinder());

View File

@ -44,6 +44,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid, AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid,
UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid, UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid,
ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid, ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid,
ValueMustBeANumberAccessor = Resources.FormatHtmlGeneration_ValueMustBeNumber,
}; };
} }
} }

View File

@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
private Func<string, string, string> _attemptedValueIsInvalidAccessor; private Func<string, string, string> _attemptedValueIsInvalidAccessor;
private Func<string, string> _unknownValueIsInvalidAccessor; private Func<string, string> _unknownValueIsInvalidAccessor;
private Func<string, string> _valueIsInvalidAccessor; private Func<string, string> _valueIsInvalidAccessor;
private Func<string, string> _valueMustBeANumberAccessor;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ModelBindingMessageProvider"/> class. /// Initializes a new instance of the <see cref="ModelBindingMessageProvider"/> class.
@ -42,6 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
AttemptedValueIsInvalidAccessor = originalProvider.AttemptedValueIsInvalidAccessor; AttemptedValueIsInvalidAccessor = originalProvider.AttemptedValueIsInvalidAccessor;
UnknownValueIsInvalidAccessor = originalProvider.UnknownValueIsInvalidAccessor; UnknownValueIsInvalidAccessor = originalProvider.UnknownValueIsInvalidAccessor;
ValueIsInvalidAccessor = originalProvider.ValueIsInvalidAccessor; ValueIsInvalidAccessor = originalProvider.ValueIsInvalidAccessor;
ValueMustBeANumberAccessor = originalProvider.ValueMustBeANumberAccessor;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -151,5 +153,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
_valueIsInvalidAccessor = value; _valueIsInvalidAccessor = value;
} }
} }
/// <inheritdoc/>
public Func<string, string> ValueMustBeANumberAccessor
{
get
{
return _valueMustBeANumberAccessor;
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_valueMustBeANumberAccessor = value;
}
}
} }
} }

View File

@ -1002,38 +1002,6 @@ namespace Microsoft.AspNetCore.Mvc.Core
return GetString("UrlNotLocal"); return GetString("UrlNotLocal");
} }
/// <summary>
/// The stream must support reading.
/// </summary>
internal static string HttpRequestStreamReader_StreamNotReadable
{
get { return GetString("HttpRequestStreamReader_StreamNotReadable"); }
}
/// <summary>
/// The stream must support reading.
/// </summary>
internal static string FormatHttpRequestStreamReader_StreamNotReadable()
{
return GetString("HttpRequestStreamReader_StreamNotReadable");
}
/// <summary>
/// The stream must support writing.
/// </summary>
internal static string HttpResponseStreamWriter_StreamNotWritable
{
get { return GetString("HttpResponseStreamWriter_StreamNotWritable"); }
}
/// <summary>
/// The stream must support writing.
/// </summary>
internal static string FormatHttpResponseStreamWriter_StreamNotWritable()
{
return GetString("HttpResponseStreamWriter_StreamNotWritable");
}
/// <summary> /// <summary>
/// The argument '{0}' is invalid. Empty or null formats are not supported. /// The argument '{0}' is invalid. Empty or null formats are not supported.
/// </summary> /// </summary>
@ -1114,6 +1082,22 @@ namespace Microsoft.AspNetCore.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlGeneration_ValueIsInvalid"), p0); return string.Format(CultureInfo.CurrentCulture, GetString("HtmlGeneration_ValueIsInvalid"), p0);
} }
/// <summary>
/// The field {0} must be a number.
/// </summary>
internal static string HtmlGeneration_ValueMustBeNumber
{
get { return GetString("HtmlGeneration_ValueMustBeNumber"); }
}
/// <summary>
/// The field {0} must be a number.
/// </summary>
internal static string FormatHtmlGeneration_ValueMustBeNumber(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlGeneration_ValueMustBeNumber"), p0);
}
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -328,4 +328,7 @@
<data name="HtmlGeneration_ValueIsInvalid" xml:space="preserve"> <data name="HtmlGeneration_ValueIsInvalid" xml:space="preserve">
<value>The value '{0}' is invalid.</value> <value>The value '{0}' is invalid.</value>
</data> </data>
<data name="HtmlGeneration_ValueMustBeNumber" xml:space="preserve">
<value>The field {0} must be a number.</value>
</data>
</root> </root>

View File

@ -44,7 +44,8 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
throw new ArgumentNullException(nameof(modelMetadata)); throw new ArgumentNullException(nameof(modelMetadata));
} }
return Resources.FormatNumericClientModelValidator_FieldMustBeNumber(modelMetadata.GetDisplayName()); return modelMetadata.ModelBindingMessageProvider.ValueMustBeANumberAccessor(
modelMetadata.GetDisplayName());
} }
} }
} }

View File

@ -42,22 +42,6 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations
return GetString("ArgumentCannotBeNullOrEmpty"); return GetString("ArgumentCannotBeNullOrEmpty");
} }
/// <summary>
/// The field {0} must be a number.
/// </summary>
internal static string NumericClientModelValidator_FieldMustBeNumber
{
get { return GetString("NumericClientModelValidator_FieldMustBeNumber"); }
}
/// <summary>
/// The field {0} must be a number.
/// </summary>
internal static string FormatNumericClientModelValidator_FieldMustBeNumber(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("NumericClientModelValidator_FieldMustBeNumber"), p0);
}
/// <summary> /// <summary>
/// The '{0}' property of '{1}' must not be null. /// The '{0}' property of '{1}' must not be null.
/// </summary> /// </summary>

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@ -123,9 +123,6 @@
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve"> <data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
<value>Value cannot be null or empty.</value> <value>Value cannot be null or empty.</value>
</data> </data>
<data name="NumericClientModelValidator_FieldMustBeNumber" xml:space="preserve">
<value>The field {0} must be a number.</value>
</data>
<data name="PropertyOfTypeCannotBeNull" xml:space="preserve"> <data name="PropertyOfTypeCannotBeNull" xml:space="preserve">
<value>The '{0}' property of '{1}' must not be null.</value> <value>The '{0}' property of '{1}' must not be null.</value>
</data> </data>

View File

@ -6,12 +6,14 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc namespace Microsoft.AspNetCore.Mvc
{ {
@ -25,6 +27,8 @@ namespace Microsoft.AspNetCore.Mvc
{ {
private string _additionalFields = string.Empty; private string _additionalFields = string.Empty;
private string[] _additionalFieldsSplit = new string[0]; private string[] _additionalFieldsSplit = new string[0];
private bool _checkedForLocalizer;
private IStringLocalizer _stringLocalizer;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RemoteAttribute"/> class. /// Initializes a new instance of the <see cref="RemoteAttribute"/> class.
@ -33,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc
/// Intended for subclasses that support URL generation with no route, action, or controller names. /// Intended for subclasses that support URL generation with no route, action, or controller names.
/// </remarks> /// </remarks>
protected RemoteAttribute() protected RemoteAttribute()
: base(Resources.RemoteAttribute_RemoteValidationFailed) : base(errorMessageAccessor: () => Resources.RemoteAttribute_RemoteValidationFailed)
{ {
RouteData = new RouteValueDictionary(); RouteData = new RouteValueDictionary();
} }
@ -241,8 +245,10 @@ namespace Microsoft.AspNetCore.Mvc
MergeAttribute(context.Attributes, "data-val", "true"); MergeAttribute(context.Attributes, "data-val", "true");
var errorMessage = FormatErrorMessage(context.ModelMetadata.GetDisplayName()); CheckForLocalizer(context);
var errorMessage = GetErrorMessage(context.ModelMetadata.GetDisplayName());
MergeAttribute(context.Attributes, "data-val-remote", errorMessage); MergeAttribute(context.Attributes, "data-val-remote", errorMessage);
MergeAttribute(context.Attributes, "data-val-remote-url", GetUrl(context)); MergeAttribute(context.Attributes, "data-val-remote-url", GetUrl(context));
if (!string.IsNullOrEmpty(HttpMethod)) if (!string.IsNullOrEmpty(HttpMethod))
@ -272,10 +278,45 @@ namespace Microsoft.AspNetCore.Mvc
return new string[0]; return new string[0];
} }
var split = original.Split(',') var split = original
.Select(piece => piece.Trim()) .Split(',')
.Where(trimmed => !string.IsNullOrEmpty(trimmed)); .Select(piece => piece.Trim())
.Where(trimmed => !string.IsNullOrEmpty(trimmed));
return split; return split;
} }
private void CheckForLocalizer(ClientModelValidationContext context)
{
if (!_checkedForLocalizer)
{
_checkedForLocalizer = true;
var services = context.ActionContext.HttpContext.RequestServices;
var options = services.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>();
var factory = services.GetService<IStringLocalizerFactory>();
var provider = options.Value.DataAnnotationLocalizerProvider;
if (factory != null && provider != null)
{
_stringLocalizer = provider(
context.ModelMetadata.ContainerType ?? context.ModelMetadata.ModelType,
factory);
}
}
}
private string GetErrorMessage(string displayName)
{
if (_stringLocalizer != null &&
!string.IsNullOrEmpty(ErrorMessage) &&
string.IsNullOrEmpty(ErrorMessageResourceName) &&
ErrorMessageResourceType == null)
{
return _stringLocalizer[ErrorMessage, displayName];
}
return FormatErrorMessage(displayName);
}
} }
} }

View File

@ -754,6 +754,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
(value, name) => "Unexpected InvalidValueWithKnownAttemptedValueAccessor use", (value, name) => "Unexpected InvalidValueWithKnownAttemptedValueAccessor use",
UnknownValueIsInvalidAccessor = name => $"Hmm, the supplied value is not valid for { name }.", UnknownValueIsInvalidAccessor = name => $"Hmm, the supplied value is not valid for { name }.",
ValueIsInvalidAccessor = value => "Unexpected InvalidValueWithUnknownModelErrorAccessor use", ValueIsInvalidAccessor = value => "Unexpected InvalidValueWithUnknownModelErrorAccessor use",
ValueMustBeANumberAccessor = name => "Unexpected ValueMustBeANumberAccessor use",
}; };
var bindingMetadataProvider = new DefaultBindingMetadataProvider(messageProvider); var bindingMetadataProvider = new DefaultBindingMetadataProvider(messageProvider);
var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider }); var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
@ -805,6 +806,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
(value, name) => $"Hmm, the value '{ value }' is not valid for { name }.", (value, name) => $"Hmm, the value '{ value }' is not valid for { name }.",
UnknownValueIsInvalidAccessor = name => "Unexpected InvalidValueWithUnknownAttemptedValueAccessor use", UnknownValueIsInvalidAccessor = name => "Unexpected InvalidValueWithUnknownAttemptedValueAccessor use",
ValueIsInvalidAccessor = value => "Unexpected InvalidValueWithUnknownModelErrorAccessor use", ValueIsInvalidAccessor = value => "Unexpected InvalidValueWithUnknownModelErrorAccessor use",
ValueMustBeANumberAccessor = name => "Unexpected ValueMustBeANumberAccessor use",
}; };
var bindingMetadataProvider = new DefaultBindingMetadataProvider(messageProvider); var bindingMetadataProvider = new DefaultBindingMetadataProvider(messageProvider);
var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider }); var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });

View File

@ -527,6 +527,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid, AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid,
UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid, UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid,
ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid, ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid,
ValueMustBeANumberAccessor = Resources.FormatHtmlGeneration_ValueMustBeNumber,
}; };
} }

View File

@ -1067,6 +1067,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
(value, name) => $"The value '{ value }' is not valid for { name }.", (value, name) => $"The value '{ value }' is not valid for { name }.",
UnknownValueIsInvalidAccessor = name => $"The supplied value is invalid for { name }.", UnknownValueIsInvalidAccessor = name => $"The supplied value is invalid for { name }.",
ValueIsInvalidAccessor = value => $"The value '{ value }' is invalid.", ValueIsInvalidAccessor = value => $"The value '{ value }' is invalid.",
ValueMustBeANumberAccessor = name => $"The field { name } must be a number.",
}; };
} }

View File

@ -37,6 +37,35 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
kvp => { Assert.Equal("data-val-number", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }); kvp => { Assert.Equal("data-val-number", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); });
} }
[Fact]
public void AddValidation_CorrectValidationTypeAndOverriddenErrorMessage()
{
// Arrange
var expectedMessage = "Error message about 'DisplayId' from override.";
var provider = new TestModelMetadataProvider();
provider
.ForProperty(typeof(TypeWithNumericProperty), nameof(TypeWithNumericProperty.Id))
.BindingDetails(d => d.ModelBindingMessageProvider.ValueMustBeANumberAccessor =
name => $"Error message about '{ name }' from override.");
var metadata = provider.GetMetadataForProperty(
typeof(TypeWithNumericProperty),
nameof(TypeWithNumericProperty.Id));
var adapter = new NumericClientModelValidator();
var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act
adapter.AddValidation(context);
// Assert
Assert.Collection(
context.Attributes,
kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
kvp => { Assert.Equal("data-val-number", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); });
}
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void AddValidation_DoesNotTrounceExistingAttributes() public void AddValidation_DoesNotTrounceExistingAttributes()

View File

@ -116,6 +116,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
AttemptedValueIsInvalidAccessor = (value, name) => $"The value '{ value }' is not valid for { name }.", AttemptedValueIsInvalidAccessor = (value, name) => $"The value '{ value }' is not valid for { name }.",
UnknownValueIsInvalidAccessor = name => $"The supplied value is invalid for { name }.", UnknownValueIsInvalidAccessor = name => $"The supplied value is invalid for { name }.",
ValueIsInvalidAccessor = value => $"The value '{ value }' is invalid.", ValueIsInvalidAccessor = value => $"The value '{ value }' is invalid.",
ValueMustBeANumberAccessor = name => $"The field { name } must be a number.",
}; };
} }

View File

@ -58,6 +58,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
return GetString("DisplayAttribute_Name"); return GetString("DisplayAttribute_Name");
} }
/// <summary>
/// Error about '{0}' from resources.
/// </summary>
internal static string RemoteAttribute_Error
{
get { return GetString("RemoteAttribute_Error"); }
}
/// <summary>
/// Error about '{0}' from resources.
/// </summary>
internal static string FormatRemoteAttribute_Error(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("RemoteAttribute_Error"), p0);
}
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -6,18 +6,22 @@ using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.WebEncoders.Testing; using Microsoft.Extensions.WebEncoders.Testing;
using Moq; using Moq;
using Xunit; using Xunit;
using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Test.Resources;
namespace Microsoft.AspNetCore.Mvc namespace Microsoft.AspNetCore.Mvc
{ {
@ -26,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc
private static readonly IModelMetadataProvider _metadataProvider = new EmptyModelMetadataProvider(); private static readonly IModelMetadataProvider _metadataProvider = new EmptyModelMetadataProvider();
private static readonly ModelMetadata _metadata = _metadataProvider.GetMetadataForProperty( private static readonly ModelMetadata _metadata = _metadataProvider.GetMetadataForProperty(
typeof(string), typeof(string),
"Length"); nameof(string.Length));
public static TheoryData<string> SomeNames public static TheoryData<string> SomeNames
{ {
@ -59,8 +63,8 @@ namespace Microsoft.AspNetCore.Mvc
public void IsValidAlwaysReturnsTrue() public void IsValidAlwaysReturnsTrue()
{ {
// Act & Assert // Act & Assert
Assert.True(new RemoteAttribute("RouteName", "ParameterName").IsValid(null)); Assert.True(new RemoteAttribute("RouteName", "ParameterName").IsValid(value: null));
Assert.True(new RemoteAttribute("ActionName", "ControllerName", "ParameterName").IsValid(null)); Assert.True(new RemoteAttribute("ActionName", "ControllerName", "ParameterName").IsValid(value: null));
} }
[Fact] [Fact]
@ -155,6 +159,70 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Null(attribute.RouteName); Assert.Null(attribute.RouteName);
} }
[Fact]
public void ErrorMessageProperties_HaveExpectedDefaultValues()
{
// Arrange & Act
var attribute = new RemoteAttribute("Action", "Controller");
// Assert
Assert.Null(attribute.ErrorMessage);
Assert.Null(attribute.ErrorMessageResourceName);
Assert.Null(attribute.ErrorMessageResourceType);
}
[Fact]
[ReplaceCulture]
public void FormatErrorMessage_ReturnsDefaultErrorMessage()
{
// Arrange
// See ViewFeatures.Resources.RemoteAttribute_RemoteValidationFailed.
var expected = "'Property1' is invalid.";
var attribute = new RemoteAttribute("Action", "Controller");
// Act
var message = attribute.FormatErrorMessage("Property1");
// Assert
Assert.Equal(expected, message);
}
[Fact]
public void FormatErrorMessage_UsesOverriddenErrorMessage()
{
// Arrange
var expected = "Error about 'Property1' from override.";
var attribute = new RemoteAttribute("Action", "Controller")
{
ErrorMessage = "Error about '{0}' from override.",
};
// Act
var message = attribute.FormatErrorMessage("Property1");
// Assert
Assert.Equal(expected, message);
}
[Fact]
[ReplaceCulture]
public void FormatErrorMessage_UsesErrorMessageFromResource()
{
// Arrange
var expected = "Error about 'Property1' from resources.";
var attribute = new RemoteAttribute("Action", "Controller")
{
ErrorMessageResourceName = nameof(Resources.RemoteAttribute_Error),
ErrorMessageResourceType = typeof(Resources),
};
// Act
var message = attribute.FormatErrorMessage("Property1");
// Assert
Assert.Equal(expected, message);
}
[Theory] [Theory]
[MemberData(nameof(NullOrEmptyNames))] [MemberData(nameof(NullOrEmptyNames))]
public void FormatAdditionalFieldsForClientValidation_WithInvalidPropertyName_Throws(string property) public void FormatAdditionalFieldsForClientValidation_WithInvalidPropertyName_Throws(string property)
@ -197,6 +265,7 @@ namespace Microsoft.AspNetCore.Mvc
} }
[Fact] [Fact]
[ReplaceCulture]
public void AddValidation_WithRoute_CallsUrlHelperWithExpectedValues() public void AddValidation_WithRoute_CallsUrlHelperWithExpectedValues()
{ {
// Arrange // Arrange
@ -222,6 +291,7 @@ namespace Microsoft.AspNetCore.Mvc
} }
[Fact] [Fact]
[ReplaceCulture]
public void AddValidation_WithActionController_CallsUrlHelperWithExpectedValues() public void AddValidation_WithActionController_CallsUrlHelperWithExpectedValues()
{ {
// Arrange // Arrange
@ -248,6 +318,7 @@ namespace Microsoft.AspNetCore.Mvc
} }
[Fact] [Fact]
[ReplaceCulture]
public void AddValidation_WithActionController_PropertiesSet_CallsUrlHelperWithExpectedValues() public void AddValidation_WithActionController_PropertiesSet_CallsUrlHelperWithExpectedValues()
{ {
// Arrange // Arrange
@ -283,6 +354,294 @@ namespace Microsoft.AspNetCore.Mvc
} }
[Fact] [Fact]
public void AddValidation_WithErrorMessage_SetsAttributesAsExpected()
{
// Arrange
var expected = "Error about 'Length' from override.";
var attribute = new RemoteAttribute("Action", "Controller")
{
HttpMethod = "POST",
ErrorMessage = "Error about '{0}' from override.",
};
var url = "/Controller/Action";
var context = GetValidationContext(url);
// Act
attribute.AddValidation(context);
// Assert
Assert.Collection(
context.Attributes,
kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
kvp =>
{
Assert.Equal("data-val-remote", kvp.Key);
Assert.Equal(expected, kvp.Value);
},
kvp =>
{
Assert.Equal("data-val-remote-additionalfields", kvp.Key);
Assert.Equal("*.Length", kvp.Value);
},
kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
}
[Fact]
public void AddValidation_WithErrorMessageAndLocalizerFactory_SetsAttributesAsExpected()
{
// Arrange
var expected = "Error about 'Length' from override.";
var attribute = new RemoteAttribute("Action", "Controller")
{
HttpMethod = "POST",
ErrorMessage = "Error about '{0}' from override.",
};
var url = "/Controller/Action";
var context = GetValidationContextWithLocalizerFactory(url);
// Act
attribute.AddValidation(context);
// Assert
Assert.Collection(
context.Attributes,
kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
kvp =>
{
// IStringLocalizerFactory existence alone is insufficient to change error message.
Assert.Equal("data-val-remote", kvp.Key);
Assert.Equal(expected, kvp.Value);
},
kvp =>
{
Assert.Equal("data-val-remote-additionalfields", kvp.Key);
Assert.Equal("*.Length", kvp.Value);
},
kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
}
[Fact]
public void AddValidation_WithErrorMessageAndLocalizerProvider_SetsAttributesAsExpected()
{
// Arrange
var expected = "Error about 'Length' from override.";
var attribute = new RemoteAttribute("Action", "Controller")
{
HttpMethod = "POST",
ErrorMessage = "Error about '{0}' from override.",
};
var url = "/Controller/Action";
var context = GetValidationContext(url);
var options = context.ActionContext.HttpContext.RequestServices
.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>();
var localizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object;
// Act
attribute.AddValidation(context);
// Assert
Assert.Collection(
context.Attributes,
kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
kvp =>
{
// Non-null DataAnnotationLocalizerProvider alone is insufficient to change error message.
Assert.Equal("data-val-remote", kvp.Key);
Assert.Equal(expected, kvp.Value);
},
kvp =>
{
Assert.Equal("data-val-remote-additionalfields", kvp.Key);
Assert.Equal("*.Length", kvp.Value);
},
kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
}
[Fact]
public void AddValidation_WithErrorMessageLocalizerFactoryAndLocalizerProvider_SetsAttributesAsExpected()
{
// Arrange
var expected = "Error about 'Length' from localizer.";
var attribute = new RemoteAttribute("Action", "Controller")
{
HttpMethod = "POST",
ErrorMessage = "Error about '{0}' from override.",
};
var url = "/Controller/Action";
var context = GetValidationContextWithLocalizerFactory(url);
var localizedString = new LocalizedString("Fred", expected);
var localizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
localizer
.Setup(l => l["Error about '{0}' from override.", "Length"])
.Returns(localizedString)
.Verifiable();
var options = context.ActionContext.HttpContext.RequestServices
.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>();
options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object;
// Act
attribute.AddValidation(context);
// Assert
localizer.VerifyAll();
Assert.Collection(
context.Attributes,
kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
kvp =>
{
Assert.Equal("data-val-remote", kvp.Key);
Assert.Equal(expected, kvp.Value);
},
kvp =>
{
Assert.Equal("data-val-remote-additionalfields", kvp.Key);
Assert.Equal("*.Length", kvp.Value);
},
kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
}
[Fact]
[ReplaceCulture]
public void AddValidation_WithErrorResourcesLocalizerFactoryAndLocalizerProvider_SetsAttributesAsExpected()
{
// Arrange
var expected = "Error about 'Length' from resources.";
var attribute = new RemoteAttribute("Action", "Controller")
{
HttpMethod = "POST",
ErrorMessageResourceName = nameof(Resources.RemoteAttribute_Error),
ErrorMessageResourceType = typeof(Resources),
};
var url = "/Controller/Action";
var context = GetValidationContextWithLocalizerFactory(url);
var localizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
var options = context.ActionContext.HttpContext.RequestServices
.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>();
options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object;
// Act
attribute.AddValidation(context);
// Assert
Assert.Collection(
context.Attributes,
kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
kvp =>
{
// Configuring the attribute using ErrorMessageResource* trumps available IStringLocalizer etc.
Assert.Equal("data-val-remote", kvp.Key);
Assert.Equal(expected, kvp.Value);
},
kvp =>
{
Assert.Equal("data-val-remote-additionalfields", kvp.Key);
Assert.Equal("*.Length", kvp.Value);
},
kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
}
[Fact]
public void AddValidation_WithErrorMessageAndDisplayName_SetsAttributesAsExpected()
{
// Arrange
var expected = "Error about 'Display Length' from override.";
var attribute = new RemoteAttribute("Action", "Controller")
{
HttpMethod = "POST",
ErrorMessage = "Error about '{0}' from override.",
};
var url = "/Controller/Action";
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty(typeof(string), nameof(string.Length))
.DisplayDetails(d => d.DisplayName = () => "Display Length");
var context = GetValidationContext(url, metadataProvider);
// Act
attribute.AddValidation(context);
// Assert
Assert.Collection(
context.Attributes,
kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
kvp =>
{
Assert.Equal("data-val-remote", kvp.Key);
Assert.Equal(expected, kvp.Value);
},
kvp =>
{
Assert.Equal("data-val-remote-additionalfields", kvp.Key);
Assert.Equal("*.Length", kvp.Value);
},
kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
}
[Fact]
public void AddValidation_WithErrorMessageLocalizerFactoryLocalizerProviderAndDisplayName_SetsAttributesAsExpected()
{
// Arrange
var expected = "Error about 'Length' from localizer.";
var attribute = new RemoteAttribute("Action", "Controller")
{
HttpMethod = "POST",
ErrorMessage = "Error about '{0}' from override.",
};
var url = "/Controller/Action";
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty(typeof(string), nameof(string.Length))
.DisplayDetails(d => d.DisplayName = () => "Display Length");
var context = GetValidationContextWithLocalizerFactory(url, metadataProvider);
var localizedString = new LocalizedString("Fred", expected);
var localizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
localizer
.Setup(l => l["Error about '{0}' from override.", "Display Length"])
.Returns(localizedString)
.Verifiable();
var options = context.ActionContext.HttpContext.RequestServices
.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>();
options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object;
// Act
attribute.AddValidation(context);
// Assert
localizer.VerifyAll();
Assert.Collection(
context.Attributes,
kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
kvp =>
{
Assert.Equal("data-val-remote", kvp.Key);
Assert.Equal(expected, kvp.Value);
},
kvp =>
{
Assert.Equal("data-val-remote-additionalfields", kvp.Key);
Assert.Equal("*.Length", kvp.Value);
},
kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); },
kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); });
}
[Fact]
[ReplaceCulture]
public void AddValidation_WithActionControllerArea_CallsUrlHelperWithExpectedValues() public void AddValidation_WithActionControllerArea_CallsUrlHelperWithExpectedValues()
{ {
// Arrange // Arrange
@ -319,6 +678,7 @@ namespace Microsoft.AspNetCore.Mvc
// Root area is current in this case. // Root area is current in this case.
[Fact] [Fact]
[ReplaceCulture]
public void AddValidation_WithActionController_FindsControllerInCurrentArea() public void AddValidation_WithActionController_FindsControllerInCurrentArea()
{ {
// Arrange // Arrange
@ -343,6 +703,7 @@ namespace Microsoft.AspNetCore.Mvc
// Test area is current in this case. // Test area is current in this case.
[Fact] [Fact]
[ReplaceCulture]
public void AddValidation_WithActionControllerInArea_FindsControllerInCurrentArea() public void AddValidation_WithActionControllerInArea_FindsControllerInCurrentArea()
{ {
// Arrange // Arrange
@ -368,6 +729,7 @@ namespace Microsoft.AspNetCore.Mvc
// Explicit reference to the (current) root area. // Explicit reference to the (current) root area.
[Theory] [Theory]
[MemberData(nameof(NullOrEmptyNames))] [MemberData(nameof(NullOrEmptyNames))]
[ReplaceCulture]
public void AddValidation_WithActionControllerArea_FindsControllerInRootArea(string areaName) public void AddValidation_WithActionControllerArea_FindsControllerInRootArea(string areaName)
{ {
// Arrange // Arrange
@ -393,6 +755,7 @@ namespace Microsoft.AspNetCore.Mvc
// Test area is current in this case. // Test area is current in this case.
[Theory] [Theory]
[MemberData(nameof(NullOrEmptyNames))] [MemberData(nameof(NullOrEmptyNames))]
[ReplaceCulture]
public void AddValidation_WithActionControllerAreaInArea_FindsControllerInRootArea(string areaName) public void AddValidation_WithActionControllerAreaInArea_FindsControllerInRootArea(string areaName)
{ {
// Arrange // Arrange
@ -417,6 +780,7 @@ namespace Microsoft.AspNetCore.Mvc
// Root area is current in this case. // Root area is current in this case.
[Fact] [Fact]
[ReplaceCulture]
public void AddValidation_WithActionControllerArea_FindsControllerInNamedArea() public void AddValidation_WithActionControllerArea_FindsControllerInNamedArea()
{ {
// Arrange // Arrange
@ -441,6 +805,7 @@ namespace Microsoft.AspNetCore.Mvc
// Explicit reference to the current (Test) area. // Explicit reference to the current (Test) area.
[Fact] [Fact]
[ReplaceCulture]
public void AddValidation_WithActionControllerAreaInArea_FindsControllerInNamedArea() public void AddValidation_WithActionControllerAreaInArea_FindsControllerInNamedArea()
{ {
// Arrange // Arrange
@ -465,6 +830,7 @@ namespace Microsoft.AspNetCore.Mvc
// Test area is current in this case. // Test area is current in this case.
[Fact] [Fact]
[ReplaceCulture]
public void AddValidation_WithActionControllerAreaInArea_FindsControllerInDifferentArea() public void AddValidation_WithActionControllerAreaInArea_FindsControllerInDifferentArea()
{ {
// Arrange // Arrange
@ -518,35 +884,59 @@ namespace Microsoft.AspNetCore.Mvc
kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal("original", kvp.Value); }); kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal("original", kvp.Value); });
} }
private static ClientModelValidationContext GetValidationContext(IUrlHelper urlHelper) private static ClientModelValidationContext GetValidationContext(
string url,
IModelMetadataProvider metadataProvider = null)
{ {
var factory = new Mock<IUrlHelperFactory>(); var urlHelper = new MockUrlHelper(url, routeName: null);
return GetValidationContext(urlHelper, localizerFactory: null, metadataProvider: metadataProvider);
}
private static ClientModelValidationContext GetValidationContextWithLocalizerFactory(
string url,
IModelMetadataProvider metadataProvider = null)
{
var urlHelper = new MockUrlHelper(url, routeName: null);
var localizerFactory = new Mock<IStringLocalizerFactory>(MockBehavior.Strict);
return GetValidationContext(urlHelper, localizerFactory.Object, metadataProvider);
}
private static ClientModelValidationContext GetValidationContext(
IUrlHelper urlHelper,
IStringLocalizerFactory localizerFactory = null,
IModelMetadataProvider metadataProvider = null)
{
var serviceCollection = GetServiceCollection(localizerFactory);
var factory = new Mock<IUrlHelperFactory>(MockBehavior.Strict);
serviceCollection.AddSingleton<IUrlHelperFactory>(factory.Object);
var serviceProvider = serviceCollection.BuildServiceProvider();
var actionContext = GetActionContext(serviceProvider, routeData: null);
factory factory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>())) .Setup(f => f.GetUrlHelper(actionContext))
.Returns(urlHelper); .Returns(urlHelper);
var serviceCollection = GetServiceCollection(); var metadata = _metadata;
serviceCollection.AddSingleton<IUrlHelperFactory>(factory.Object); if (metadataProvider == null)
var serviceProvider = serviceCollection.BuildServiceProvider();
var actionContext = new ActionContext()
{ {
HttpContext = new DefaultHttpContext() metadataProvider = _metadataProvider;
{ }
RequestServices = serviceProvider, else
}, {
}; metadata = metadataProvider.GetMetadataForProperty(typeof(string), nameof(string.Length));
}
return new ClientModelValidationContext( return new ClientModelValidationContext(
actionContext, actionContext,
_metadata, metadata,
_metadataProvider, metadataProvider,
new AttributeDictionary()); new AttributeDictionary());
} }
private static ClientModelValidationContext GetValidationContextWithArea(string currentArea) private static ClientModelValidationContext GetValidationContextWithArea(string currentArea)
{ {
var serviceCollection = GetServiceCollection(); var serviceCollection = GetServiceCollection(localizerFactory: null);
var serviceProvider = serviceCollection.BuildServiceProvider(); var serviceProvider = serviceCollection.BuildServiceProvider();
var routeCollection = GetRouteCollectionWithArea(serviceProvider); var routeCollection = GetRouteCollectionWithArea(serviceProvider);
var routeData = new RouteData var routeData = new RouteData
@ -566,24 +956,18 @@ namespace Microsoft.AspNetCore.Mvc
routeData.Values["area"] = currentArea; routeData.Values["area"] = currentArea;
} }
var context = GetActionContext(serviceProvider, routeData); var actionContext = GetActionContext(serviceProvider, routeData);
var urlHelper = new UrlHelper(context); var urlHelper = new UrlHelper(actionContext);
var factory = new Mock<IUrlHelperFactory>(); var factory = new Mock<IUrlHelperFactory>(MockBehavior.Strict);
factory factory
.Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>())) .Setup(f => f.GetUrlHelper(actionContext))
.Returns(urlHelper); .Returns(urlHelper);
// Make an IUrlHelperFactory available through the ActionContext.
serviceCollection.AddSingleton<IUrlHelperFactory>(factory.Object); serviceCollection.AddSingleton<IUrlHelperFactory>(factory.Object);
serviceProvider = serviceCollection.BuildServiceProvider(); serviceProvider = serviceCollection.BuildServiceProvider();
actionContext.HttpContext.RequestServices = serviceProvider;
var actionContext = new ActionContext()
{
HttpContext = new DefaultHttpContext()
{
RequestServices = serviceProvider,
},
};
return new ClientModelValidationContext( return new ClientModelValidationContext(
actionContext, actionContext,
@ -615,7 +999,7 @@ namespace Microsoft.AspNetCore.Mvc
private static RouteBuilder GetRouteBuilder(IServiceProvider serviceProvider) private static RouteBuilder GetRouteBuilder(IServiceProvider serviceProvider)
{ {
var app = new Mock<IApplicationBuilder>(); var app = new Mock<IApplicationBuilder>(MockBehavior.Strict);
app app
.SetupGet(a => a.ApplicationServices) .SetupGet(a => a.ApplicationServices)
.Returns(serviceProvider); .Returns(serviceProvider);
@ -631,9 +1015,7 @@ namespace Microsoft.AspNetCore.Mvc
return builder; return builder;
} }
private static ActionContext GetActionContext( private static ActionContext GetActionContext(IServiceProvider serviceProvider, RouteData routeData)
IServiceProvider serviceProvider,
RouteData routeData = null)
{ {
// Set IServiceProvider properties because TemplateRoute gets services (e.g. an ILoggerFactory instance) // Set IServiceProvider properties because TemplateRoute gets services (e.g. an ILoggerFactory instance)
// through the HttpContext. // through the HttpContext.
@ -653,23 +1035,22 @@ namespace Microsoft.AspNetCore.Mvc
return new ActionContext(httpContext, routeData, new ActionDescriptor()); return new ActionContext(httpContext, routeData, new ActionDescriptor());
} }
private static ServiceCollection GetServiceCollection() private static ServiceCollection GetServiceCollection(IStringLocalizerFactory localizerFactory)
{ {
var serviceCollection = new ServiceCollection(); var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<ILoggerFactory>(new NullLoggerFactory()); serviceCollection.AddSingleton<ILoggerFactory>(new NullLoggerFactory());
serviceCollection.AddSingleton<UrlEncoder>(new UrlTestEncoder()); serviceCollection.AddSingleton<UrlEncoder>(new UrlTestEncoder());
var routeOptions = new RouteOptions(); serviceCollection.AddOptions();
var accessor = new Mock<IOptions<RouteOptions>>();
accessor
.SetupGet(options => options.Value)
.Returns(routeOptions);
serviceCollection.AddSingleton<IOptions<RouteOptions>>(accessor.Object);
serviceCollection.AddRouting(); serviceCollection.AddRouting();
serviceCollection.AddSingleton<IInlineConstraintResolver>( serviceCollection.AddSingleton<IInlineConstraintResolver>(
new DefaultInlineConstraintResolver(accessor.Object)); provider => new DefaultInlineConstraintResolver(provider.GetRequiredService<IOptions<RouteOptions>>()));
if (localizerFactory != null)
{
serviceCollection.AddSingleton<IStringLocalizerFactory>(localizerFactory);
}
return serviceCollection; return serviceCollection;
} }

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@ -126,4 +126,7 @@
<data name="DisplayAttribute_Name" xml:space="preserve"> <data name="DisplayAttribute_Name" xml:space="preserve">
<value>name from resources</value> <value>name from resources</value>
</data> </data>
<data name="RemoteAttribute_Error" xml:space="preserve">
<value>Error about '{0}' from resources.</value>
</data>
</root> </root>