Merge pull request #8491 from dotnet-maestro-bot/merge/release/2.2-to-master

[automated] Merge branch 'release/2.2' => 'master'
This commit is contained in:
Doug Bunting 2018-09-21 15:40:49 -07:00 committed by GitHub
commit 90089953d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 843 additions and 126 deletions

View File

@ -14,7 +14,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// Error message the model binding system adds when a property with an associated /// Error message the model binding system adds when a property with an associated
/// <c>BindRequiredAttribute</c> is not bound. /// <c>BindRequiredAttribute</c> is not bound.
/// </summary> /// </summary>
/// <value>Default <see cref="string"/> is "A value for the '{0}' property was not provided.".</value> /// <value>
/// Default <see cref="string"/> is "A value for the '{0}' parameter or property was not provided.".
/// </value>
public virtual Func<string, string> MissingBindRequiredValueAccessor { get; } public virtual Func<string, string> MissingBindRequiredValueAccessor { get; }
/// <summary> /// <summary>

View File

@ -38,11 +38,35 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
/// The <see cref="IModelBinder"/> for binding <typeparamref name="TElement"/>. /// The <see cref="IModelBinder"/> for binding <typeparamref name="TElement"/>.
/// </param> /// </param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param> /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <remarks>
/// The binder will not add an error for an unbound top-level model even if
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/>.
/// </remarks>
public ArrayModelBinder(IModelBinder elementBinder, ILoggerFactory loggerFactory) public ArrayModelBinder(IModelBinder elementBinder, ILoggerFactory loggerFactory)
: base(elementBinder, loggerFactory) : base(elementBinder, loggerFactory)
{ {
} }
/// <summary>
/// Creates a new <see cref="ArrayModelBinder{TElement}"/>.
/// </summary>
/// <param name="elementBinder">
/// The <see cref="IModelBinder"/> for binding <typeparamref name="TElement"/>.
/// </param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="allowValidatingTopLevelNodes">
/// Indication that validation of top-level models is enabled. If <see langword="true"/> and
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/> for a top-level model, the binder
/// adds a <see cref="ModelStateDictionary"/> error when the model is not bound.
/// </param>
public ArrayModelBinder(
IModelBinder elementBinder,
ILoggerFactory loggerFactory,
bool allowValidatingTopLevelNodes)
: base(elementBinder, loggerFactory, allowValidatingTopLevelNodes)
{
}
/// <inheritdoc /> /// <inheritdoc />
public override bool CanCreateInstance(Type targetType) public override bool CanCreateInstance(Type targetType)
{ {

View File

@ -4,6 +4,7 @@
using System; using System;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
@ -27,7 +28,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var binderType = typeof(ArrayModelBinder<>).MakeGenericType(elementType); var binderType = typeof(ArrayModelBinder<>).MakeGenericType(elementType);
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>(); var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
return (IModelBinder)Activator.CreateInstance(binderType, elementBinder, loggerFactory); var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
return (IModelBinder)Activator.CreateInstance(
binderType,
elementBinder,
loggerFactory,
mvcOptions.AllowValidatingTopLevelNodes);
} }
return null; return null;

View File

@ -44,7 +44,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
/// </summary> /// </summary>
/// <param name="elementBinder">The <see cref="IModelBinder"/> for binding elements.</param> /// <param name="elementBinder">The <see cref="IModelBinder"/> for binding elements.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param> /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <remarks>
/// The binder will not add an error for an unbound top-level model even if
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/>.
/// </remarks>
public CollectionModelBinder(IModelBinder elementBinder, ILoggerFactory loggerFactory) public CollectionModelBinder(IModelBinder elementBinder, ILoggerFactory loggerFactory)
: this(elementBinder, loggerFactory, allowValidatingTopLevelNodes: false)
{
}
/// <summary>
/// Creates a new <see cref="CollectionModelBinder{TElement}"/>.
/// </summary>
/// <param name="elementBinder">The <see cref="IModelBinder"/> for binding elements.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="allowValidatingTopLevelNodes">
/// Indication that validation of top-level models is enabled. If <see langword="true"/> and
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/> for a top-level model, the binder
/// adds a <see cref="ModelStateDictionary"/> error when the model is not bound.
/// </param>
public CollectionModelBinder(
IModelBinder elementBinder,
ILoggerFactory loggerFactory,
bool allowValidatingTopLevelNodes)
{ {
if (elementBinder == null) if (elementBinder == null)
{ {
@ -58,8 +80,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
ElementBinder = elementBinder; ElementBinder = elementBinder;
Logger = loggerFactory.CreateLogger(GetType()); Logger = loggerFactory.CreateLogger(GetType());
AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
} }
// Internal for testing.
internal bool AllowValidatingTopLevelNodes { get; }
/// <summary> /// <summary>
/// Gets the <see cref="IModelBinder"/> instances for binding collection elements. /// Gets the <see cref="IModelBinder"/> instances for binding collection elements.
/// </summary> /// </summary>
@ -94,6 +120,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
model = CreateEmptyCollection(bindingContext.ModelType); model = CreateEmptyCollection(bindingContext.ModelType);
} }
if (AllowValidatingTopLevelNodes)
{
AddErrorIfBindingRequired(bindingContext);
}
bindingContext.Result = ModelBindingResult.Success(model); bindingContext.Result = ModelBindingResult.Success(model);
} }
@ -161,6 +192,34 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
typeof(ICollection<TElement>).IsAssignableFrom(targetType); typeof(ICollection<TElement>).IsAssignableFrom(targetType);
} }
/// <summary>
/// Add a <see cref="ModelError" /> to <see cref="ModelBindingContext.ModelState" /> if
/// <see cref="ModelMetadata.IsBindingRequired" />.
/// </summary>
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
/// <remarks>
/// <para>
/// This method should be called only when <see cref="MvcOptions.AllowValidatingTopLevelNodes" /> is
/// <see langword="true" /> and a top-level model was not bound.
/// </para>
/// <para>
/// For back-compatibility reasons, <see cref="ModelBindingContext.Result" /> must have
/// <see cref="ModelBindingResult.IsModelSet" /> equal to <see langword="true" /> when a
/// top-level model is not bound. Therefore, ParameterBinder can not detect a
/// <see cref="ModelMetadata.IsBindingRequired" /> failure for collections. Add the error here.
/// </para>
/// </remarks>
protected void AddErrorIfBindingRequired(ModelBindingContext bindingContext)
{
var modelMetadata = bindingContext.ModelMetadata;
if (modelMetadata.IsBindingRequired)
{
var messageProvider = modelMetadata.ModelBindingMessageProvider;
var message = messageProvider.MissingBindRequiredValueAccessor(bindingContext.FieldName);
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message);
}
}
/// <summary> /// <summary>
/// Create an <see cref="object"/> assignable to <paramref name="targetType"/>. /// Create an <see cref="object"/> assignable to <paramref name="targetType"/>.
/// </summary> /// </summary>

View File

@ -7,6 +7,7 @@ using System.Reflection;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
@ -32,7 +33,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
} }
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>(); var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
// If the model type is ICollection<> then we can call its Add method, so we can always support it. // If the model type is ICollection<> then we can call its Add method, so we can always support it.
var collectionType = ClosedGenericMatcher.ExtractGenericInterface(modelType, typeof(ICollection<>)); var collectionType = ClosedGenericMatcher.ExtractGenericInterface(modelType, typeof(ICollection<>));
if (collectionType != null) if (collectionType != null)
@ -41,7 +43,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var elementBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(elementType)); var elementBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(elementType));
var binderType = typeof(CollectionModelBinder<>).MakeGenericType(collectionType.GenericTypeArguments); var binderType = typeof(CollectionModelBinder<>).MakeGenericType(collectionType.GenericTypeArguments);
return (IModelBinder)Activator.CreateInstance(binderType, elementBinder, loggerFactory); return (IModelBinder)Activator.CreateInstance(
binderType,
elementBinder,
loggerFactory,
mvcOptions.AllowValidatingTopLevelNodes);
} }
// If the model type is IEnumerable<> then we need to know if we can assign a List<> to it, since // If the model type is IEnumerable<> then we need to know if we can assign a List<> to it, since
@ -57,7 +63,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var elementBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(elementType)); var elementBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(elementType));
var binderType = typeof(CollectionModelBinder<>).MakeGenericType(enumerableType.GenericTypeArguments); var binderType = typeof(CollectionModelBinder<>).MakeGenericType(enumerableType.GenericTypeArguments);
return (IModelBinder)Activator.CreateInstance(binderType, elementBinder, loggerFactory); return (IModelBinder)Activator.CreateInstance(
binderType,
elementBinder,
loggerFactory,
mvcOptions.AllowValidatingTopLevelNodes);
} }
} }

View File

@ -45,9 +45,33 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
/// The <see cref="IDictionary{TKey, TValue}"/> of binders to use for binding properties. /// The <see cref="IDictionary{TKey, TValue}"/> of binders to use for binding properties.
/// </param> /// </param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param> /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <remarks>
/// The binder will not add an error for an unbound top-level model even if
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/>.
/// </remarks>
public ComplexTypeModelBinder( public ComplexTypeModelBinder(
IDictionary<ModelMetadata, IModelBinder> propertyBinders, IDictionary<ModelMetadata, IModelBinder> propertyBinders,
ILoggerFactory loggerFactory) ILoggerFactory loggerFactory)
: this(propertyBinders, loggerFactory, allowValidatingTopLevelNodes: false)
{
}
/// <summary>
/// Creates a new <see cref="ComplexTypeModelBinder"/>.
/// </summary>
/// <param name="propertyBinders">
/// The <see cref="IDictionary{TKey, TValue}"/> of binders to use for binding properties.
/// </param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="allowValidatingTopLevelNodes">
/// Indication that validation of top-level models is enabled. If <see langword="true"/> and
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/> for a top-level model, the binder
/// adds a <see cref="ModelStateDictionary"/> error when the model is not bound.
/// </param>
public ComplexTypeModelBinder(
IDictionary<ModelMetadata, IModelBinder> propertyBinders,
ILoggerFactory loggerFactory,
bool allowValidatingTopLevelNodes)
{ {
if (propertyBinders == null) if (propertyBinders == null)
{ {
@ -61,8 +85,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
_propertyBinders = propertyBinders; _propertyBinders = propertyBinders;
_logger = loggerFactory.CreateLogger<ComplexTypeModelBinder>(); _logger = loggerFactory.CreateLogger<ComplexTypeModelBinder>();
AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
} }
// Internal for testing.
internal bool AllowValidatingTopLevelNodes { get; }
/// <inheritdoc /> /// <inheritdoc />
public Task BindModelAsync(ModelBindingContext bindingContext) public Task BindModelAsync(ModelBindingContext bindingContext)
{ {
@ -91,9 +119,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
bindingContext.Model = CreateModel(bindingContext); bindingContext.Model = CreateModel(bindingContext);
} }
for (var i = 0; i < bindingContext.ModelMetadata.Properties.Count; i++) var modelMetadata = bindingContext.ModelMetadata;
var attemptedPropertyBinding = false;
for (var i = 0; i < modelMetadata.Properties.Count; i++)
{ {
var property = bindingContext.ModelMetadata.Properties[i]; var property = modelMetadata.Properties[i];
if (!CanBindProperty(bindingContext, property)) if (!CanBindProperty(bindingContext, property))
{ {
continue; continue;
@ -127,15 +157,32 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
if (result.IsModelSet) if (result.IsModelSet)
{ {
attemptedPropertyBinding = true;
SetProperty(bindingContext, modelName, property, result); SetProperty(bindingContext, modelName, property, result);
} }
else if (property.IsBindingRequired) else if (property.IsBindingRequired)
{ {
attemptedPropertyBinding = true;
var message = property.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(fieldName); var message = property.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(fieldName);
bindingContext.ModelState.TryAddModelError(modelName, message); bindingContext.ModelState.TryAddModelError(modelName, message);
} }
} }
// Have we created a top-level model despite an inability to bind anything in said model and a lack of
// other IsBindingRequired errors? Does that violate [BindRequired] on the model? This case occurs when
// 1. The top-level model has no public settable properties.
// 2. All properties in a [BindRequired] model have [BindNever] or are otherwise excluded from binding.
// 3. No data exists for any property.
if (AllowValidatingTopLevelNodes &&
!attemptedPropertyBinding &&
bindingContext.IsTopLevelObject &&
modelMetadata.IsBindingRequired)
{
var messageProvider = modelMetadata.ModelBindingMessageProvider;
var message = messageProvider.MissingBindRequiredValueAccessor(bindingContext.FieldName);
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message);
}
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model); bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
_logger.DoneAttemptingToBindModel(bindingContext); _logger.DoneAttemptingToBindModel(bindingContext);
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
@ -31,7 +32,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
} }
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>(); var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
return new ComplexTypeModelBinder(propertyBinders, loggerFactory); var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
return new ComplexTypeModelBinder(
propertyBinders,
loggerFactory,
mvcOptions.AllowValidatingTopLevelNodes);
} }
return null; return null;

View File

@ -43,6 +43,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
/// <param name="keyBinder">The <see cref="IModelBinder"/> for <typeparamref name="TKey"/>.</param> /// <param name="keyBinder">The <see cref="IModelBinder"/> for <typeparamref name="TKey"/>.</param>
/// <param name="valueBinder">The <see cref="IModelBinder"/> for <typeparamref name="TValue"/>.</param> /// <param name="valueBinder">The <see cref="IModelBinder"/> for <typeparamref name="TValue"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param> /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <remarks>
/// The binder will not add an error for an unbound top-level model even if
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/>.
/// </remarks>
public DictionaryModelBinder(IModelBinder keyBinder, IModelBinder valueBinder, ILoggerFactory loggerFactory) public DictionaryModelBinder(IModelBinder keyBinder, IModelBinder valueBinder, ILoggerFactory loggerFactory)
: base(new KeyValuePairModelBinder<TKey, TValue>(keyBinder, valueBinder, loggerFactory), loggerFactory) : base(new KeyValuePairModelBinder<TKey, TValue>(keyBinder, valueBinder, loggerFactory), loggerFactory)
{ {
@ -54,6 +58,40 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
_valueBinder = valueBinder; _valueBinder = valueBinder;
} }
/// <summary>
/// Creates a new <see cref="DictionaryModelBinder{TKey, TValue}"/>.
/// </summary>
/// <param name="keyBinder">The <see cref="IModelBinder"/> for <typeparamref name="TKey"/>.</param>
/// <param name="valueBinder">The <see cref="IModelBinder"/> for <typeparamref name="TValue"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="allowValidatingTopLevelNodes">
/// Indication that validation of top-level models is enabled. If <see langword="true"/> and
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/> for a top-level model, the binder
/// adds a <see cref="ModelStateDictionary"/> error when the model is not bound.
/// </param>
public DictionaryModelBinder(
IModelBinder keyBinder,
IModelBinder valueBinder,
ILoggerFactory loggerFactory,
bool allowValidatingTopLevelNodes)
: base(
new KeyValuePairModelBinder<TKey, TValue>(keyBinder, valueBinder, loggerFactory),
loggerFactory,
// CollectionModelBinder should not check IsRequired, done in this model binder.
allowValidatingTopLevelNodes: false)
{
if (valueBinder == null)
{
throw new ArgumentNullException(nameof(valueBinder));
}
_valueBinder = valueBinder;
AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
}
// Internal for testing.
internal new bool AllowValidatingTopLevelNodes { get; }
/// <inheritdoc /> /// <inheritdoc />
public override async Task BindModelAsync(ModelBindingContext bindingContext) public override async Task BindModelAsync(ModelBindingContext bindingContext)
{ {
@ -85,6 +123,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
// No IEnumerableValueProvider available for the fallback approach. For example the user may have // No IEnumerableValueProvider available for the fallback approach. For example the user may have
// replaced the ValueProvider with something other than a CompositeValueProvider. // replaced the ValueProvider with something other than a CompositeValueProvider.
if (AllowValidatingTopLevelNodes && bindingContext.IsTopLevelObject)
{
AddErrorIfBindingRequired(bindingContext);
}
return; return;
} }
@ -94,6 +137,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
if (keys.Count == 0) if (keys.Count == 0)
{ {
// No entries with the expected keys. // No entries with the expected keys.
if (AllowValidatingTopLevelNodes && bindingContext.IsTopLevelObject)
{
AddErrorIfBindingRequired(bindingContext);
}
return; return;
} }

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
@ -34,7 +35,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var binderType = typeof(DictionaryModelBinder<,>).MakeGenericType(dictionaryType.GenericTypeArguments); var binderType = typeof(DictionaryModelBinder<,>).MakeGenericType(dictionaryType.GenericTypeArguments);
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>(); var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
return (IModelBinder)Activator.CreateInstance(binderType, keyBinder, valueBinder, loggerFactory); var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
return (IModelBinder)Activator.CreateInstance(
binderType,
keyBinder,
valueBinder,
loggerFactory,
mvcOptions.AllowValidatingTopLevelNodes);
} }
return null; return null;

View File

@ -795,7 +795,7 @@ namespace Microsoft.AspNetCore.Mvc.Core
=> GetString("ModelBinderUtil_ModelMetadataCannotBeNull"); => GetString("ModelBinderUtil_ModelMetadataCannotBeNull");
/// <summary> /// <summary>
/// A value for the '{0}' property was not provided. /// A value for the '{0}' parameter or property was not provided.
/// </summary> /// </summary>
internal static string ModelBinding_MissingBindRequiredMember internal static string ModelBinding_MissingBindRequiredMember
{ {
@ -803,7 +803,7 @@ namespace Microsoft.AspNetCore.Mvc.Core
} }
/// <summary> /// <summary>
/// A value for the '{0}' property was not provided. /// A value for the '{0}' parameter or property was not provided.
/// </summary> /// </summary>
internal static string FormatModelBinding_MissingBindRequiredMember(object p0) internal static string FormatModelBinding_MissingBindRequiredMember(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("ModelBinding_MissingBindRequiredMember"), p0); => string.Format(CultureInfo.CurrentCulture, GetString("ModelBinding_MissingBindRequiredMember"), p0);

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.
--> -->
@ -295,7 +295,7 @@
<value>The binding context cannot have a null ModelMetadata.</value> <value>The binding context cannot have a null ModelMetadata.</value>
</data> </data>
<data name="ModelBinding_MissingBindRequiredMember" xml:space="preserve"> <data name="ModelBinding_MissingBindRequiredMember" xml:space="preserve">
<value>A value for the '{0}' property was not provided.</value> <value>A value for the '{0}' parameter or property was not provided.</value>
</data> </data>
<data name="ModelBinding_MissingRequestBodyRequiredMember" xml:space="preserve"> <data name="ModelBinding_MissingRequestBodyRequiredMember" xml:space="preserve">
<value>A non-empty request body is required.</value> <value>A non-empty request body is required.</value>

View File

@ -51,6 +51,31 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.IsType(typeof(ArrayModelBinder<>).MakeGenericType(modelType.GetElementType()), result); Assert.IsType(typeof(ArrayModelBinder<>).MakeGenericType(modelType.GetElementType()), result);
} }
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Create_ForArrayType_ReturnsBinder_WithExpectedAllowValidatingTopLevelNodes(
bool allowValidatingTopLevelNodes)
{
// Arrange
var provider = new ArrayModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(int[]));
context.MvcOptions.AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
context.OnCreatingBinder(m =>
{
Assert.Equal(typeof(int), m.ModelType);
return Mock.Of<IModelBinder>();
});
// Act
var result = provider.GetBinder(context);
// Assert
var binder = Assert.IsType<ArrayModelBinder<int>>(result);
Assert.Equal(allowValidatingTopLevelNodes, binder.AllowValidatingTopLevelNodes);
}
[Fact] [Fact]
public void Create_ForModelMetadataReadOnly_ReturnsNull() public void Create_ForModelMetadataReadOnly_ReturnsNull()
{ {

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
@ -42,13 +43,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.Equal(new[] { 42, 84 }, array); Assert.Equal(new[] { 42, 84 }, array);
} }
[Fact] private IActionResult ActionWithArrayParameter(string[] parameter) => null;
public async Task ArrayModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
public async Task ArrayModelBinder_CreatesEmptyCollection_IfIsTopLevelObject(
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{ {
// Arrange // Arrange
var binder = new ArrayModelBinder<string>( var binder = new ArrayModelBinder<string>(
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance), new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
NullLoggerFactory.Instance); NullLoggerFactory.Instance,
allowValidatingTopLevelNodes);
var bindingContext = CreateContext(); var bindingContext = CreateContext();
bindingContext.IsTopLevelObject = true; bindingContext.IsTopLevelObject = true;
@ -57,7 +66,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
bindingContext.ModelName = "modelName"; bindingContext.ModelName = "modelName";
var metadataProvider = new TestModelMetadataProvider(); var metadataProvider = new TestModelMetadataProvider();
bindingContext.ModelMetadata = metadataProvider.GetMetadataForType(typeof(string[])); var parameter = typeof(ArrayModelBinderTest)
.GetMethod(nameof(ActionWithArrayParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForParameter(parameter);
bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>()); bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
@ -67,22 +82,74 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert // Assert
Assert.Empty(Assert.IsType<string[]>(bindingContext.Result.Model)); Assert.Empty(Assert.IsType<string[]>(bindingContext.Result.Model));
Assert.True(bindingContext.Result.IsModelSet); Assert.True(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
} }
[Theory] [Fact]
[InlineData("")] public async Task ArrayModelBinder_CreatesEmptyCollectionAndAddsError_IfIsTopLevelObject()
[InlineData("param")]
public async Task ArrayModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix)
{ {
// Arrange // Arrange
var binder = new ArrayModelBinder<string>( var binder = new ArrayModelBinder<string>(
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance), new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
NullLoggerFactory.Instance); NullLoggerFactory.Instance,
allowValidatingTopLevelNodes: true);
var bindingContext = CreateContext();
bindingContext.IsTopLevelObject = true;
bindingContext.FieldName = "fieldName";
bindingContext.ModelName = "modelName";
var metadataProvider = new TestModelMetadataProvider();
var parameter = typeof(ArrayModelBinderTest)
.GetMethod(nameof(ActionWithArrayParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = true);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForParameter(parameter);
bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.Empty(Assert.IsType<string[]>(bindingContext.Result.Model));
Assert.True(bindingContext.Result.IsModelSet);
var keyValuePair = Assert.Single(bindingContext.ModelState);
Assert.Equal("modelName", keyValuePair.Key);
var error = Assert.Single(keyValuePair.Value.Errors);
Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
}
[Theory]
[InlineData("", false, false)]
[InlineData("", true, false)]
[InlineData("", false, true)]
[InlineData("", true, true)]
[InlineData("param", false, false)]
[InlineData("param", true, false)]
[InlineData("param", false, true)]
[InlineData("param", true, true)]
public async Task ArrayModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(
string prefix,
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{
// Arrange
var binder = new ArrayModelBinder<string>(
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes);
var bindingContext = CreateContext(); var bindingContext = CreateContext();
bindingContext.ModelName = ModelNames.CreatePropertyModelName(prefix, "ArrayProperty"); bindingContext.ModelName = ModelNames.CreatePropertyModelName(prefix, "ArrayProperty");
var metadataProvider = new TestModelMetadataProvider(); var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty(typeof(ModelWithArrayProperty), nameof(ModelWithArrayProperty.ArrayProperty))
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForProperty( bindingContext.ModelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithArrayProperty), typeof(ModelWithArrayProperty),
nameof(ModelWithArrayProperty.ArrayProperty)); nameof(ModelWithArrayProperty.ArrayProperty));
@ -94,6 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert // Assert
Assert.False(bindingContext.Result.IsModelSet); Assert.False(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
} }
public static TheoryData<int[]> ArrayModelData public static TheoryData<int[]> ArrayModelData
@ -177,23 +245,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
private static DefaultModelBindingContext GetBindingContext(IValueProvider valueProvider) private static DefaultModelBindingContext GetBindingContext(IValueProvider valueProvider)
{ {
var bindingContext = new DefaultModelBindingContext() var bindingContext = CreateContext();
{ bindingContext.ModelName = "someName";
ModelName = "someName", bindingContext.ValueProvider = valueProvider;
ModelState = new ModelStateDictionary(),
ValueProvider = valueProvider,
};
return bindingContext; return bindingContext;
} }
private static DefaultModelBindingContext CreateContext() private static DefaultModelBindingContext CreateContext()
{ {
var modelBindingContext = new DefaultModelBindingContext() var actionContext = new ActionContext
{ {
ActionContext = new ActionContext() HttpContext = new DefaultHttpContext(),
{ };
HttpContext = new DefaultHttpContext(), var modelBindingContext = new DefaultModelBindingContext
}, {
ActionContext = actionContext,
ModelState = actionContext.ModelState,
}; };
return modelBindingContext; return modelBindingContext;

View File

@ -66,6 +66,31 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.IsType<CollectionModelBinder<int>>(result); Assert.IsType<CollectionModelBinder<int>>(result);
} }
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Create_ForSupportedType_ReturnsBinder_WithExpectedAllowValidatingTopLevelNodes(
bool allowValidatingTopLevelNodes)
{
// Arrange
var provider = new CollectionModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(List<int>));
context.MvcOptions.AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
context.OnCreatingBinder(m =>
{
Assert.Equal(typeof(int), m.ModelType);
return Mock.Of<IModelBinder>();
});
// Act
var result = provider.GetBinder(context);
// Assert
var binder = Assert.IsType<CollectionModelBinder<int>>(result);
Assert.Equal(allowValidatingTopLevelNodes, binder.AllowValidatingTopLevelNodes);
}
private class Person private class Person
{ {
public string Name { get; set; } public string Name { get; set; }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
@ -211,13 +212,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.Empty(boundCollection.Model); Assert.Empty(boundCollection.Model);
} }
[Fact] private IActionResult ActionWithListParameter(List<string> parameter) => null;
public async Task CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
public async Task CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObject(
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{ {
// Arrange // Arrange
var binder = new CollectionModelBinder<string>( var binder = new CollectionModelBinder<string>(
new StubModelBinder(result: ModelBindingResult.Failed()), new StubModelBinder(result: ModelBindingResult.Failed()),
NullLoggerFactory.Instance); NullLoggerFactory.Instance,
allowValidatingTopLevelNodes);
var bindingContext = CreateContext(); var bindingContext = CreateContext();
bindingContext.IsTopLevelObject = true; bindingContext.IsTopLevelObject = true;
@ -226,7 +235,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
bindingContext.ModelName = "modelName"; bindingContext.ModelName = "modelName";
var metadataProvider = new TestModelMetadataProvider(); var metadataProvider = new TestModelMetadataProvider();
bindingContext.ModelMetadata = metadataProvider.GetMetadataForType(typeof(List<string>)); var parameter = typeof(CollectionModelBinderTest)
.GetMethod(nameof(ActionWithListParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForParameter(parameter);
bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>()); bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
@ -236,6 +251,45 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert // Assert
Assert.Empty(Assert.IsType<List<string>>(bindingContext.Result.Model)); Assert.Empty(Assert.IsType<List<string>>(bindingContext.Result.Model));
Assert.True(bindingContext.Result.IsModelSet); Assert.True(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
}
[Fact]
public async Task CollectionModelBinder_CreatesEmptyCollectionAndAddsError_IfIsTopLevelObject()
{
// Arrange
var binder = new CollectionModelBinder<string>(
new StubModelBinder(result: ModelBindingResult.Failed()),
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes: true);
var bindingContext = CreateContext();
bindingContext.IsTopLevelObject = true;
bindingContext.FieldName = "fieldName";
bindingContext.ModelName = "modelName";
var metadataProvider = new TestModelMetadataProvider();
var parameter = typeof(CollectionModelBinderTest)
.GetMethod(nameof(ActionWithListParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = true);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForParameter(parameter);
bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.Empty(Assert.IsType<List<string>>(bindingContext.Result.Model));
Assert.True(bindingContext.Result.IsModelSet);
var keyValuePair = Assert.Single(bindingContext.ModelState);
Assert.Equal("modelName", keyValuePair.Key);
var error = Assert.Single(keyValuePair.Value.Errors);
Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
} }
// Setup like CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObject except // Setup like CollectionModelBinder_CreatesEmptyCollection_IfIsTopLevelObject except
@ -272,19 +326,32 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
} }
[Theory] [Theory]
[InlineData("")] [InlineData("", false, false)]
[InlineData("param")] [InlineData("", true, false)]
public async Task CollectionModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix) [InlineData("", false, true)]
[InlineData("", true, true)]
[InlineData("param", false, false)]
[InlineData("param", true, false)]
[InlineData("param", false, true)]
[InlineData("param", true, true)]
public async Task CollectionModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(
string prefix,
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{ {
// Arrange // Arrange
var binder = new CollectionModelBinder<string>( var binder = new CollectionModelBinder<string>(
new StubModelBinder(result: ModelBindingResult.Failed()), new StubModelBinder(result: ModelBindingResult.Failed()),
NullLoggerFactory.Instance); NullLoggerFactory.Instance,
allowValidatingTopLevelNodes);
var bindingContext = CreateContext(); var bindingContext = CreateContext();
bindingContext.ModelName = ModelNames.CreatePropertyModelName(prefix, "ListProperty"); bindingContext.ModelName = ModelNames.CreatePropertyModelName(prefix, "ListProperty");
var metadataProvider = new TestModelMetadataProvider(); var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty(typeof(ModelWithListProperty), nameof(ModelWithListProperty.ListProperty))
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForProperty( bindingContext.ModelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithListProperty), typeof(ModelWithListProperty),
nameof(ModelWithListProperty.ListProperty)); nameof(ModelWithListProperty.ListProperty));
@ -296,6 +363,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert // Assert
Assert.False(bindingContext.Result.IsModelSet); Assert.False(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
} }
// Model type -> can create instance. // Model type -> can create instance.
@ -365,15 +433,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
typeof(ModelWithIListProperty), typeof(ModelWithIListProperty),
nameof(ModelWithIListProperty.ListProperty)); nameof(ModelWithIListProperty.ListProperty));
var bindingContext = new DefaultModelBindingContext var bindingContext = CreateContext();
{ bindingContext.FieldName = "testfieldname";
ModelMetadata = metadata, bindingContext.ModelName = "someName";
ModelName = "someName", bindingContext.ModelMetadata = metadata;
ModelState = new ModelStateDictionary(), bindingContext.ValueProvider = valueProvider;
ValueProvider = valueProvider,
ValidationState = new ValidationStateDictionary(),
FieldName = "testfieldname",
};
return bindingContext; return bindingContext;
} }
@ -412,12 +476,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
private static DefaultModelBindingContext CreateContext() private static DefaultModelBindingContext CreateContext()
{ {
var actionContext = new ActionContext()
{
HttpContext = new DefaultHttpContext(),
};
var modelBindingContext = new DefaultModelBindingContext() var modelBindingContext = new DefaultModelBindingContext()
{ {
ActionContext = new ActionContext() ActionContext = actionContext,
{ ModelState = actionContext.ModelState,
HttpContext = new DefaultHttpContext(), ValidationState = new ValidationStateDictionary(),
},
}; };
return modelBindingContext; return modelBindingContext;

View File

@ -55,6 +55,38 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.IsType<ComplexTypeModelBinder>(result); Assert.IsType<ComplexTypeModelBinder>(result);
} }
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Create_ForSupportedType_ReturnsBinder_WithExpectedAllowValidatingTopLevelNodes(
bool allowValidatingTopLevelNodes)
{
// Arrange
var provider = new ComplexTypeModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(Person));
context.MvcOptions.AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
context.OnCreatingBinder(m =>
{
if (m.ModelType == typeof(int) || m.ModelType == typeof(string))
{
return Mock.Of<IModelBinder>();
}
else
{
Assert.False(true, "Not the right model type");
return null;
}
});
// Act
var result = provider.GetBinder(context);
// Assert
var binder = Assert.IsType<ComplexTypeModelBinder>(result);
Assert.Equal(allowValidatingTopLevelNodes, binder.AllowValidatingTopLevelNodes);
}
private class Person private class Person
{ {
public string Name { get; set; } public string Name { get; set; }

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -275,8 +276,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.Equal(expectedCanCreate, canCreate); Assert.Equal(expectedCanCreate, canCreate);
} }
[Fact] private IActionResult ActionWithComplexParameter(Person parameter) => null;
public async Task BindModelAsync_CreatesModel_IfIsTopLevelObject()
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
public async Task BindModelAsync_CreatesModel_IfIsTopLevelObject(
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{ {
// Arrange // Arrange
var mockValueProvider = new Mock<IValueProvider>(); var mockValueProvider = new Mock<IValueProvider>();
@ -287,6 +295,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Mock binder fails to bind all properties. // Mock binder fails to bind all properties.
var mockBinder = new StubModelBinder(); var mockBinder = new StubModelBinder();
var parameter = typeof(ComplexTypeModelBinderTest)
.GetMethod(nameof(ActionWithComplexParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
var metadata = metadataProvider.GetMetadataForParameter(parameter);
var bindingContext = new DefaultModelBindingContext var bindingContext = new DefaultModelBindingContext
{ {
IsTopLevelObject = true, IsTopLevelObject = true,
@ -298,7 +314,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var model = new Person(); var model = new Person();
var testableBinder = new Mock<TestableComplexTypeModelBinder> { CallBase = true }; var testableBinder = new Mock<TestableComplexTypeModelBinder>(allowValidatingTopLevelNodes)
{
CallBase = true
};
testableBinder testableBinder
.Setup(o => o.CreateModelPublic(bindingContext)) .Setup(o => o.CreateModelPublic(bindingContext))
.Returns(model) .Returns(model)
@ -312,11 +331,149 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert // Assert
Assert.True(bindingContext.Result.IsModelSet); Assert.True(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
var returnedPerson = Assert.IsType<Person>(bindingContext.Result.Model); var returnedPerson = Assert.IsType<Person>(bindingContext.Result.Model);
Assert.Same(model, returnedPerson); Assert.Same(model, returnedPerson);
testableBinder.Verify(); testableBinder.Verify();
} }
[Fact]
public async Task BindModelAsync_CreatesModelAndAddsError_IfIsTopLevelObject_WithNoData()
{
// Arrange
var parameter = typeof(ComplexTypeModelBinderTest)
.GetMethod(nameof(ActionWithComplexParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = true);
var metadata = metadataProvider.GetMetadataForParameter(parameter);
var bindingContext = new DefaultModelBindingContext
{
IsTopLevelObject = true,
FieldName = "fieldName",
ModelMetadata = metadata,
ModelName = string.Empty,
ValueProvider = new TestValueProvider(new Dictionary<string, object>()),
ModelState = new ModelStateDictionary(),
};
// Mock binder fails to bind all properties.
var innerBinder = new StubModelBinder();
var binders = new Dictionary<ModelMetadata, IModelBinder>();
foreach (var property in metadataProvider.GetMetadataForProperties(typeof(Person)))
{
binders.Add(property, innerBinder);
}
var binder = new ComplexTypeModelBinder(
binders,
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes: true);
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(bindingContext.Result.IsModelSet);
Assert.IsType<Person>(bindingContext.Result.Model);
var keyValuePair = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, keyValuePair.Key);
var error = Assert.Single(keyValuePair.Value.Errors);
Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
}
private IActionResult ActionWithNoSettablePropertiesParameter(PersonWithNoProperties parameter) => null;
[Fact]
public async Task BindModelAsync_CreatesModelAndAddsError_IfIsTopLevelObject_WithNoSettableProperties()
{
// Arrange
var parameter = typeof(ComplexTypeModelBinderTest)
.GetMethod(
nameof(ActionWithNoSettablePropertiesParameter),
BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = true);
var metadata = metadataProvider.GetMetadataForParameter(parameter);
var bindingContext = new DefaultModelBindingContext
{
IsTopLevelObject = true,
FieldName = "fieldName",
ModelMetadata = metadata,
ModelName = string.Empty,
ValueProvider = new TestValueProvider(new Dictionary<string, object>()),
ModelState = new ModelStateDictionary(),
};
var binder = new ComplexTypeModelBinder(
new Dictionary<ModelMetadata, IModelBinder>(),
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes: true);
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(bindingContext.Result.IsModelSet);
Assert.IsType<PersonWithNoProperties>(bindingContext.Result.Model);
var keyValuePair = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, keyValuePair.Key);
var error = Assert.Single(keyValuePair.Value.Errors);
Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
}
private IActionResult ActionWithAllPropertiesExcludedParameter(PersonWithAllPropertiesExcluded parameter) => null;
[Fact]
public async Task BindModelAsync_CreatesModelAndAddsError_IfIsTopLevelObject_WithAllPropertiesExcluded()
{
// Arrange
var parameter = typeof(ComplexTypeModelBinderTest)
.GetMethod(
nameof(ActionWithAllPropertiesExcludedParameter),
BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = true);
var metadata = metadataProvider.GetMetadataForParameter(parameter);
var bindingContext = new DefaultModelBindingContext
{
IsTopLevelObject = true,
FieldName = "fieldName",
ModelMetadata = metadata,
ModelName = string.Empty,
ValueProvider = new TestValueProvider(new Dictionary<string, object>()),
ModelState = new ModelStateDictionary(),
};
var binder = new ComplexTypeModelBinder(
new Dictionary<ModelMetadata, IModelBinder>(),
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes: true);
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(bindingContext.Result.IsModelSet);
Assert.IsType<PersonWithAllPropertiesExcluded>(bindingContext.Result.Model);
var keyValuePair = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, keyValuePair.Key);
var error = Assert.Single(keyValuePair.Value.Errors);
Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
}
[Theory] [Theory]
[InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyInt), false)] // read-only value type [InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyInt), false)] // read-only value type
[InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyObject), true)] [InlineData(nameof(MyModelTestingCanUpdateProperty.ReadOnlyObject), true)]
@ -644,7 +801,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var modelError = Assert.Single(entry.Errors); var modelError = Assert.Single(entry.Errors);
Assert.Null(modelError.Exception); Assert.Null(modelError.Exception);
Assert.NotNull(modelError.ErrorMessage); Assert.NotNull(modelError.ErrorMessage);
Assert.Equal("A value for the 'Age' property was not provided.", modelError.ErrorMessage); Assert.Equal("A value for the 'Age' parameter or property was not provided.", modelError.ErrorMessage);
} }
[Fact] [Fact]
@ -678,7 +835,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var modelError = Assert.Single(entry.Errors); var modelError = Assert.Single(entry.Errors);
Assert.Null(modelError.Exception); Assert.Null(modelError.Exception);
Assert.NotNull(modelError.ErrorMessage); Assert.NotNull(modelError.ErrorMessage);
Assert.Equal("A value for the 'Age' property was not provided.", modelError.ErrorMessage); Assert.Equal("A value for the 'Age' parameter or property was not provided.", modelError.ErrorMessage);
} }
[Fact] [Fact]
@ -1203,6 +1360,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
public string name = null; public string name = null;
} }
private class PersonWithAllPropertiesExcluded
{
[BindNever]
public DateTime DateOfBirth { get; set; }
[BindNever]
public DateTime? DateOfDeath { get; set; }
[BindNever]
public string FirstName { get; set; }
[BindNever]
public string LastName { get; set; }
public string NonUpdateableProperty { get; private set; }
}
private class PersonWithBindExclusion private class PersonWithBindExclusion
{ {
[BindNever] [BindNever]
@ -1405,13 +1579,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{ {
} }
public TestableComplexTypeModelBinder(bool allowValidatingTopLevelNodes)
: this(new Dictionary<ModelMetadata, IModelBinder>(), allowValidatingTopLevelNodes)
{
}
public TestableComplexTypeModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders) public TestableComplexTypeModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders)
: base(propertyBinders, NullLoggerFactory.Instance) : base(propertyBinders, NullLoggerFactory.Instance)
{ {
Results = new Dictionary<ModelMetadata, ModelBindingResult>();
} }
public Dictionary<ModelMetadata, ModelBindingResult> Results { get; } public TestableComplexTypeModelBinder(
IDictionary<ModelMetadata, IModelBinder> propertyBinders,
bool allowValidatingTopLevelNodes)
: base(propertyBinders, NullLoggerFactory.Instance, allowValidatingTopLevelNodes)
{
}
public Dictionary<ModelMetadata, ModelBindingResult> Results { get; } = new Dictionary<ModelMetadata, ModelBindingResult>();
public virtual Task BindPropertyPublic(ModelBindingContext bindingContext) public virtual Task BindPropertyPublic(ModelBindingContext bindingContext)
{ {

View File

@ -60,6 +60,39 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.IsType<DictionaryModelBinder<string, int>>(result); Assert.IsType<DictionaryModelBinder<string, int>>(result);
} }
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Create_ForDictionaryType_ReturnsBinder_WithExpectedAllowValidatingTopLevelNodes(
bool allowValidatingTopLevelNodes)
{
// Arrange
var provider = new DictionaryModelBinderProvider();
var context = new TestModelBinderProviderContext(typeof(Dictionary<string, string>));
context.MvcOptions.AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
context.OnCreatingBinder(m =>
{
if (m.ModelType == typeof(KeyValuePair<string, string>) || m.ModelType == typeof(string))
{
return Mock.Of<IModelBinder>();
}
else
{
Assert.False(true, "Not the right model type");
return null;
}
});
// Act
var result = provider.GetBinder(context);
// Assert
var binder = Assert.IsType<DictionaryModelBinder<string, string>>(result);
Assert.Equal(allowValidatingTopLevelNodes, binder.AllowValidatingTopLevelNodes);
Assert.False(((CollectionModelBinder<KeyValuePair<string, string>>)binder).AllowValidatingTopLevelNodes);
}
private class Person private class Person
{ {
public string Name { get; set; } public string Name { get; set; }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
@ -343,14 +344,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.Equal(expectedDictionary, resultDictionary); Assert.Equal(expectedDictionary, resultDictionary);
} }
[Fact] private IActionResult ActionWithDictionaryParameter(Dictionary<string, string> parameter) => null;
public async Task DictionaryModelBinder_CreatesEmptyCollection_IfIsTopLevelObject()
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
public async Task DictionaryModelBinder_CreatesEmptyCollection_IfIsTopLevelObject(
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{ {
// Arrange // Arrange
var binder = new DictionaryModelBinder<string, string>( var binder = new DictionaryModelBinder<string, string>(
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance), new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance), new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
NullLoggerFactory.Instance); NullLoggerFactory.Instance,
allowValidatingTopLevelNodes);
var bindingContext = CreateContext(); var bindingContext = CreateContext();
bindingContext.IsTopLevelObject = true; bindingContext.IsTopLevelObject = true;
@ -359,7 +368,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
bindingContext.ModelName = "modelName"; bindingContext.ModelName = "modelName";
var metadataProvider = new TestModelMetadataProvider(); var metadataProvider = new TestModelMetadataProvider();
bindingContext.ModelMetadata = metadataProvider.GetMetadataForType(typeof(Dictionary<string, string>)); var parameter = typeof(DictionaryModelBinderTest)
.GetMethod(nameof(ActionWithDictionaryParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForParameter(parameter);
bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>()); bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
@ -369,23 +384,78 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert // Assert
Assert.Empty(Assert.IsType<Dictionary<string, string>>(bindingContext.Result.Model)); Assert.Empty(Assert.IsType<Dictionary<string, string>>(bindingContext.Result.Model));
Assert.True(bindingContext.Result.IsModelSet); Assert.True(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
}
[Fact]
public async Task DictionaryModelBinder_CreatesEmptyCollectionAndAddsError_IfIsTopLevelObject()
{
// Arrange
var binder = new DictionaryModelBinder<string, string>(
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
new SimpleTypeModelBinder(typeof(string), NullLoggerFactory.Instance),
NullLoggerFactory.Instance,
allowValidatingTopLevelNodes: true);
var bindingContext = CreateContext();
bindingContext.IsTopLevelObject = true;
bindingContext.FieldName = "fieldName";
bindingContext.ModelName = "modelName";
var metadataProvider = new TestModelMetadataProvider();
var parameter = typeof(DictionaryModelBinderTest)
.GetMethod(nameof(ActionWithDictionaryParameter), BindingFlags.Instance | BindingFlags.NonPublic)
.GetParameters()[0];
metadataProvider
.ForParameter(parameter)
.BindingDetails(b => b.IsBindingRequired = true);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForParameter(parameter);
bindingContext.ValueProvider = new TestValueProvider(new Dictionary<string, object>());
// Act
await binder.BindModelAsync(bindingContext);
// Assert
Assert.Empty(Assert.IsType<Dictionary<string, string>>(bindingContext.Result.Model));
Assert.True(bindingContext.Result.IsModelSet);
var keyValuePair = Assert.Single(bindingContext.ModelState);
Assert.Equal("modelName", keyValuePair.Key);
var error = Assert.Single(keyValuePair.Value.Errors);
Assert.Equal("A value for the 'fieldName' parameter or property was not provided.", error.ErrorMessage);
} }
[Theory] [Theory]
[InlineData("")] [InlineData("", false, false)]
[InlineData("param")] [InlineData("", true, false)]
public async Task DictionaryModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(string prefix) [InlineData("", false, true)]
[InlineData("", true, true)]
[InlineData("param", false, false)]
[InlineData("param", true, false)]
[InlineData("param", false, true)]
[InlineData("param", true, true)]
public async Task DictionaryModelBinder_DoesNotCreateCollection_IfNotIsTopLevelObject(
string prefix,
bool allowValidatingTopLevelNodes,
bool isBindingRequired)
{ {
// Arrange // Arrange
var binder = new DictionaryModelBinder<int, int>( var binder = new DictionaryModelBinder<int, int>(
new SimpleTypeModelBinder(typeof(int), NullLoggerFactory.Instance), new SimpleTypeModelBinder(typeof(int), NullLoggerFactory.Instance),
new SimpleTypeModelBinder(typeof(int), NullLoggerFactory.Instance), new SimpleTypeModelBinder(typeof(int), NullLoggerFactory.Instance),
NullLoggerFactory.Instance); NullLoggerFactory.Instance,
allowValidatingTopLevelNodes);
var bindingContext = CreateContext(); var bindingContext = CreateContext();
bindingContext.ModelName = ModelNames.CreatePropertyModelName(prefix, "ListProperty"); bindingContext.ModelName = ModelNames.CreatePropertyModelName(prefix, "ListProperty");
var metadataProvider = new TestModelMetadataProvider(); var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty(
typeof(ModelWithDictionaryProperties),
nameof(ModelWithDictionaryProperties.DictionaryProperty))
.BindingDetails(b => b.IsBindingRequired = isBindingRequired);
bindingContext.ModelMetadata = metadataProvider.GetMetadataForProperty( bindingContext.ModelMetadata = metadataProvider.GetMetadataForProperty(
typeof(ModelWithDictionaryProperties), typeof(ModelWithDictionaryProperties),
nameof(ModelWithDictionaryProperties.DictionaryProperty)); nameof(ModelWithDictionaryProperties.DictionaryProperty));
@ -397,6 +467,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
// Assert // Assert
Assert.False(bindingContext.Result.IsModelSet); Assert.False(bindingContext.Result.IsModelSet);
Assert.Equal(0, bindingContext.ModelState.ErrorCount);
} }
// Model type -> can create instance. // Model type -> can create instance.
@ -436,13 +507,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
private static DefaultModelBindingContext CreateContext() private static DefaultModelBindingContext CreateContext()
{ {
var actionContext = new ActionContext()
{
HttpContext = new DefaultHttpContext(),
};
var modelBindingContext = new DefaultModelBindingContext() var modelBindingContext = new DefaultModelBindingContext()
{ {
ActionContext = new ActionContext() ActionContext = actionContext,
{ ModelState = actionContext.ModelState,
HttpContext = new DefaultHttpContext(),
},
ModelState = new ModelStateDictionary(),
ValidationState = new ValidationStateDictionary(), ValidationState = new ValidationStateDictionary(),
}; };
@ -495,14 +567,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
valueProvider.Add(kvp.Key, string.Empty); valueProvider.Add(kvp.Key, string.Empty);
} }
var bindingContext = new DefaultModelBindingContext var bindingContext = CreateContext();
{ bindingContext.ModelMetadata = metadata;
ModelMetadata = metadata, bindingContext.ModelName = "someName";
ModelName = "someName", bindingContext.ValueProvider = valueProvider;
ModelState = new ModelStateDictionary(),
ValueProvider = valueProvider,
ValidationState = new ValidationStateDictionary(),
};
return bindingContext; return bindingContext;
} }

View File

@ -36,13 +36,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
BindingSource = Metadata.BindingSource, BindingSource = Metadata.BindingSource,
PropertyFilterProvider = Metadata.PropertyFilterProvider, PropertyFilterProvider = Metadata.PropertyFilterProvider,
}; };
Services = GetServices();
(Services, MvcOptions) = GetServicesAndOptions();
} }
public override BindingInfo BindingInfo => _bindingInfo; public override BindingInfo BindingInfo => _bindingInfo;
public override ModelMetadata Metadata { get; } public override ModelMetadata Metadata { get; }
public MvcOptions MvcOptions { get; }
public override IModelMetadataProvider MetadataProvider { get; } public override IModelMetadataProvider MetadataProvider { get; }
public override IServiceProvider Services { get; } public override IServiceProvider Services { get; }
@ -77,12 +80,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
_binderCreators.Add((m) => m.Equals(metadata) ? binderCreator() : null); _binderCreators.Add((m) => m.Equals(metadata) ? binderCreator() : null);
} }
private static IServiceProvider GetServices() private static (IServiceProvider, MvcOptions) GetServicesAndOptions()
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory, NullLoggerFactory>(); services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
services.AddSingleton(Options.Create(new MvcOptions()));
return services.BuildServiceProvider(); var mvcOptions = new MvcOptions();
services.AddSingleton(Options.Create(mvcOptions));
return (services.BuildServiceProvider(), mvcOptions);
} }
} }
} }

View File

@ -89,10 +89,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
"The RequiredProp field is required.", "The RequiredProp field is required.",
errors["RequiredProp"]); errors["RequiredProp"]);
Assert.Equal( Assert.Equal(
"A value for the 'BindRequiredProp' property was not provided.", "A value for the 'BindRequiredProp' parameter or property was not provided.",
errors["BindRequiredProp"]); errors["BindRequiredProp"]);
Assert.Equal( Assert.Equal(
"A value for the 'RequiredAndBindRequiredProp' property was not provided.", "A value for the 'RequiredAndBindRequiredProp' parameter or property was not provided.",
errors["RequiredAndBindRequiredProp"]); errors["RequiredAndBindRequiredProp"]);
Assert.Equal( Assert.Equal(
"The field OptionalStringLengthProp must be a string with a maximum length of 5.", "The field OptionalStringLengthProp must be a string with a maximum length of 5.",
@ -104,10 +104,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
"The requiredParam field is required.", "The requiredParam field is required.",
errors["requiredParam"]); errors["requiredParam"]);
Assert.Equal( Assert.Equal(
"A value for the 'bindRequiredParam' property was not provided.", "A value for the 'bindRequiredParam' parameter or property was not provided.",
errors["bindRequiredParam"]); errors["bindRequiredParam"]);
Assert.Equal( Assert.Equal(
"A value for the 'requiredAndBindRequiredParam' property was not provided.", "A value for the 'requiredAndBindRequiredParam' parameter or property was not provided.",
errors["requiredAndBindRequiredParam"]); errors["requiredAndBindRequiredParam"]);
Assert.Equal( Assert.Equal(
"The field optionalStringLengthParam must be a string with a maximum length of 5.", "The field optionalStringLengthParam must be a string with a maximum length of 5.",

View File

@ -333,13 +333,13 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue); Assert.Null(entry.RawValue);
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
var error = Assert.Single(entry.Errors); var error = Assert.Single(entry.Errors);
Assert.Equal("A value for the 'Name' property was not provided.", error.ErrorMessage); Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[1].Name").Value; entry = Assert.Single(modelState, kvp => kvp.Key == "parameter[1].Name").Value;
Assert.Null(entry.RawValue); Assert.Null(entry.RawValue);
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
error = Assert.Single(entry.Errors); error = Assert.Single(entry.Errors);
Assert.Equal("A value for the 'Name' property was not provided.", error.ErrorMessage); Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
} }
[Fact] [Fact]

View File

@ -2123,7 +2123,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue); Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue); Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["Customer"].Errors); var error = Assert.Single(modelState["Customer"].Errors);
Assert.Equal("A value for the 'Customer' property was not provided.", error.ErrorMessage); Assert.Equal("A value for the 'Customer' parameter or property was not provided.", error.ErrorMessage);
} }
[Fact] [Fact]
@ -2245,7 +2245,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue); Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue); Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["parameter.Customer.Name"].Errors); var error = Assert.Single(modelState["parameter.Customer.Name"].Errors);
Assert.Equal("A value for the 'Name' property was not provided.", error.ErrorMessage); Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
} }
[Fact] [Fact]
@ -2299,7 +2299,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue); Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue); Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["Customer.Name"].Errors); var error = Assert.Single(modelState["Customer.Name"].Errors);
Assert.Equal("A value for the 'Name' property was not provided.", error.ErrorMessage); Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
} }
[Fact] [Fact]
@ -2357,7 +2357,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue); Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue); Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["customParameter.Customer.Name"].Errors); var error = Assert.Single(modelState["customParameter.Customer.Name"].Errors);
Assert.Equal("A value for the 'Name' property was not provided.", error.ErrorMessage); Assert.Equal("A value for the 'Name' parameter or property was not provided.", error.ErrorMessage);
} }
private class Order12 private class Order12
@ -2411,7 +2411,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue); Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue); Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["ProductName"].Errors); var error = Assert.Single(modelState["ProductName"].Errors);
Assert.Equal("A value for the 'ProductName' property was not provided.", error.ErrorMessage); Assert.Equal("A value for the 'ProductName' parameter or property was not provided.", error.ErrorMessage);
} }
[Fact] [Fact]
@ -2463,7 +2463,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue); Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue); Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["customParameter.ProductName"].Errors); var error = Assert.Single(modelState["customParameter.ProductName"].Errors);
Assert.Equal("A value for the 'ProductName' property was not provided.", error.ErrorMessage); Assert.Equal("A value for the 'ProductName' parameter or property was not provided.", error.ErrorMessage);
} }
[Fact] [Fact]
@ -2563,7 +2563,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue); Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue); Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["OrderIds"].Errors); var error = Assert.Single(modelState["OrderIds"].Errors);
Assert.Equal("A value for the 'OrderIds' property was not provided.", error.ErrorMessage); Assert.Equal("A value for the 'OrderIds' parameter or property was not provided.", error.ErrorMessage);
} }
[Fact] [Fact]
@ -2615,7 +2615,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Null(entry.RawValue); Assert.Null(entry.RawValue);
Assert.Null(entry.AttemptedValue); Assert.Null(entry.AttemptedValue);
var error = Assert.Single(modelState["customParameter.OrderIds"].Errors); var error = Assert.Single(modelState["customParameter.OrderIds"].Errors);
Assert.Equal("A value for the 'OrderIds' property was not provided.", error.ErrorMessage); Assert.Equal("A value for the 'OrderIds' parameter or property was not provided.", error.ErrorMessage);
} }
[Fact] [Fact]